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:
parent
26044ff53b
commit
36b8558dd0
4 changed files with 127 additions and 32 deletions
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 wä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>
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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 wä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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue