/** * CommCoach Dossier View (Main View) * * Unified view per context: Coaching session, Tasks, Sessions history, Scores, Documents. * Voice first, always with text fallback. */ import React, { useState, useRef, useCallback, useEffect } from 'react'; import { useCommcoach } from '../../../hooks/useCommcoach'; import { type TtsEvent } from '../../../hooks/useTtsPlayback'; import { useApiRequest } from '../../../hooks/useApi'; import { useInstanceId } from '../../../hooks/useCurrentInstance'; import api from '../../../api'; import { getDossierExportUrl, getSessionExportUrl, getDocumentsApi, uploadDocumentApi, deleteDocumentApi, getScoreHistoryApi, getPersonasApi, type CoachingDocument, type CoachingPersona, } from '../../../api/commcoachApi'; import AutoScroll from '../../../components/UiComponents/AutoScroll/AutoScroll'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import styles from './CommcoachDossierView.module.css'; import { useVoiceController } from './useVoiceController'; type TabKey = 'coaching' | 'tasks' | 'sessions' | 'scores' | 'documents'; export const CommcoachDossierView: React.FC = () => { const coach = useCommcoach(); const { request } = useApiRequest(); const instanceId = useInstanceId(); const [activeTab, setActiveTab] = useState('coaching'); const [showNewContext, setShowNewContext] = useState(false); const [newTitle, setNewTitle] = useState(''); const [newDescription, setNewDescription] = useState(''); const [newCategory, setNewCategory] = useState('custom'); const [newTaskTitle, setNewTaskTitle] = useState(''); const [documents, setDocuments] = useState([]); const [uploading, setUploading] = useState(false); const [scoreHistory, setScoreHistory] = useState>>({}); const [personas, setPersonas] = useState([]); const [selectedPersonaId, setSelectedPersonaId] = useState(undefined); const inputRef = useRef(null); const sendMessageRef = useRef(coach.sendMessage); sendMessageRef.current = coach.sendMessage; const voice = useVoiceController({ onFinalText: (text) => sendMessageRef.current(text), }); // #region agent log const debugLogsRef = useRef([]); const [debugVisible, setDebugVisible] = useState(false); const [debugSnapshot, setDebugSnapshot] = useState([]); const _dlog = useCallback((tag: string, info?: string) => { const t = new Date(); const ts = `${t.getMinutes()}:${String(t.getSeconds()).padStart(2,'0')}.${String(t.getMilliseconds()).padStart(3,'0')}`; const entry = `[${ts}] ${tag}${info ? ' ' + info : ''}`; debugLogsRef.current.push(entry); if (debugLogsRef.current.length > 80) debugLogsRef.current.shift(); }, []); useEffect(() => { (window as any).__dlog = _dlog; return () => { delete (window as any).__dlog; }; }, [_dlog]); // #endregion useEffect(() => { coach.onTtsEventRef.current = (event: TtsEvent) => { if (event === 'playing') voice.ttsPlaying(); else if (event === 'ended') voice.ttsEnded(); else if (event === 'paused') voice.ttsPaused(); else if (event === 'error') voice.ttsEnded(); }; return () => { coach.onTtsEventRef.current = null; }; }, [coach.onTtsEventRef, voice.ttsPlaying, voice.ttsEnded, voice.ttsPaused]); // Auto-select first context useEffect(() => { if (!coach.selectedContextId && coach.contexts.length > 0) { coach.selectContext(coach.contexts[0].id, { skipSessionResume: true }); } }, [coach.contexts, coach.selectedContextId, coach.selectContext]); // Load documents, scores, personas when context changes useEffect(() => { if (!instanceId || !coach.selectedContextId) return; getDocumentsApi(request, instanceId, coach.selectedContextId) .then(d => setDocuments(d)) .catch(() => {}); getScoreHistoryApi(request, instanceId, coach.selectedContextId) .then(h => setScoreHistory(h)) .catch(() => {}); }, [instanceId, request, coach.selectedContextId]); useEffect(() => { coach.onDocumentCreatedRef.current = (doc) => { setDocuments(prev => { if (prev.some(d => d.id === doc.id)) return prev; return [doc, ...prev]; }); }; return () => { coach.onDocumentCreatedRef.current = null; }; }, [coach.onDocumentCreatedRef]); useEffect(() => { if (!instanceId) return; getPersonasApi(request, instanceId) .then(p => setPersonas(p)) .catch(() => {}); }, [instanceId, request]); useEffect(() => { if (activeTab !== 'coaching' || !coach.session) { voice.deactivate(); } else if (voice.state === 'idle') { voice.activate(); } }, [activeTab, coach.session?.id, voice]); const handleStopTts = useCallback(() => coach.stopTts(), [coach]); const handlePauseTts = useCallback(() => coach.pauseTts(), [coach]); const handleResumeTts = useCallback(() => coach.resumeTts(), [coach]); const handleSend = useCallback(async () => { if (!coach.inputValue.trim() || coach.isStreaming) return; await coach.sendMessage(coach.inputValue); }, [coach]); const handleKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }, [handleSend]); const handleCreateContext = useCallback(async () => { if (!newTitle.trim()) return; await coach.createContext(newTitle, newDescription || undefined, newCategory); setNewTitle(''); setNewDescription(''); setNewCategory('custom'); setShowNewContext(false); }, [newTitle, newDescription, newCategory, coach]); const handleSelectContext = useCallback((contextId: string) => { coach.selectContext(contextId, { skipSessionResume: true }); }, [coach]); const handleUpload = useCallback(async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file || !instanceId || !coach.selectedContextId) return; setUploading(true); try { const doc = await uploadDocumentApi(instanceId, coach.selectedContextId, file); setDocuments(prev => [doc, ...prev]); } catch { /* upload failed */ } finally { setUploading(false); e.target.value = ''; } }, [instanceId, coach.selectedContextId]); const handleDeleteDocument = useCallback(async (docId: string) => { if (!instanceId) return; try { await deleteDocumentApi(request, instanceId, docId); setDocuments(prev => prev.filter(d => d.id !== docId)); } catch { /* delete failed */ } }, [instanceId, request]); const handleDownloadDocument = useCallback(async (doc: CoachingDocument) => { if (!doc.fileRef) return; try { const response = await api.get(`/api/files/${doc.fileRef}/download`, { responseType: 'blob', }); const url = window.URL.createObjectURL(response.data); const a = document.createElement('a'); a.href = url; a.download = doc.fileName; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); } catch (err) { console.error('Download failed:', err); } }, []); const handleAddTask = useCallback(async () => { if (!newTaskTitle.trim()) return; await coach.addTask(newTaskTitle); setNewTaskTitle(''); }, [newTaskTitle, coach]); if (coach.loadingContexts) { return

Lade...

; } return (
{/* Context Selector */}
{coach.contexts.map(ctx => ( ))}
{/* New Context Form */} {showNewContext && (
setNewTitle(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleCreateContext()} autoFocus /> setNewDescription(e.target.value)} />
)} {/* No context selected */} {!coach.selectedContextId && !showNewContext && coach.contexts.length === 0 && (

Willkommen beim Kommunikations-Coach

Erstelle ein Thema, um zu beginnen.

)} {coach.selectedContextId && (<> {/* Context Header */}

{coach.selectedContext?.title}

{coach.selectedContext?.description && (

{coach.selectedContext.description}

)}
{instanceId && ( <> Export MD Export PDF )}
{/* Tab Navigation */}
{(['coaching', 'tasks', 'sessions', 'scores', 'documents'] as TabKey[]).map(tab => ( ))}
{/* ============================================================ */} {/* COACHING TAB */} {/* ============================================================ */} {activeTab === 'coaching' && (
{!coach.session ? (

Starte eine neue Coaching-Session zu diesem Thema.

{personas.length > 0 && (
{personas.map(p => ( ))}
)}
) : ( <> {/* Session Header */}
Session aktiv
{voice.state === 'botSpeaking' && ( <> )} {voice.state === 'interrupted' && coach.hasAudioToResume() && ( )}
{/* Messages */}
{coach.messages.map(msg => (
{msg.content}
{msg.createdAt ? new Date(msg.createdAt).toLocaleTimeString('de-CH', { hour: '2-digit', minute: '2-digit' }) : ''}
))} {voice.liveTranscript && (
{voice.liveTranscript}
)} {coach.isStreaming && (
{coach.streamingMessage ? ( {coach.streamingMessage} ) : (
{coach.streamingStatus || 'Coach denkt nach'}...
)}
)}
{/* Input Area */}
{voice.muted ? 'Stumm – Mikrofon aus' : voice.state === 'botSpeaking' ? (coach.streamingStatus || 'Coach spricht...') : coach.isStreaming ? (coach.streamingStatus || 'Coach denkt nach...') : voice.state === 'interrupted' ? 'Unterbrochen – Mikrofon an' : voice.state === 'listening' ? (voice.liveTranscript ? 'Spricht...' : 'Mikrofon an – bitte sprechen') : 'Mikrofon wird gestartet...'}