/** * CommCoach Dossier View (Main View) * * Unified view per context: Coaching session, Tasks, Sessions history, Scores. * Voice first, always with text fallback. * Files & Sources are provided via the shared UnifiedDataBar sidebar. */ 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, useMandateId } from '../../../hooks/useCurrentInstance'; import { getDossierExportUrl, getSessionExportUrl, getScoreHistoryApi, getPersonasApi, type CoachingPersona, type SendMessageOptions, } from '../../../api/commcoachApi'; import api from '../../../api'; import AutoScroll from '../../../components/UiComponents/AutoScroll/AutoScroll'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { UnifiedDataBar } from '../../../components/UnifiedDataBar'; import type { UdbContext, UdbTab } from '../../../components/UnifiedDataBar'; import { ProviderMultiSelect, _defaultProviderSelection } from '../../../components/ProviderSelector'; import type { ProviderSelection } from '../../../components/ProviderSelector'; import { getPageIcon } from '../../../config/pageRegistry'; import styles from './CommcoachDossierView.module.css'; import { useVoiceController } from './useVoiceController'; import { useLanguage } from '../../../providers/language/LanguageContext'; interface WorkspaceFileInfo { id: string; fileName: string; mimeType: string; fileSize: number; } interface DataSourceInfo { id: string; connectionId: string; sourceType: string; path: string; label: string; } interface FeatureDataSourceInfo { id: string; featureInstanceId: string; featureCode: string; tableName: string; label: string; } type TabKey = 'coaching' | 'tasks' | 'sessions' | 'scores'; interface CommcoachDossierViewProps { persistentInstanceId?: string; persistentMandateId?: string; } export const CommcoachDossierView: React.FC = ({ persistentInstanceId, persistentMandateId, }) => { const { t } = useLanguage(); const routeInstanceId = useInstanceId(); const routeMandateId = useMandateId(); const instanceId = persistentInstanceId || routeInstanceId; const mandateId = persistentMandateId || routeMandateId; const coach = useCommcoach(instanceId); const { request } = useApiRequest(); const [activeTab, setActiveTab] = useState('coaching'); const [showNewContext, setShowNewContext] = useState(false); const [newTitle, setNewTitle] = useState(''); const [newDescription, setNewDescription] = useState(''); const [newCategory, setNewCategory] = useState('custom'); const [udbCollapsed, setUdbCollapsed] = useState(false); const [udbTab, setUdbTab] = useState('files'); const [newTaskTitle, setNewTaskTitle] = useState(''); const [scoreHistory, setScoreHistory] = useState>>({}); const [personas, setPersonas] = useState([]); const [selectedPersonaId, setSelectedPersonaId] = useState(undefined); const [wsFiles, setWsFiles] = useState([]); const [wsDataSources, setWsDataSources] = useState([]); const [wsFeatureDataSources, setWsFeatureDataSources] = useState([]); const [attachedFileIds, setAttachedFileIds] = useState([]); const [attachedDsIds, setAttachedDsIds] = useState([]); const [attachedFdsIds, setAttachedFdsIds] = useState([]); const [providerSelection, setProviderSelection] = useState(_defaultProviderSelection); const [showSourcePicker, setShowSourcePicker] = useState(false); const [showFilePicker, setShowFilePicker] = useState(false); const [showAgentActivity, setShowAgentActivity] = useState(true); const _udbContext: UdbContext | null = instanceId ? { instanceId, mandateId: mandateId || undefined, featureInstanceId: instanceId } : null; const inputRef = useRef(null); const sendMessageRef = useRef(coach.sendMessage); sendMessageRef.current = coach.sendMessage; const attachedFileIdsRef = useRef(attachedFileIds); attachedFileIdsRef.current = attachedFileIds; const attachedDsIdsRef = useRef(attachedDsIds); attachedDsIdsRef.current = attachedDsIds; const attachedFdsIdsRef = useRef(attachedFdsIds); attachedFdsIdsRef.current = attachedFdsIds; const providerSelRef = useRef(providerSelection); providerSelRef.current = providerSelection; const voice = useVoiceController({ onFinalText: (text) => { const opts: SendMessageOptions = {}; if (attachedFileIdsRef.current.length) opts.fileIds = attachedFileIdsRef.current; if (attachedDsIdsRef.current.length) opts.dataSourceIds = attachedDsIdsRef.current; if (attachedFdsIdsRef.current.length) opts.featureDataSourceIds = attachedFdsIdsRef.current; const allowed = providerSelRef.current.include.length > 0 ? providerSelRef.current.include : undefined; if (allowed) opts.allowedProviders = allowed; sendMessageRef.current(text, Object.keys(opts).length ? opts : undefined); }, }); 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 scores, personas when context changes useEffect(() => { if (!instanceId || !coach.selectedContextId) return; getScoreHistoryApi(request, instanceId, coach.selectedContextId) .then(h => setScoreHistory(h)) .catch(() => {}); }, [instanceId, request, coach.selectedContextId]); useEffect(() => { if (!instanceId) return; getPersonasApi(request, instanceId) .then(p => setPersonas(p)) .catch(() => {}); }, [instanceId, request]); const _refreshWorkspaceAssets = useCallback(() => { if (!instanceId) return; api.get(`/api/workspace/${instanceId}/files`).then(r => setWsFiles(r.data.files || [])).catch(() => {}); api.get(`/api/workspace/${instanceId}/datasources`).then(r => setWsDataSources(r.data.dataSources || [])).catch(() => {}); api.get(`/api/workspace/${instanceId}/feature-datasources`).then(r => setWsFeatureDataSources(r.data.featureDataSources || [])).catch(() => {}); }, [instanceId]); useEffect(() => { _refreshWorkspaceAssets(); }, [_refreshWorkspaceAssets]); useEffect(() => { const _handleFileUploaded = () => _refreshWorkspaceAssets(); window.addEventListener('fileUploaded', _handleFileUploaded); return () => window.removeEventListener('fileUploaded', _handleFileUploaded); }, [_refreshWorkspaceAssets]); useEffect(() => { if (activeTab !== 'coaching' || !coach.session) { voice.deactivate(); } else if (voice.state === 'idle') { voice.activate(); } }, [activeTab, coach.session?.id, voice]); useEffect(() => { coach.onDocumentCreatedRef.current = () => { window.dispatchEvent(new CustomEvent('fileUploaded', { detail: { source: 'commcoachDocument' } })); }; return () => { coach.onDocumentCreatedRef.current = null; }; }, [coach, _refreshWorkspaceAssets]); useEffect(() => { if (coach.agentToolCalls.length > 0) { setShowAgentActivity(true); } }, [coach.agentToolCalls.length]); const handleStopTts = useCallback(() => { coach.stopTts(); voice.ttsStopped(); }, [coach, voice]); const handlePauseTts = useCallback(() => coach.pauseTts(), [coach]); const handleResumeTts = useCallback(() => coach.resumeTts(), [coach]); const handleSend = useCallback(async () => { if (!coach.inputValue.trim() || coach.isStreaming) return; const opts: SendMessageOptions = {}; if (attachedFileIds.length) opts.fileIds = attachedFileIds; if (attachedDsIds.length) opts.dataSourceIds = attachedDsIds; if (attachedFdsIds.length) opts.featureDataSourceIds = attachedFdsIds; const allowed = providerSelection.include.length > 0 ? providerSelection.include : undefined; if (allowed) opts.allowedProviders = allowed; await coach.sendMessage(coach.inputValue, Object.keys(opts).length ? opts : undefined); setAttachedFileIds([]); setShowSourcePicker(false); setShowFilePicker(false); }, [coach, attachedFileIds, attachedDsIds, attachedFdsIds, providerSelection]); const _toggleFile = useCallback((fileId: string) => { setAttachedFileIds(prev => prev.includes(fileId) ? prev.filter(id => id !== fileId) : [...prev, fileId]); }, []); const _toggleDs = useCallback((dsId: string) => { setAttachedDsIds(prev => prev.includes(dsId) ? prev.filter(id => id !== dsId) : [...prev, dsId]); }, []); const _toggleFds = useCallback((fdsId: string) => { setAttachedFdsIds(prev => prev.includes(fdsId) ? prev.filter(id => id !== fdsId) : [...prev, fdsId]); }, []); 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 handleAddTask = useCallback(async () => { if (!newTaskTitle.trim()) return; await coach.addTask(newTaskTitle); setNewTaskTitle(''); }, [newTaskTitle, coach]); if (coach.loadingContexts) { return

{t('lade')}

; } return (
{/* UDB Sidebar */} {_udbContext && (
{!udbCollapsed && ( )}
)} {/* Main Content */}
{/* 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 && (

{t('Willkommen beim Kommunikationscoach')}

{t('Erstelle ein Thema, um zu')}

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

{coach.selectedContext?.title}

{coach.selectedContext?.description && (

{coach.selectedContext.description}

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

{t('Starte eine neue Coachingsession zu')}

{personas.length > 0 && (
{personas.map(p => ( ))}
)}
) : ( <> {/* Session Header */}
{t('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'}...
)}
)}
{(coach.isStreaming || coach.agentToolCalls.length > 0) && (
{showAgentActivity && (
{coach.agentToolCalls.length === 0 ? (
Noch keine Tool-Aufrufe in dieser Antwort.
) : ( coach.agentToolCalls.map((toolCall, idx) => (
{toolCall.toolName} {toolCall.success === true ? 'fertig' : toolCall.success === false ? 'fehler' : 'läuft'}
{toolCall.args && (
Args: {_formatToolPayload(toolCall.args)}
)} {toolCall.result && (
Result: {toolCall.result}
)}
)) )}
)}
)} {/* 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...'}
{/* Attachment Chips */} {(attachedFileIds.length > 0 || attachedDsIds.length > 0 || attachedFdsIds.length > 0) && (
{attachedFileIds.map(fId => { const file = wsFiles.find(f => f.id === fId); return ( {file?.fileName || fId} ); })} {attachedDsIds.map(dsId => { const ds = wsDataSources.find(d => d.id === dsId); return ( {ds?.label || ds?.path || dsId} ); })} {attachedFdsIds.map(fdsId => { const fds = wsFeatureDataSources.find(d => d.id === fdsId); return ( {fds ? getPageIcon(`feature.${fds.featureCode}`) || '\uD83D\uDDC3\uFE0F' : '\uD83D\uDDC3\uFE0F'} {fds?.label || fdsId} ); })}
)}