Alle 9 Fixes sind implementiert. Hier die Zusammenfassung:

Fix 1 -- Opening-Prompt: processSessionOpening in serviceCommcoach.py prüft jetzt ob es die erste Session ist (isFirstSession) und gibt der AI einen expliziten Prompt, der das Erfinden von Kontext verbietet.
Fix 2 -- Stabiler Transcript: onresult in CommcoachCoachingView.tsx nutzt jetzt processedResultIndexRef um nur neue Results zu verarbeiten. Finalisierte Teile werden stabil akkumuliert, kein Flackern mehr.
Fix 3 -- Hintergrundgeräusche-Timeout: Neuer silenceTimerRef mit 5s Timeout. Wenn nach onspeechstart kein Text kommt, wird isUserSpeaking automatisch zurückgesetzt.
Fix 4 -- Stop-Button: "Stop" Button erscheint im Session-Header wenn TTS läuft (via isTtsPlaying State, synchronisiert per 200ms Interval mit isTtsPlayingRef).
Fix 5 -- Weitersprechen-Button: lastTtsAudioRef speichert das zuletzt gespielte Audio. stopTts setzt wasInterrupted = true. "Weitersprechen" Button erscheint nach Unterbrechung und spielt das Audio erneut ab.
Fix 6 -- Paralleles TTS: Neue _generateAndEmitTts() Hilfsfunktion. In processMessage und processSessionOpening wird TTS als asyncio.create_task parallel zu _emitChunkedResponse gestartet.
Fix 7 -- JSON-Response: Die AI antwortet jetzt als JSON mit text, speech, documents. Neuer Prompt-Block wird in buildCoachingSystemPrompt angehängt. _parseAiJsonResponse() und _saveGeneratedDocument() im Backend. processMessage und processSessionOpening nutzen die neue Struktur.
Fix 8 -- Loading-States: Neuer actionLoading State in useCommcoach. Alle async Funktionen setzen setActionLoading('key') vor dem Await und null im finally. Buttons zeigen Loading-Text und werden disabled.
Fix 9 -- Umlaute: Alle deutschen Strings in allen CommCoach-Dateien (Backend + Frontend) korrigiert: ae->ä, oe->ö, ue->ü.
This commit is contained in:
ValueOn AG 2026-03-04 22:53:45 +01:00
parent 26044ff53b
commit 36b8558dd0
4 changed files with 127 additions and 32 deletions

View file

