From 36b8558dd0e3a11c08b4a0da5a41d18df977eb2c Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Wed, 4 Mar 2026 22:53:45 +0100 Subject: [PATCH] =?UTF-8?q?Alle=209=20Fixes=20sind=20implementiert.=20Hier?= =?UTF-8?q?=20die=20Zusammenfassung:=20Fix=201=20--=20Opening-Prompt:=20pr?= =?UTF-8?q?ocessSessionOpening=20in=20serviceCommcoach.py=20pr=C3=BCft=20j?= =?UTF-8?q?etzt=20ob=20es=20die=20erste=20Session=20ist=20(isFirstSession)?= =?UTF-8?q?=20und=20gibt=20der=20AI=20einen=20expliziten=20Prompt,=20der?= =?UTF-8?q?=20das=20Erfinden=20von=20Kontext=20verbietet.=20Fix=202=20--?= =?UTF-8?q?=20Stabiler=20Transcript:=20onresult=20in=20CommcoachCoachingVi?= =?UTF-8?q?ew.tsx=20nutzt=20jetzt=20processedResultIndexRef=20um=20nur=20n?= =?UTF-8?q?eue=20Results=20zu=20verarbeiten.=20Finalisierte=20Teile=20werd?= =?UTF-8?q?en=20stabil=20akkumuliert,=20kein=20Flackern=20mehr.=20Fix=203?= =?UTF-8?q?=20--=20Hintergrundger=C3=A4usche-Timeout:=20Neuer=20silenceTim?= =?UTF-8?q?erRef=20mit=205s=20Timeout.=20Wenn=20nach=20onspeechstart=20kei?= =?UTF-8?q?n=20Text=20kommt,=20wird=20isUserSpeaking=20automatisch=20zur?= =?UTF-8?q?=C3=BCckgesetzt.=20Fix=204=20--=20Stop-Button:=20"Stop"=20Butto?= =?UTF-8?q?n=20erscheint=20im=20Session-Header=20wenn=20TTS=20l=C3=A4uft?= =?UTF-8?q?=20(via=20isTtsPlaying=20State,=20synchronisiert=20per=20200ms?= =?UTF-8?q?=20Interval=20mit=20isTtsPlayingRef).=20Fix=205=20--=20Weitersp?= =?UTF-8?q?rechen-Button:=20lastTtsAudioRef=20speichert=20das=20zuletzt=20?= =?UTF-8?q?gespielte=20Audio.=20stopTts=20setzt=20wasInterrupted=20=3D=20t?= =?UTF-8?q?rue.=20"Weitersprechen"=20Button=20erscheint=20nach=20Unterbrec?= =?UTF-8?q?hung=20und=20spielt=20das=20Audio=20erneut=20ab.=20Fix=206=20--?= =?UTF-8?q?=20Paralleles=20TTS:=20Neue=20=5FgenerateAndEmitTts()=20Hilfsfu?= =?UTF-8?q?nktion.=20In=20processMessage=20und=20processSessionOpening=20w?= =?UTF-8?q?ird=20TTS=20als=20asyncio.create=5Ftask=20parallel=20zu=20=5Fem?= =?UTF-8?q?itChunkedResponse=20gestartet.=20Fix=207=20--=20JSON-Response:?= =?UTF-8?q?=20Die=20AI=20antwortet=20jetzt=20als=20JSON=20mit=20text,=20sp?= =?UTF-8?q?eech,=20documents.=20Neuer=20Prompt-Block=20wird=20in=20buildCo?= =?UTF-8?q?achingSystemPrompt=20angeh=C3=A4ngt.=20=5FparseAiJsonResponse()?= =?UTF-8?q?=20und=20=5FsaveGeneratedDocument()=20im=20Backend.=20processMe?= =?UTF-8?q?ssage=20und=20processSessionOpening=20nutzen=20die=20neue=20Str?= =?UTF-8?q?uktur.=20Fix=208=20--=20Loading-States:=20Neuer=20actionLoading?= =?UTF-8?q?=20State=20in=20useCommcoach.=20Alle=20async=20Funktionen=20set?= =?UTF-8?q?zen=20setActionLoading('key')=20vor=20dem=20Await=20und=20null?= =?UTF-8?q?=20im=20finally.=20Buttons=20zeigen=20Loading-Text=20und=20werd?= =?UTF-8?q?en=20disabled.=20Fix=209=20--=20Umlaute:=20Alle=20deutschen=20S?= =?UTF-8?q?trings=20in=20allen=20CommCoach-Dateien=20(Backend=20+=20Fronte?= =?UTF-8?q?nd)=20korrigiert:=20ae->=C3=A4,=20oe->=C3=B6,=20ue->=C3=BC.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useCommcoach.ts | 42 ++++++++- .../views/commcoach/CommcoachCoachingView.tsx | 87 +++++++++++++++---- .../commcoach/CommcoachDashboardView.tsx | 8 +- .../views/commcoach/CommcoachDossierView.tsx | 22 +++-- 4 files changed, 127 insertions(+), 32 deletions(-) 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;