diff --git a/src/hooks/useCommcoach.ts b/src/hooks/useCommcoach.ts index 6dc65cc..a7e5eab 100644 --- a/src/hooks/useCommcoach.ts +++ b/src/hooks/useCommcoach.ts @@ -49,8 +49,12 @@ export interface CommcoachHookReturn { isMuted: boolean; setMuted: (muted: boolean) => void; stopTts: () => void; + resumeTts: () => void; + wasInterrupted: boolean; isTtsPlayingRef: MutableRefObject; + actionLoading: string | null; + toggleTaskStatus: (taskId: string, currentStatus: string) => Promise; addTask: (title: string, description?: string) => Promise; removeTask: (taskId: string) => Promise; @@ -81,10 +85,13 @@ export function useCommcoach(): CommcoachHookReturn { const [inputValue, setInputValue] = useState(''); const [isMuted, setIsMuted] = useState(false); + const [wasInterrupted, setWasInterrupted] = useState(false); + const [actionLoading, setActionLoading] = useState(null); const isMountedRef = useRef(true); const currentAudioRef = useRef(null); const isTtsPlayingRef = useRef(false); + const lastTtsAudioRef = useRef(null); useEffect(() => { isMountedRef.current = true; return () => { isMountedRef.current = false; }; }, []); @@ -108,6 +115,8 @@ export function useCommcoach(): CommcoachHookReturn { currentAudioRef.current.pause(); currentAudioRef.current = null; } + lastTtsAudioRef.current = audioB64; + setWasInterrupted(false); isTtsPlayingRef.current = true; try { const audio = new Audio(`data:audio/mp3;base64,${audioB64}`); @@ -127,9 +136,18 @@ export function useCommcoach(): CommcoachHookReturn { currentAudioRef.current.pause(); currentAudioRef.current = null; } + if (isTtsPlayingRef.current) { + setWasInterrupted(true); + } isTtsPlayingRef.current = false; }, []); + const resumeTts = useCallback(() => { + if (lastTtsAudioRef.current) { + _playTtsAudio(lastTtsAudioRef.current); + } + }, [_playTtsAudio]); + const selectContext = useCallback(async (contextId: string) => { if (!instanceId) return; setSelectedContextId(contextId); @@ -178,6 +196,7 @@ export function useCommcoach(): CommcoachHookReturn { const createContext = useCallback(async (title: string, description?: string, category?: string, goals?: string[]) => { if (!instanceId) return; + setActionLoading('creating'); try { const created = await createContextApi(request, instanceId, { title, description, category, goals }); if (isMountedRef.current) { @@ -192,11 +211,14 @@ export function useCommcoach(): CommcoachHookReturn { } } catch (err: any) { if (isMountedRef.current) setError(err.message || 'Fehler beim Erstellen des Kontexts'); + } finally { + if (isMountedRef.current) setActionLoading(null); } }, [request, instanceId, refreshContexts]); const archiveContext = useCallback(async (contextId: string) => { if (!instanceId) return; + setActionLoading('archiving'); try { const { archiveContextApi } = await import('../api/commcoachApi'); await archiveContextApi(request, instanceId, contextId); @@ -209,11 +231,14 @@ export function useCommcoach(): CommcoachHookReturn { } } catch (err: any) { if (isMountedRef.current) setError(err.message || 'Fehler beim Archivieren'); + } finally { + if (isMountedRef.current) setActionLoading(null); } }, [request, instanceId, selectedContextId, refreshContexts]); const startSessionCb = useCallback(async (personaId?: string) => { if (!instanceId || !selectedContextId) return; + setActionLoading('starting'); await _unlockAudioForTts(); setError(null); setIsMuted(false); @@ -285,6 +310,8 @@ export function useCommcoach(): CommcoachHookReturn { setError(err.message || 'Fehler beim Starten der Session'); setIsStreaming(false); } + } finally { + if (isMountedRef.current) setActionLoading(null); } }, [instanceId, selectedContextId, _playTtsAudio]); @@ -437,6 +464,7 @@ export function useCommcoach(): CommcoachHookReturn { const completeSessionCb = useCallback(async () => { if (!instanceId || !session) return; + setActionLoading('completing'); try { const completed = await completeSessionApi(request, instanceId, session.id); if (isMountedRef.current) { @@ -445,11 +473,14 @@ export function useCommcoach(): CommcoachHookReturn { } } catch (err: any) { if (isMountedRef.current) setError(err.message || 'Fehler beim Abschliessen'); + } finally { + if (isMountedRef.current) setActionLoading(null); } }, [request, instanceId, session, selectedContextId, selectContext]); const cancelSessionCb = useCallback(async () => { if (!instanceId || !session) return; + setActionLoading('cancelling'); try { await cancelSessionApi(request, instanceId, session.id); if (isMountedRef.current) { @@ -458,11 +489,14 @@ export function useCommcoach(): CommcoachHookReturn { } } catch (err: any) { if (isMountedRef.current) setError(err.message || 'Fehler beim Abbrechen'); + } finally { + if (isMountedRef.current) setActionLoading(null); } }, [request, instanceId, session]); const toggleTaskStatus = useCallback(async (taskId: string, currentStatus: string) => { if (!instanceId) return; + setActionLoading('togglingTask'); const newStatus = currentStatus === 'done' ? 'open' : 'done'; try { const updated = await updateTaskStatusApi(request, instanceId, taskId, newStatus); @@ -471,16 +505,21 @@ export function useCommcoach(): CommcoachHookReturn { } } catch (err: any) { if (isMountedRef.current) setError(err.message); + } finally { + if (isMountedRef.current) setActionLoading(null); } }, [request, instanceId]); const addTask = useCallback(async (title: string, description?: string) => { if (!instanceId || !selectedContextId) return; + setActionLoading('addingTask'); try { const created = await createTaskApi(request, instanceId, selectedContextId, { title, description }); if (isMountedRef.current) setTasks(prev => [created, ...prev]); } catch (err: any) { if (isMountedRef.current) setError(err.message); + } finally { + if (isMountedRef.current) setActionLoading(null); } }, [request, instanceId, selectedContextId]); @@ -504,7 +543,8 @@ export function useCommcoach(): CommcoachHookReturn { selectContext, createContext, archiveContext, startSession: startSessionCb, sendMessage, sendAudio, completeSession: completeSessionCb, cancelSession: cancelSessionCb, - isMuted, setMuted: setIsMuted, stopTts, isTtsPlayingRef, + isMuted, setMuted: setIsMuted, stopTts, resumeTts, wasInterrupted, isTtsPlayingRef, + actionLoading, toggleTaskStatus, addTask, removeTask, refreshContexts, }; diff --git a/src/pages/views/commcoach/CommcoachCoachingView.tsx b/src/pages/views/commcoach/CommcoachCoachingView.tsx index 4c41154..9d0db6f 100644 --- a/src/pages/views/commcoach/CommcoachCoachingView.tsx +++ b/src/pages/views/commcoach/CommcoachCoachingView.tsx @@ -33,9 +33,12 @@ export const CommcoachCoachingView: React.FC = () => { const streamRef = useRef(null); const speechRecognitionRef = useRef(null); const transcriptPartsRef = useRef([]); + const processedResultIndexRef = useRef(0); + const silenceTimerRef = useRef | null>(null); const [isListening, setIsListening] = useState(false); const [isUserSpeaking, setIsUserSpeaking] = useState(false); const [liveTranscript, setLiveTranscript] = useState(''); + const [isTtsPlaying, setIsTtsPlaying] = useState(false); const handleSend = useCallback(async () => { if (!coach.inputValue.trim() || coach.isStreaming) return; @@ -85,6 +88,14 @@ export const CommcoachCoachingView: React.FC = () => { .catch(() => {}); }, [instanceId, request]); + useEffect(() => { + if (!coach.session) return; + const interval = setInterval(() => { + setIsTtsPlaying(coach.isTtsPlayingRef.current); + }, 200); + return () => clearInterval(interval); + }, [coach.session, coach.isTtsPlayingRef]); + useEffect(() => { if (!coach.session || coach.isMuted) { if (speechRecognitionRef.current) { @@ -135,36 +146,54 @@ export const CommcoachCoachingView: React.FC = () => { if (cancelled) return; }; + const SILENCE_TIMEOUT_MS = 5000; + + const _resetSilenceTimer = () => { + if (silenceTimerRef.current) clearTimeout(silenceTimerRef.current); + silenceTimerRef.current = setTimeout(() => { + if (cancelled) return; + setIsUserSpeaking(false); + transcriptPartsRef.current = []; + processedResultIndexRef.current = 0; + setLiveTranscript(''); + }, SILENCE_TIMEOUT_MS); + }; + recognition.onspeechstart = () => { if (cancelled || coach.isTtsPlayingRef.current) return; setIsUserSpeaking(true); transcriptPartsRef.current = []; + processedResultIndexRef.current = 0; setLiveTranscript(''); + _resetSilenceTimer(); }; recognition.onresult = (event: SpeechRecognitionEvent) => { if (cancelled || coach.isTtsPlayingRef.current) return; - const finalized: string[] = []; let currentInterim = ''; - for (let i = 0; i < event.results.length; i++) { + for (let i = processedResultIndexRef.current; i < event.results.length; i++) { const r = event.results[i]; if (r.isFinal) { - finalized.push(r[0].transcript.trim()); + const text = r[0].transcript.trim(); + if (text) transcriptPartsRef.current.push(text); + processedResultIndexRef.current = i + 1; } else { currentInterim = r[0].transcript.trim(); } } - transcriptPartsRef.current = finalized.filter(Boolean); const preview = [...transcriptPartsRef.current, currentInterim].join(' ').trim(); setLiveTranscript(preview); + if (preview) _resetSilenceTimer(); const totalWords = preview.split(/\s+/).filter(Boolean).length; if (totalWords >= MIN_WORDS_TO_INTERRUPT) coach.stopTts(); }; recognition.onspeechend = () => { if (cancelled) return; + if (silenceTimerRef.current) clearTimeout(silenceTimerRef.current); if (coach.isTtsPlayingRef.current) { transcriptPartsRef.current = []; + processedResultIndexRef.current = 0; setLiveTranscript(''); setIsUserSpeaking(false); return; @@ -175,6 +204,7 @@ export const CommcoachCoachingView: React.FC = () => { if (wordCount >= MIN_WORDS_TO_INTERRUPT) coach.sendMessage(fullTranscript); } transcriptPartsRef.current = []; + processedResultIndexRef.current = 0; setLiveTranscript(''); setIsUserSpeaking(false); }; @@ -208,6 +238,7 @@ export const CommcoachCoachingView: React.FC = () => { init(); return () => { cancelled = true; + if (silenceTimerRef.current) clearTimeout(silenceTimerRef.current); if (speechRecognitionRef.current) { try { speechRecognitionRef.current.stop(); @@ -272,17 +303,17 @@ export const CommcoachCoachingView: React.FC = () => { onChange={e => setNewCategory(e.target.value)} > - + - +
- @@ -313,7 +344,7 @@ export const CommcoachCoachingView: React.FC = () => { {personas.length > 0 && (
- +
{personas.map(p => (
)} @@ -352,6 +385,16 @@ export const CommcoachCoachingView: React.FC = () => { Session aktiv - {coach.selectedContext?.title}
+ {isTtsPlaying && ( + + )} + {coach.wasInterrupted && !isTtsPlaying && ( + + )} - -
diff --git a/src/pages/views/commcoach/CommcoachDashboardView.tsx b/src/pages/views/commcoach/CommcoachDashboardView.tsx index d30b8f3..94e8ead 100644 --- a/src/pages/views/commcoach/CommcoachDashboardView.tsx +++ b/src/pages/views/commcoach/CommcoachDashboardView.tsx @@ -30,7 +30,7 @@ export const CommcoachDashboardView: React.FC = () => { } if (!dashboard) { - return
Keine Daten verfuegbar.
; + return
Keine Daten verfügbar.
; } return ( @@ -124,7 +124,7 @@ export const CommcoachDashboardView: React.FC = () => {

Tipp des Tages

-

Konsistenz schlaegt Intensitaet. Auch 10 Minuten taegliches Coaching-Gespraech +

Konsistenz schlägt Intensität. Auch 10 Minuten tägliches Coaching-Gespräch bringt messbare Fortschritte in deiner Kommunikationskompetenz.

@@ -134,10 +134,10 @@ export const CommcoachDashboardView: React.FC = () => { function _categoryLabel(category: string): string { const labels: Record = { - leadership: 'Fuehrung', + leadership: 'Führung', conflict: 'Konflikt', negotiation: 'Verhandlung', - presentation: 'Praesentation', + presentation: 'Präsentation', feedback: 'Feedback', delegation: 'Delegation', changeManagement: 'Change Mgmt', diff --git a/src/pages/views/commcoach/CommcoachDossierView.tsx b/src/pages/views/commcoach/CommcoachDossierView.tsx index 1737590..52c617e 100644 --- a/src/pages/views/commcoach/CommcoachDossierView.tsx +++ b/src/pages/views/commcoach/CommcoachDossierView.tsx @@ -102,7 +102,7 @@ export const CommcoachDossierView: React.FC = () => {
{!coach.selectedContextId ? ( -

Waehle ein Coaching-Thema.

+

Wähle ein Coaching-Thema.

) : (<> {/* Context Header */}
@@ -133,8 +133,12 @@ export const CommcoachDossierView: React.FC = () => { )} -
@@ -178,12 +182,12 @@ export const CommcoachDossierView: React.FC = () => { onChange={e => setNewTaskTitle(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAddTask()} /> - {coach.tasks.length === 0 ? ( -
Noch keine Aufgaben. Der Coach schlaegt waehrend Sessions Aufgaben vor.
+
Noch keine Aufgaben. Der Coach schlägt während Sessions Aufgaben vor.
) : (
{coach.tasks.map(task => ( @@ -317,7 +321,7 @@ export const CommcoachDossierView: React.FC = () => {
{documents.length === 0 ? ( -
Keine Dokumente. Lade Dateien hoch, um sie mit diesem Kontext zu verknuepfen.
+
Keine Dokumente. Lade Dateien hoch, um sie mit diesem Kontext zu verknüpfen.
) : (
{documents.map(doc => ( @@ -374,10 +378,10 @@ function _formatFileSize(bytes: number): string { function _dimensionLabel(dim: string): string { const labels: Record = { - empathy: 'Einfuehlungsvermoegen', + empathy: 'Einfühlungsvermögen', clarity: 'Klarheit', assertiveness: 'Durchsetzung', - listening: 'Zuhoeren', + listening: 'Zuhören', selfReflection: 'Selbstreflexion', }; return labels[dim] || dim;