@ -49,8 +49,12 @@ export interface CommcoachHookReturn {
isMuted: boolean; isMuted: boolean;
setMuted: (muted: boolean) => void; setMuted: (muted: boolean) => void;
stopTts: () => void; stopTts: () => void;
resumeTts: () => void;
wasInterrupted: boolean;
isTtsPlayingRef: MutableRefObject<boolean>; isTtsPlayingRef: MutableRefObject<boolean>;
actionLoading: string | null;
toggleTaskStatus: (taskId: string, currentStatus: string) => Promise<void>; toggleTaskStatus: (taskId: string, currentStatus: string) => Promise<void>;
addTask: (title: string, description?: string) => Promise<void>; addTask: (title: string, description?: string) => Promise<void>;
removeTask: (taskId: string) => Promise<void>; removeTask: (taskId: string) => Promise<void>;
@ -81,10 +85,13 @@ export function useCommcoach(): CommcoachHookReturn {
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
const [isMuted, setIsMuted] = useState(false); const [isMuted, setIsMuted] = useState(false);
const [wasInterrupted, setWasInterrupted] = useState(false);
const [actionLoading, setActionLoading] = useState<string | null>(null);
const isMountedRef = useRef(true); const isMountedRef = useRef(true);
const currentAudioRef = useRef<HTMLAudioElement | null>(null); const currentAudioRef = useRef<HTMLAudioElement | null>(null);
const isTtsPlayingRef = useRef(false); const isTtsPlayingRef = useRef(false);
const lastTtsAudioRef = useRef<string | null>(null);
useEffect(() => { isMountedRef.current = true; return () => { isMountedRef.current = false; }; }, []); useEffect(() => { isMountedRef.current = true; return () => { isMountedRef.current = false; }; }, []);
@ -108,6 +115,8 @@ export function useCommcoach(): CommcoachHookReturn {
currentAudioRef.current.pause(); currentAudioRef.current.pause();
currentAudioRef.current = null; currentAudioRef.current = null;
} }
lastTtsAudioRef.current = audioB64;
setWasInterrupted(false);
isTtsPlayingRef.current = true; isTtsPlayingRef.current = true;
try { try {
const audio = new Audio(`data:audio/mp3;base64,${audioB64}`); const audio = new Audio(`data:audio/mp3;base64,${audioB64}`);
@ -127,9 +136,18 @@ export function useCommcoach(): CommcoachHookReturn {
currentAudioRef.current.pause(); currentAudioRef.current.pause();
currentAudioRef.current = null; currentAudioRef.current = null;
} }
if (isTtsPlayingRef.current) {
setWasInterrupted(true);
}
isTtsPlayingRef.current = false; isTtsPlayingRef.current = false;
}, []); }, []);
const resumeTts = useCallback(() => {
if (lastTtsAudioRef.current) {
_playTtsAudio(lastTtsAudioRef.current);
}
}, [_playTtsAudio]);
const selectContext = useCallback(async (contextId: string) => { const selectContext = useCallback(async (contextId: string) => {
if (!instanceId) return; if (!instanceId) return;
setSelectedContextId(contextId); setSelectedContextId(contextId);
@ -178,6 +196,7 @@ export function useCommcoach(): CommcoachHookReturn {
const createContext = useCallback(async (title: string, description?: string, category?: string, goals?: string[]) => { const createContext = useCallback(async (title: string, description?: string, category?: string, goals?: string[]) => {
if (!instanceId) return; if (!instanceId) return;
setActionLoading('creating');
try { try {
const created = await createContextApi(request, instanceId, { title, description, category, goals }); const created = await createContextApi(request, instanceId, { title, description, category, goals });
if (isMountedRef.current) { if (isMountedRef.current) {
@ -192,11 +211,14 @@ export function useCommcoach(): CommcoachHookReturn {
} }
} catch (err: any) { } catch (err: any) {
if (isMountedRef.current) setError(err.message || 'Fehler beim Erstellen des Kontexts'); if (isMountedRef.current) setError(err.message || 'Fehler beim Erstellen des Kontexts');
} finally {
if (isMountedRef.current) setActionLoading(null);
} }
}, [request, instanceId, refreshContexts]); }, [request, instanceId, refreshContexts]);
const archiveContext = useCallback(async (contextId: string) => { const archiveContext = useCallback(async (contextId: string) => {
if (!instanceId) return; if (!instanceId) return;
setActionLoading('archiving');
try { try {
const { archiveContextApi } = await import('../api/commcoachApi'); const { archiveContextApi } = await import('../api/commcoachApi');
await archiveContextApi(request, instanceId, contextId); await archiveContextApi(request, instanceId, contextId);
@ -209,11 +231,14 @@ export function useCommcoach(): CommcoachHookReturn {
} }
} catch (err: any) { } catch (err: any) {
if (isMountedRef.current) setError(err.message || 'Fehler beim Archivieren'); if (isMountedRef.current) setError(err.message || 'Fehler beim Archivieren');
} finally {
if (isMountedRef.current) setActionLoading(null);
} }
}, [request, instanceId, selectedContextId, refreshContexts]); }, [request, instanceId, selectedContextId, refreshContexts]);
const startSessionCb = useCallback(async (personaId?: string) => { const startSessionCb = useCallback(async (personaId?: string) => {
if (!instanceId || !selectedContextId) return; if (!instanceId || !selectedContextId) return;
setActionLoading('starting');
await _unlockAudioForTts(); await _unlockAudioForTts();
setError(null); setError(null);
setIsMuted(false); setIsMuted(false);
@ -285,6 +310,8 @@ export function useCommcoach(): CommcoachHookReturn {
setError(err.message || 'Fehler beim Starten der Session'); setError(err.message || 'Fehler beim Starten der Session');
setIsStreaming(false); setIsStreaming(false);
} }
} finally {
if (isMountedRef.current) setActionLoading(null);
} }
}, [instanceId, selectedContextId, _playTtsAudio]); }, [instanceId, selectedContextId, _playTtsAudio]);
@ -437,6 +464,7 @@ export function useCommcoach(): CommcoachHookReturn {
const completeSessionCb = useCallback(async () => { const completeSessionCb = useCallback(async () => {
if (!instanceId || !session) return; if (!instanceId || !session) return;
setActionLoading('completing');
try { try {
const completed = await completeSessionApi(request, instanceId, session.id); const completed = await completeSessionApi(request, instanceId, session.id);
if (isMountedRef.current) { if (isMountedRef.current) {
@ -445,11 +473,14 @@ export function useCommcoach(): CommcoachHookReturn {
} }
} catch (err: any) { } catch (err: any) {
if (isMountedRef.current) setError(err.message || 'Fehler beim Abschliessen'); if (isMountedRef.current) setError(err.message || 'Fehler beim Abschliessen');
} finally {
if (isMountedRef.current) setActionLoading(null);
} }
}, [request, instanceId, session, selectedContextId, selectContext]); }, [request, instanceId, session, selectedContextId, selectContext]);
const cancelSessionCb = useCallback(async () => { const cancelSessionCb = useCallback(async () => {
if (!instanceId || !session) return; if (!instanceId || !session) return;
setActionLoading('cancelling');
try { try {
await cancelSessionApi(request, instanceId, session.id); await cancelSessionApi(request, instanceId, session.id);
if (isMountedRef.current) { if (isMountedRef.current) {
@ -458,11 +489,14 @@ export function useCommcoach(): CommcoachHookReturn {
} }
} catch (err: any) { } catch (err: any) {
if (isMountedRef.current) setError(err.message || 'Fehler beim Abbrechen'); if (isMountedRef.current) setError(err.message || 'Fehler beim Abbrechen');
} finally {
if (isMountedRef.current) setActionLoading(null);
} }
}, [request, instanceId, session]); }, [request, instanceId, session]);
const toggleTaskStatus = useCallback(async (taskId: string, currentStatus: string) => { const toggleTaskStatus = useCallback(async (taskId: string, currentStatus: string) => {
if (!instanceId) return; if (!instanceId) return;
setActionLoading('togglingTask');
const newStatus = currentStatus === 'done' ? 'open' : 'done'; const newStatus = currentStatus === 'done' ? 'open' : 'done';
try { try {
const updated = await updateTaskStatusApi(request, instanceId, taskId, newStatus); const updated = await updateTaskStatusApi(request, instanceId, taskId, newStatus);
@ -471,16 +505,21 @@ export function useCommcoach(): CommcoachHookReturn {
} }
} catch (err: any) { } catch (err: any) {
if (isMountedRef.current) setError(err.message); if (isMountedRef.current) setError(err.message);
} finally {
if (isMountedRef.current) setActionLoading(null);
} }
}, [request, instanceId]); }, [request, instanceId]);
const addTask = useCallback(async (title: string, description?: string) => { const addTask = useCallback(async (title: string, description?: string) => {
if (!instanceId || !selectedContextId) return; if (!instanceId || !selectedContextId) return;
setActionLoading('addingTask');
try { try {
const created = await createTaskApi(request, instanceId, selectedContextId, { title, description }); const created = await createTaskApi(request, instanceId, selectedContextId, { title, description });
if (isMountedRef.current) setTasks(prev => [created, ...prev]); if (isMountedRef.current) setTasks(prev => [created, ...prev]);
} catch (err: any) { } catch (err: any) {
if (isMountedRef.current) setError(err.message); if (isMountedRef.current) setError(err.message);
} finally {
if (isMountedRef.current) setActionLoading(null);
} }
}, [request, instanceId, selectedContextId]); }, [request, instanceId, selectedContextId]);
@ -504,7 +543,8 @@ export function useCommcoach(): CommcoachHookReturn {
selectContext, createContext, archiveContext, selectContext, createContext, archiveContext,
startSession: startSessionCb, startSession: startSessionCb,
sendMessage, sendAudio, completeSession: completeSessionCb, cancelSession: cancelSessionCb, sendMessage, sendAudio, completeSession: completeSessionCb, cancelSession: cancelSessionCb,
isMuted, setMuted: setIsMuted, stopTts, isTtsPlayingRef, isMuted, setMuted: setIsMuted, stopTts, resumeTts, wasInterrupted, isTtsPlayingRef,
actionLoading,
toggleTaskStatus, addTask, removeTask, toggleTaskStatus, addTask, removeTask,
refreshContexts, refreshContexts,
}; };

View file

@ -33,9 +33,12 @@ export const CommcoachCoachingView: React.FC = () => {
const streamRef = useRef<MediaStream | null>(null); const streamRef = useRef<MediaStream | null>(null);
const speechRecognitionRef = useRef<SpeechRecognition | null>(null); const speechRecognitionRef = useRef<SpeechRecognition | null>(null);
const transcriptPartsRef = useRef<string[]>([]); const transcriptPartsRef = useRef<string[]>([]);
const processedResultIndexRef = useRef(0);
const silenceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [isListening, setIsListening] = useState(false); const [isListening, setIsListening] = useState(false);
const [isUserSpeaking, setIsUserSpeaking] = useState(false); const [isUserSpeaking, setIsUserSpeaking] = useState(false);
const [liveTranscript, setLiveTranscript] = useState(''); const [liveTranscript, setLiveTranscript] = useState('');
const [isTtsPlaying, setIsTtsPlaying] = useState(false);
const handleSend = useCallback(async () => { const handleSend = useCallback(async () => {
if (!coach.inputValue.trim() || coach.isStreaming) return; if (!coach.inputValue.trim() || coach.isStreaming) return;
@ -85,6 +88,14 @@ export const CommcoachCoachingView: React.FC = () => {
.catch(() => {}); .catch(() => {});
}, [instanceId, request]); }, [instanceId, request]);
useEffect(() => {
if (!coach.session) return;
const interval = setInterval(() => {
setIsTtsPlaying(coach.isTtsPlayingRef.current);
}, 200);
return () => clearInterval(interval);
}, [coach.session, coach.isTtsPlayingRef]);
useEffect(() => { useEffect(() => {
if (!coach.session || coach.isMuted) { if (!coach.session || coach.isMuted) {
if (speechRecognitionRef.current) { if (speechRecognitionRef.current) {
@ -135,36 +146,54 @@ export const CommcoachCoachingView: React.FC = () => {
if (cancelled) return; 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 = () => { recognition.onspeechstart = () => {
if (cancelled || coach.isTtsPlayingRef.current) return; if (cancelled || coach.isTtsPlayingRef.current) return;
setIsUserSpeaking(true); setIsUserSpeaking(true);
transcriptPartsRef.current = []; transcriptPartsRef.current = [];
processedResultIndexRef.current = 0;
setLiveTranscript(''); setLiveTranscript('');
_resetSilenceTimer();
}; };
recognition.onresult = (event: SpeechRecognitionEvent) => { recognition.onresult = (event: SpeechRecognitionEvent) => {
if (cancelled || coach.isTtsPlayingRef.current) return; if (cancelled || coach.isTtsPlayingRef.current) return;
const finalized: string[] = [];
let currentInterim = ''; 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]; const r = event.results[i];
if (r.isFinal) { 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 { } else {
currentInterim = r[0].transcript.trim(); currentInterim = r[0].transcript.trim();
} }
} }
transcriptPartsRef.current = finalized.filter(Boolean);
const preview = [...transcriptPartsRef.current, currentInterim].join(' ').trim(); const preview = [...transcriptPartsRef.current, currentInterim].join(' ').trim();
setLiveTranscript(preview); setLiveTranscript(preview);
if (preview) _resetSilenceTimer();
const totalWords = preview.split(/\s+/).filter(Boolean).length; const totalWords = preview.split(/\s+/).filter(Boolean).length;
if (totalWords >= MIN_WORDS_TO_INTERRUPT) coach.stopTts(); if (totalWords >= MIN_WORDS_TO_INTERRUPT) coach.stopTts();
}; };
recognition.onspeechend = () => { recognition.onspeechend = () => {
if (cancelled) return; if (cancelled) return;
if (silenceTimerRef.current) clearTimeout(silenceTimerRef.current);
if (coach.isTtsPlayingRef.current) { if (coach.isTtsPlayingRef.current) {
transcriptPartsRef.current = []; transcriptPartsRef.current = [];
processedResultIndexRef.current = 0;
setLiveTranscript(''); setLiveTranscript('');
setIsUserSpeaking(false); setIsUserSpeaking(false);
return; return;
@ -175,6 +204,7 @@ export const CommcoachCoachingView: React.FC = () => {
if (wordCount >= MIN_WORDS_TO_INTERRUPT) coach.sendMessage(fullTranscript); if (wordCount >= MIN_WORDS_TO_INTERRUPT) coach.sendMessage(fullTranscript);
} }
transcriptPartsRef.current = []; transcriptPartsRef.current = [];
processedResultIndexRef.current = 0;
setLiveTranscript(''); setLiveTranscript('');
setIsUserSpeaking(false); setIsUserSpeaking(false);
}; };
@ -208,6 +238,7 @@ export const CommcoachCoachingView: React.FC = () => {
init(); init();
return () => { return () => {
cancelled = true; cancelled = true;
if (silenceTimerRef.current) clearTimeout(silenceTimerRef.current);
if (speechRecognitionRef.current) { if (speechRecognitionRef.current) {
try { try {
speechRecognitionRef.current.stop(); speechRecognitionRef.current.stop();
@ -272,17 +303,17 @@ export const CommcoachCoachingView: React.FC = () => {
onChange={e => setNewCategory(e.target.value)} onChange={e => setNewCategory(e.target.value)}
> >
<option value="custom">Individuell</option> <option value="custom">Individuell</option>
<option value="leadership">Fuehrung</option> <option value="leadership">Führung</option>
<option value="conflict">Konflikt</option> <option value="conflict">Konflikt</option>
<option value="negotiation">Verhandlung</option> <option value="negotiation">Verhandlung</option>
<option value="presentation">Praesentation</option> <option value="presentation">Präsentation</option>
<option value="feedback">Feedback</option> <option value="feedback">Feedback</option>
<option value="delegation">Delegation</option> <option value="delegation">Delegation</option>
<option value="changeManagement">Change Management</option> <option value="changeManagement">Change Management</option>
</select> </select>
<div className={styles.newContextActions}> <div className={styles.newContextActions}>
<button className={styles.btnPrimary} onClick={handleCreateContext} disabled={!newTitle.trim()}> <button className={styles.btnPrimary} onClick={handleCreateContext} disabled={!newTitle.trim() || !!coach.actionLoading}>
Erstellen {coach.actionLoading === 'creating' ? 'Wird erstellt...' : 'Erstellen'}
</button> </button>
<button className={styles.btnSecondary} onClick={() => setShowNewContext(false)}> <button className={styles.btnSecondary} onClick={() => setShowNewContext(false)}>
Abbrechen Abbrechen
@ -295,7 +326,7 @@ export const CommcoachCoachingView: React.FC = () => {
{!coach.selectedContextId && !showNewContext && ( {!coach.selectedContextId && !showNewContext && (
<div className={styles.noContext}> <div className={styles.noContext}>
<h3>Willkommen beim Kommunikations-Coach</h3> <h3>Willkommen beim Kommunikations-Coach</h3>
<p>Waehle ein bestehendes Thema oder erstelle ein neues, um zu beginnen.</p> <p>Wähle ein bestehendes Thema oder erstelle ein neues, um zu beginnen.</p>
<button className={styles.btnPrimary} onClick={() => setShowNewContext(true)}> <button className={styles.btnPrimary} onClick={() => setShowNewContext(true)}>
Neues Thema erstellen Neues Thema erstellen
</button> </button>
@ -313,7 +344,7 @@ export const CommcoachCoachingView: React.FC = () => {
{personas.length > 0 && ( {personas.length > 0 && (
<div className={styles.personaSelector}> <div className={styles.personaSelector}>
<label className={styles.personaLabel}>Gespraechspartner waehlen:</label> <label className={styles.personaLabel}>Gesprächspartner hlen:</label>
<div className={styles.personaGrid}> <div className={styles.personaGrid}>
{personas.map(p => ( {personas.map(p => (
<button <button
@ -335,11 +366,13 @@ export const CommcoachCoachingView: React.FC = () => {
<button <button
className={styles.btnPrimary} className={styles.btnPrimary}
onClick={() => coach.startSession(selectedPersonaId)} onClick={() => coach.startSession(selectedPersonaId)}
disabled={!!coach.actionLoading}
> >
Session starten {coach.actionLoading === 'starting'
{selectedPersonaId && personas.find(p => p.id === selectedPersonaId) ? 'Wird gestartet...'
? ` mit ${personas.find(p => p.id === selectedPersonaId)!.label}` : selectedPersonaId && personas.find(p => p.id === selectedPersonaId)
: ''} ? `Session starten mit ${personas.find(p => p.id === selectedPersonaId)!.label}`
: 'Session starten'}
</button> </button>
</div> </div>
)} )}
@ -352,6 +385,16 @@ export const CommcoachCoachingView: React.FC = () => {
Session aktiv - {coach.selectedContext?.title} Session aktiv - {coach.selectedContext?.title}
</span> </span>
<div className={styles.sessionActions}> <div className={styles.sessionActions}>
{isTtsPlaying && (
<button className={styles.btnSmallDanger} onClick={coach.stopTts}>
Stop
</button>
)}
{coach.wasInterrupted && !isTtsPlaying && (
<button className={styles.btnSmall} onClick={coach.resumeTts}>
Weitersprechen
</button>
)}
<button <button
className={`${styles.btnSmall} ${coach.isMuted ? styles.mutedActive : ''}`} className={`${styles.btnSmall} ${coach.isMuted ? styles.mutedActive : ''}`}
onClick={() => coach.setMuted(!coach.isMuted)} onClick={() => coach.setMuted(!coach.isMuted)}
@ -359,11 +402,19 @@ export const CommcoachCoachingView: React.FC = () => {
> >
{coach.isMuted ? '\u{1F507}' : '\u{1F3A4}'} {coach.isMuted ? 'Stumm' : 'Ton an'} {coach.isMuted ? '\u{1F507}' : '\u{1F3A4}'} {coach.isMuted ? 'Stumm' : 'Ton an'}
</button> </button>
<button className={styles.btnSmall} onClick={coach.completeSession}> <button
Abschliessen className={styles.btnSmall}
onClick={coach.completeSession}
disabled={!!coach.actionLoading}
>
{coach.actionLoading === 'completing' ? 'Wird abgeschlossen...' : 'Abschliessen'}
</button> </button>
<button className={styles.btnSmallDanger} onClick={coach.cancelSession}> <button
Abbrechen className={styles.btnSmallDanger}
onClick={coach.cancelSession}
disabled={!!coach.actionLoading}
>
{coach.actionLoading === 'cancelling' ? 'Wird abgebrochen...' : 'Abbrechen'}
</button> </button>
</div> </div>
</div> </div>

View file

@ -30,7 +30,7 @@ export const CommcoachDashboardView: React.FC = () => {
} }
if (!dashboard) { if (!dashboard) {
return <div className={styles.empty}>Keine Daten verfuegbar.</div>; return <div className={styles.empty}>Keine Daten verfügbar.</div>;
} }
return ( return (
@ -124,7 +124,7 @@ export const CommcoachDashboardView: React.FC = () => {
<div className={styles.section}> <div className={styles.section}>
<h3 className={styles.sectionTitle}>Tipp des Tages</h3> <h3 className={styles.sectionTitle}>Tipp des Tages</h3>
<div className={styles.tipCard}> <div className={styles.tipCard}>
<p>Konsistenz schlaegt Intensitaet. Auch 10 Minuten taegliches Coaching-Gespraech <p>Konsistenz schlägt Intensität. Auch 10 Minuten tägliches Coaching-Gespräch
bringt messbare Fortschritte in deiner Kommunikationskompetenz.</p> bringt messbare Fortschritte in deiner Kommunikationskompetenz.</p>
</div> </div>
</div> </div>
@ -134,10 +134,10 @@ export const CommcoachDashboardView: React.FC = () => {
function _categoryLabel(category: string): string { function _categoryLabel(category: string): string {
const labels: Record<string, string> = { const labels: Record<string, string> = {
leadership: 'Fuehrung', leadership: 'Führung',
conflict: 'Konflikt', conflict: 'Konflikt',
negotiation: 'Verhandlung', negotiation: 'Verhandlung',
presentation: 'Praesentation', presentation: 'Präsentation',
feedback: 'Feedback', feedback: 'Feedback',
delegation: 'Delegation', delegation: 'Delegation',
changeManagement: 'Change Mgmt', changeManagement: 'Change Mgmt',

View file

@ -102,7 +102,7 @@ export const CommcoachDossierView: React.FC = () => {
</div> </div>
{!coach.selectedContextId ? ( {!coach.selectedContextId ? (
<div className={styles.empty}><p>Waehle ein Coaching-Thema.</p></div> <div className={styles.empty}><p>Wähle ein Coaching-Thema.</p></div>
) : (<> ) : (<>
{/* Context Header */} {/* Context Header */}
<div className={styles.header}> <div className={styles.header}>
@ -133,8 +133,12 @@ export const CommcoachDossierView: React.FC = () => {
</a> </a>
</> </>
)} )}
<button className={styles.btnArchive} onClick={() => coach.archiveContext(coach.selectedContextId!)}> <button
Archivieren className={styles.btnArchive}
onClick={() => coach.archiveContext(coach.selectedContextId!)}
disabled={!!coach.actionLoading}
>
{coach.actionLoading === 'archiving' ? 'Wird archiviert...' : 'Archivieren'}
</button> </button>
</div> </div>
</div> </div>
@ -178,12 +182,12 @@ export const CommcoachDossierView: React.FC = () => {
onChange={e => setNewTaskTitle(e.target.value)} onChange={e => setNewTaskTitle(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleAddTask()} onKeyDown={e => e.key === 'Enter' && handleAddTask()}
/> />
<button className={styles.addTaskBtn} onClick={handleAddTask} disabled={!newTaskTitle.trim()}> <button className={styles.addTaskBtn} onClick={handleAddTask} disabled={!newTaskTitle.trim() || !!coach.actionLoading}>
Hinzufuegen {coach.actionLoading === 'addingTask' ? 'Wird hinzugefügt...' : 'Hinzufügen'}
</button> </button>
</div> </div>
{coach.tasks.length === 0 ? ( {coach.tasks.length === 0 ? (
<div className={styles.emptyTab}>Noch keine Aufgaben. Der Coach schlaegt waehrend Sessions Aufgaben vor.</div> <div className={styles.emptyTab}>Noch keine Aufgaben. Der Coach schlägt hrend Sessions Aufgaben vor.</div>
) : ( ) : (
<div className={styles.taskList}> <div className={styles.taskList}>
{coach.tasks.map(task => ( {coach.tasks.map(task => (
@ -317,7 +321,7 @@ export const CommcoachDossierView: React.FC = () => {
</label> </label>
</div> </div>
{documents.length === 0 ? ( {documents.length === 0 ? (
<div className={styles.emptyTab}>Keine Dokumente. Lade Dateien hoch, um sie mit diesem Kontext zu verknuepfen.</div> <div className={styles.emptyTab}>Keine Dokumente. Lade Dateien hoch, um sie mit diesem Kontext zu verknüpfen.</div>
) : ( ) : (
<div className={styles.documentList}> <div className={styles.documentList}>
{documents.map(doc => ( {documents.map(doc => (
@ -374,10 +378,10 @@ function _formatFileSize(bytes: number): string {
function _dimensionLabel(dim: string): string { function _dimensionLabel(dim: string): string {
const labels: Record<string, string> = { const labels: Record<string, string> = {
empathy: 'Einfuehlungsvermoegen', empathy: 'Einfühlungsvermögen',
clarity: 'Klarheit', clarity: 'Klarheit',
assertiveness: 'Durchsetzung', assertiveness: 'Durchsetzung',
listening: 'Zuhoeren', listening: 'Zuhören',
selfReflection: 'Selbstreflexion', selfReflection: 'Selbstreflexion',
}; };
return labels[dim] || dim; return labels[dim] || dim;