diff --git a/src/pages/views/commcoach/CommcoachCoachingView.module.css b/src/pages/views/commcoach/CommcoachCoachingView.module.css
new file mode 100644
index 0000000..b898159
--- /dev/null
+++ b/src/pages/views/commcoach/CommcoachCoachingView.module.css
@@ -0,0 +1,402 @@
+.coaching {
+ display: flex;
+ flex-direction: column;
+ height: calc(100vh - 140px);
+ overflow: hidden;
+}
+
+/* Context Tabs */
+.contextBar {
+ border-bottom: 1px solid var(--border-color, #e0e0e0);
+ padding: 0.5rem 1rem;
+ flex-shrink: 0;
+}
+
+.contextTabs {
+ display: flex;
+ gap: 0.5rem;
+ overflow-x: auto;
+ align-items: center;
+}
+
+.contextTab {
+ display: flex;
+ align-items: center;
+ gap: 0.35rem;
+ padding: 0.4rem 0.75rem;
+ border: 1px solid var(--border-color, #e0e0e0);
+ border-radius: 20px;
+ background: var(--bg-card, #fff);
+ cursor: pointer;
+ font-size: 0.8rem;
+ white-space: nowrap;
+ transition: all 0.15s;
+ color: var(--text-primary, #333);
+}
+
+.contextTab:hover {
+ background: var(--bg-hover, #f5f5f5);
+}
+
+.contextTabActive {
+ background: var(--primary-color, #F25843);
+ color: #fff;
+ border-color: var(--primary-color, #F25843);
+}
+
+.contextTabIcon {
+ font-weight: 700;
+ font-size: 0.75rem;
+}
+
+.contextTabLabel {
+ max-width: 120px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.contextTabNew {
+ width: 32px;
+ height: 32px;
+ border: 1px dashed var(--border-color, #ccc);
+ border-radius: 50%;
+ background: transparent;
+ cursor: pointer;
+ font-size: 1.2rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--text-secondary, #888);
+ flex-shrink: 0;
+}
+
+.contextTabNew:hover {
+ background: var(--bg-hover, #f5f5f5);
+ color: var(--primary-color, #F25843);
+}
+
+/* New Context Form */
+.newContextForm {
+ padding: 1rem;
+ background: var(--bg-card, #fff);
+ border-bottom: 1px solid var(--border-color, #e0e0e0);
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.newContextInput,
+.newContextSelect {
+ padding: 0.5rem 0.75rem;
+ border: 1px solid var(--border-color, #ddd);
+ border-radius: 6px;
+ font-size: 0.9rem;
+ background: var(--bg-input, #fff);
+ color: var(--text-primary, #333);
+}
+
+.newContextActions {
+ display: flex;
+ gap: 0.5rem;
+}
+
+/* Buttons */
+.btnPrimary {
+ padding: 0.5rem 1.25rem;
+ background: var(--primary-color, #F25843);
+ color: #fff;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 0.85rem;
+ font-weight: 500;
+}
+
+.btnPrimary:hover:not(:disabled) { filter: brightness(1.08); }
+.btnPrimary:disabled {
+ background: var(--color-medium-gray, #ccc);
+ color: var(--text-secondary, #888);
+ cursor: not-allowed;
+ opacity: 0.8;
+}
+
+.btnSecondary {
+ padding: 0.5rem 1.25rem;
+ background: transparent;
+ color: var(--text-primary, #333);
+ border: 1px solid var(--border-color, #ddd);
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 0.85rem;
+}
+
+.btnSecondary:hover:not(:disabled) {
+ background: var(--hover-bg, #f5f5f5);
+ border-color: var(--primary-color, #F25843);
+ color: var(--primary-color, #F25843);
+}
+
+.btnSmall {
+ padding: 0.3rem 0.75rem;
+ background: var(--primary-color, #F25843);
+ color: #fff;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 0.8rem;
+}
+
+.btnSmall:hover:not(:disabled) { filter: brightness(1.08); }
+
+.btnSmallDanger {
+ padding: 0.3rem 0.75rem;
+ background: transparent;
+ color: var(--error-color, #dc2626);
+ border: 1px solid var(--error-color, #dc2626);
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 0.8rem;
+}
+
+.btnSmallDanger:hover:not(:disabled) {
+ background: var(--error-color, #dc2626);
+ color: #fff;
+}
+
+/* No context */
+.noContext {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ flex: 1;
+ text-align: center;
+ padding: 2rem;
+ color: var(--text-secondary, #666);
+}
+
+.noContext h3 {
+ color: var(--text-primary, #333);
+ margin-bottom: 0.5rem;
+}
+
+.noContext p {
+ margin-bottom: 1rem;
+}
+
+/* Chat Area */
+.chatArea {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.sessionStart {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ flex: 1;
+ text-align: center;
+ padding: 2rem;
+}
+
+.sessionStart h3 {
+ color: var(--text-primary, #333);
+ margin-bottom: 0.5rem;
+}
+
+.sessionStart p {
+ color: var(--text-secondary, #666);
+ margin-bottom: 1rem;
+}
+
+.sessionHeader {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.5rem 1rem;
+ background: var(--bg-card, #fff);
+ border-bottom: 1px solid var(--border-color, #e0e0e0);
+ flex-shrink: 0;
+}
+
+.sessionLabel {
+ font-size: 0.85rem;
+ font-weight: 500;
+ color: var(--text-primary, #333);
+}
+
+.sessionActions {
+ display: flex;
+ gap: 0.5rem;
+}
+
+/* Messages */
+.messages {
+ flex: 1;
+ padding: 1rem;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.message {
+ max-width: 80%;
+}
+
+.messageUser {
+ align-self: flex-end;
+}
+
+.messageAssistant {
+ align-self: flex-start;
+}
+
+.messageBubble {
+ padding: 0.75rem 1rem;
+ border-radius: 12px;
+ font-size: 0.9rem;
+ line-height: 1.5;
+}
+
+.messageUser .messageBubble {
+ background: var(--primary-color, #F25843);
+ color: #fff;
+ border-bottom-right-radius: 4px;
+}
+
+.messageLive {
+ opacity: 0.7;
+ font-style: italic;
+ border: 1px dashed rgba(255, 255, 255, 0.4);
+}
+
+.messageAssistant .messageBubble {
+ background: var(--bg-card, #f5f5f5);
+ color: var(--text-primary, #333);
+ border: 1px solid var(--border-color, #e0e0e0);
+ border-bottom-left-radius: 4px;
+}
+
+.messageBubble p {
+ margin: 0 0 0.5rem;
+}
+
+.messageBubble p:last-child {
+ margin-bottom: 0;
+}
+
+.messageTime {
+ font-size: 0.7rem;
+ color: var(--text-secondary, #999);
+ margin-top: 0.2rem;
+ padding: 0 0.25rem;
+}
+
+.messageUser .messageTime {
+ text-align: right;
+}
+
+.typing {
+ color: var(--text-secondary, #888);
+ font-style: italic;
+}
+
+.typingDots {
+ animation: blink 1.4s infinite both;
+}
+
+@keyframes blink {
+ 0%, 80%, 100% { opacity: 0; }
+ 40% { opacity: 1; }
+}
+
+/* Input */
+.inputArea {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ padding: 0.75rem 1rem;
+ border-top: 1px solid var(--border-color, #e0e0e0);
+ background: var(--bg-card, #fff);
+ flex-shrink: 0;
+}
+
+.textInputRow {
+ display: flex;
+ gap: 0.5rem;
+ align-items: flex-end;
+}
+
+.textInput {
+ flex: 1;
+ min-width: 0;
+ padding: 0.6rem 0.75rem;
+ border: 1px solid var(--border-color, #ddd);
+ border-radius: 8px;
+ resize: none;
+ font-size: 0.9rem;
+ font-family: inherit;
+ min-height: 40px;
+ max-height: 120px;
+ background: var(--bg-input, #fff);
+ color: var(--text-primary, #333);
+}
+
+.sendBtn {
+ padding: 0.6rem 1.25rem;
+ background: var(--primary-color, #F25843);
+ color: #fff;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ font-size: 0.85rem;
+ font-weight: 500;
+ align-self: flex-end;
+}
+
+.sendBtn:hover:not(:disabled) { filter: brightness(1.08); }
+.sendBtn:disabled {
+ background: var(--color-medium-gray, #ccc);
+ color: var(--text-secondary, #888);
+ cursor: not-allowed;
+ opacity: 0.8;
+}
+
+.voiceStatus {
+ display: flex;
+ align-items: center;
+ padding: 0.25rem 0;
+ min-height: 1.5rem;
+}
+
+.voiceIndicator {
+ font-size: 0.9rem;
+ color: var(--text-secondary, #888);
+}
+
+.voiceIndicator.voiceActive {
+ color: var(--primary-color, #F25843);
+ font-weight: 500;
+}
+
+.voiceActive {
+ border: 2px solid #22c55e;
+}
+
+.mutedActive {
+ background: var(--color-medium-gray, #999);
+ color: #fff;
+ border-color: var(--color-medium-gray, #999);
+}
+
+.errorBanner {
+ padding: 0.5rem 1rem;
+ background: #fde8e8;
+ color: var(--color-error, #d32f2f);
+ font-size: 0.85rem;
+ text-align: center;
+}
diff --git a/src/pages/views/commcoach/CommcoachCoachingView.tsx b/src/pages/views/commcoach/CommcoachCoachingView.tsx
new file mode 100644
index 0000000..38bd1a3
--- /dev/null
+++ b/src/pages/views/commcoach/CommcoachCoachingView.tsx
@@ -0,0 +1,411 @@
+/**
+ * CommCoach Coaching View
+ *
+ * Voice first, always with text fallback (CONCEPT.md).
+ * Chat und Voice parallel: Mikrofon und Texteingabe gleichzeitig nutzbar.
+ * Mute: nur Mikrofon stummschalten, kein Moduswechsel.
+ */
+
+import React, { useState, useRef, useEffect, useCallback } from 'react';
+import { useSearchParams } from 'react-router-dom';
+import { useCommcoach } from '../../../hooks/useCommcoach';
+import AutoScroll from '../../../components/UiComponents/AutoScroll/AutoScroll';
+import ReactMarkdown from 'react-markdown';
+import remarkGfm from 'remark-gfm';
+import styles from './CommcoachCoachingView.module.css';
+
+export const CommcoachCoachingView: React.FC = () => {
+ const [searchParams, setSearchParams] = useSearchParams();
+ const coach = useCommcoach();
+ const [showNewContext, setShowNewContext] = useState(false);
+ const [newTitle, setNewTitle] = useState('');
+ const [newDescription, setNewDescription] = useState('');
+ const [newCategory, setNewCategory] = useState('custom');
+ const inputRef = useRef
(null);
+
+ const streamRef = useRef(null);
+ const speechRecognitionRef = useRef(null);
+ const transcriptPartsRef = useRef([]);
+ const [isListening, setIsListening] = useState(false);
+ const [isUserSpeaking, setIsUserSpeaking] = useState(false);
+ const [liveTranscript, setLiveTranscript] = useState('');
+
+ 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]);
+
+ useEffect(() => {
+ const contextId = searchParams.get('context');
+ if (contextId && coach.contexts.some(c => c.id === contextId)) {
+ coach.selectContext(contextId);
+ setSearchParams({}, { replace: true });
+ }
+ }, [searchParams, coach.contexts, coach.selectContext, setSearchParams]);
+
+ useEffect(() => {
+ if (coach.session && inputRef.current) {
+ inputRef.current.focus();
+ }
+ }, [coach.session]);
+
+ useEffect(() => {
+ if (!coach.session) {
+ coach.setMuted(false);
+ }
+ }, [coach.session]);
+
+ useEffect(() => {
+ if (!coach.session || coach.isMuted) {
+ if (speechRecognitionRef.current) {
+ try {
+ speechRecognitionRef.current.stop();
+ } catch {
+ // ignore
+ }
+ speechRecognitionRef.current = null;
+ }
+ if (streamRef.current) {
+ streamRef.current.getTracks().forEach((t) => t.stop());
+ streamRef.current = null;
+ }
+ setIsListening(false);
+ setIsUserSpeaking(false);
+ return;
+ }
+
+ const SpeechRecognitionApi = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
+ if (!SpeechRecognitionApi) {
+ console.warn('SpeechRecognition not supported');
+ return;
+ }
+
+ let cancelled = false;
+ const MIN_WORDS_TO_INTERRUPT = 2;
+ const lang = 'de-DE';
+
+ const init = async () => {
+ try {
+ const stream = await navigator.mediaDevices.getUserMedia({
+ audio: { echoCancellation: true, noiseSuppression: true },
+ });
+ if (cancelled) {
+ stream.getTracks().forEach((t) => t.stop());
+ return;
+ }
+ streamRef.current = stream;
+ setIsListening(true);
+
+ const recognition = new SpeechRecognitionApi();
+ recognition.continuous = true;
+ recognition.interimResults = true;
+ recognition.lang = lang;
+
+ recognition.onstart = () => {
+ if (cancelled) return;
+ };
+
+ recognition.onspeechstart = () => {
+ if (cancelled) return;
+ setIsUserSpeaking(true);
+ transcriptPartsRef.current = [];
+ setLiveTranscript('');
+ };
+
+ recognition.onresult = (event: SpeechRecognitionEvent) => {
+ if (cancelled) return;
+ const finalized: string[] = [];
+ let currentInterim = '';
+ for (let i = 0; i < event.results.length; i++) {
+ const r = event.results[i];
+ if (r.isFinal) {
+ finalized.push(r[0].transcript.trim());
+ } else {
+ currentInterim = r[0].transcript.trim();
+ }
+ }
+ transcriptPartsRef.current = finalized.filter(Boolean);
+ const preview = [...transcriptPartsRef.current, currentInterim].join(' ').trim();
+ setLiveTranscript(preview);
+ const totalWords = preview.split(/\s+/).filter(Boolean).length;
+ if (totalWords >= MIN_WORDS_TO_INTERRUPT) coach.stopTts();
+ };
+
+ recognition.onspeechend = () => {
+ if (cancelled) return;
+ const fullTranscript = transcriptPartsRef.current.join(' ').trim();
+ if (fullTranscript) {
+ const wordCount = fullTranscript.split(/\s+/).filter(Boolean).length;
+ if (wordCount >= MIN_WORDS_TO_INTERRUPT) coach.sendMessage(fullTranscript);
+ }
+ transcriptPartsRef.current = [];
+ setLiveTranscript('');
+ setIsUserSpeaking(false);
+ };
+
+ recognition.onend = () => {
+ if (cancelled) return;
+ if (speechRecognitionRef.current === recognition) {
+ speechRecognitionRef.current = null;
+ setIsUserSpeaking(false);
+ }
+ };
+
+ recognition.onerror = (event: any) => {
+ if (event.error === 'no-speech' || event.error === 'aborted') return;
+ console.warn('SpeechRecognition error:', event.error);
+ };
+
+ speechRecognitionRef.current = recognition;
+ recognition.start();
+ } catch (err) {
+ console.warn('Mic access failed:', err);
+ }
+ };
+
+ init();
+ return () => {
+ cancelled = true;
+ if (speechRecognitionRef.current) {
+ try {
+ speechRecognitionRef.current.stop();
+ } catch {
+ // ignore
+ }
+ speechRecognitionRef.current = null;
+ }
+ if (streamRef.current) {
+ streamRef.current.getTracks().forEach((t) => t.stop());
+ streamRef.current = null;
+ }
+ };
+ }, [coach.session, coach.isMuted, coach.stopTts, coach.sendMessage]);
+
+ return (
+
+ {/* Context Tabs */}
+
+
+ {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 && (
+
+
Willkommen beim Kommunikations-Coach
+
Waehle ein bestehendes Thema oder erstelle ein neues, um zu beginnen.
+
+
+ )}
+
+ {/* Chat Area */}
+ {coach.selectedContextId && (
+
+ {/* Session controls */}
+ {!coach.session && (
+
+
{coach.selectedContext?.title}
+
{coach.selectedContext?.description || 'Starte eine neue Coaching-Session zu diesem Thema.'}
+
+
+ )}
+
+ {/* Messages */}
+ {coach.session && (
+ <>
+
+
+ Session aktiv - {coach.selectedContext?.title}
+
+
+
+
+
+
+
+
+
+
+ {coach.messages.map(msg => (
+
+
+
+ {msg.content}
+
+
+
+ {msg.createdAt ? new Date(msg.createdAt).toLocaleTimeString('de-CH', { hour: '2-digit', minute: '2-digit' }) : ''}
+
+
+ ))}
+ {liveTranscript && (
+
+ )}
+ {coach.isStreaming && (
+
+
+
+ {coach.streamingStatus || 'Coach denkt nach'}
+ ...
+
+
+
+ )}
+
+
+
+ {/* Input: Chat und Voice parallel (CONCEPT: Voice first, always with text fallback) */}
+
+
+
+ {coach.isMuted
+ ? 'Stumm – Mikrofon aus'
+ : coach.isStreaming
+ ? (coach.streamingStatus || 'Coach antwortet...')
+ : isUserSpeaking
+ ? 'Spricht...'
+ : isListening
+ ? 'Mikrofon an – bitte sprechen'
+ : 'Mikrofon wird gestartet...'}
+
+
+
+
+
+ >
+ )}
+
+ {/* Error */}
+ {coach.error && (
+
{coach.error}
+ )}
+
+ )}
+
+ );
+};
+
+function _categoryIcon(category: string): string {
+ const icons: Record = {
+ leadership: 'L', conflict: 'K', negotiation: 'V',
+ presentation: 'P', feedback: 'F', delegation: 'D',
+ changeManagement: 'C', custom: '*',
+ };
+ return icons[category] || '*';
+}
+
+export default CommcoachCoachingView;
diff --git a/src/pages/views/commcoach/CommcoachDashboardView.module.css b/src/pages/views/commcoach/CommcoachDashboardView.module.css
new file mode 100644
index 0000000..bfea6ac
--- /dev/null
+++ b/src/pages/views/commcoach/CommcoachDashboardView.module.css
@@ -0,0 +1,125 @@
+.dashboard {
+ padding: 1rem;
+ max-width: 1200px;
+}
+
+.loading, .error, .empty {
+ padding: 2rem;
+ text-align: center;
+ color: var(--text-secondary, #666);
+}
+
+.error {
+ color: var(--error-color, #dc2626);
+}
+
+.kpiGrid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: 1rem;
+ margin-bottom: 2rem;
+}
+
+.kpiCard {
+ background: var(--bg-card, #fff);
+ border: 1px solid var(--border-color, #e0e0e0);
+ border-radius: 12px;
+ padding: 1.25rem;
+ text-align: center;
+}
+
+.kpiValue {
+ font-size: 2rem;
+ font-weight: 700;
+ color: var(--primary-color, #F25843);
+ line-height: 1.2;
+}
+
+.kpiLabel {
+ font-size: 0.85rem;
+ font-weight: 600;
+ color: var(--text-primary, #333);
+ margin-top: 0.25rem;
+}
+
+.kpiSub {
+ font-size: 0.75rem;
+ color: var(--text-secondary, #888);
+ margin-top: 0.25rem;
+}
+
+.section {
+ margin-bottom: 2rem;
+}
+
+.sectionTitle {
+ font-size: 1.1rem;
+ font-weight: 600;
+ margin-bottom: 1rem;
+ color: var(--text-primary, #333);
+}
+
+.contextGrid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
+ gap: 1rem;
+}
+
+.contextCard {
+ background: var(--bg-card, #fff);
+ border: 1px solid var(--border-color, #e0e0e0);
+ border-radius: 10px;
+ padding: 1rem;
+ cursor: pointer;
+ transition: box-shadow 0.15s;
+}
+
+.contextCard:hover {
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
+}
+
+.contextTitle {
+ font-weight: 600;
+ font-size: 0.95rem;
+ margin-bottom: 0.5rem;
+}
+
+.contextMeta {
+ display: flex;
+ gap: 0.75rem;
+ font-size: 0.8rem;
+ color: var(--text-secondary, #666);
+}
+
+.contextCategory {
+ background: var(--bg-tag, #e3f2fd);
+ color: var(--primary-color, #F25843);
+ padding: 0.1rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.75rem;
+}
+
+.contextLast {
+ font-size: 0.75rem;
+ color: var(--text-secondary, #888);
+ margin-top: 0.5rem;
+}
+
+.emptyState {
+ text-align: center;
+ padding: 2rem;
+ color: var(--text-secondary, #666);
+ background: var(--bg-card, #fff);
+ border: 1px dashed var(--border-color, #ccc);
+ border-radius: 10px;
+}
+
+.tipCard {
+ background: var(--bg-card, #fff);
+ border: 1px solid var(--border-color, #e0e0e0);
+ border-radius: 10px;
+ padding: 1.25rem;
+ color: var(--text-primary, #333);
+ font-size: 0.9rem;
+ line-height: 1.6;
+}
diff --git a/src/pages/views/commcoach/CommcoachDashboardView.tsx b/src/pages/views/commcoach/CommcoachDashboardView.tsx
new file mode 100644
index 0000000..05df578
--- /dev/null
+++ b/src/pages/views/commcoach/CommcoachDashboardView.tsx
@@ -0,0 +1,132 @@
+/**
+ * CommCoach Dashboard View
+ *
+ * Shows KPIs, streak, active contexts, and quick-start coaching entry.
+ */
+
+import React from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useCommcoachDashboard } from '../../../hooks/useCommcoachDashboard';
+import { useCurrentInstance } from '../../../hooks/useCurrentInstance';
+import styles from './CommcoachDashboardView.module.css';
+
+export const CommcoachDashboardView: React.FC = () => {
+ const navigate = useNavigate();
+ const { mandateId, instanceId } = useCurrentInstance();
+ const { dashboard, profile, loading, error, refresh } = useCommcoachDashboard();
+
+ const handleContextClick = (contextId: string) => {
+ if (mandateId && instanceId) {
+ navigate(`/mandates/${mandateId}/commcoach/${instanceId}/coaching?context=${contextId}`);
+ }
+ };
+
+ if (loading && !dashboard) {
+ return Dashboard wird geladen...
;
+ }
+
+ if (error) {
+ return {error}
;
+ }
+
+ if (!dashboard) {
+ return Keine Daten verfuegbar.
;
+ }
+
+ return (
+
+ {/* KPI Cards */}
+
+
+
{dashboard.streakDays}
+
Tage Streak
+
Rekord: {dashboard.longestStreak}
+
+
+
{dashboard.totalSessions}
+
Sessions
+
{dashboard.totalMinutes} Min. gesamt
+
+
+
+ {dashboard.averageScore != null ? Math.round(dashboard.averageScore) : '--'}
+
+
Kompetenz-Score
+
Durchschnitt
+
+
+
{dashboard.openTasks}
+
Offene Aufgaben
+
{dashboard.completedTasks} erledigt
+
+
+
+ {/* Active Contexts */}
+
+
Aktive Coaching-Themen
+ {dashboard.contexts.length === 0 ? (
+
+
Noch keine Coaching-Themen erstellt.
+
Wechsle zum Coaching-Tab, um dein erstes Thema anzulegen.
+
+ ) : (
+
+ {dashboard.contexts.map(ctx => (
+
handleContextClick(ctx.id)}
+ role="button"
+ tabIndex={0}
+ onKeyDown={e => e.key === 'Enter' && handleContextClick(ctx.id)}
+ >
+
{ctx.title}
+
+ {_categoryLabel(ctx.category)}
+ {ctx.sessionCount} Sessions
+
+ {ctx.lastSessionAt && (
+
+ Letzte Session: {_formatDate(ctx.lastSessionAt)}
+
+ )}
+
+ ))}
+
+ )}
+
+
+ {/* Quick Start */}
+
+
Tipp des Tages
+
+
Konsistenz schlaegt Intensitaet. Auch 10 Minuten taegliches Coaching-Gespraech
+ bringt messbare Fortschritte in deiner Kommunikationskompetenz.
+
+
+
+ );
+};
+
+function _categoryLabel(category: string): string {
+ const labels: Record = {
+ leadership: 'Fuehrung',
+ conflict: 'Konflikt',
+ negotiation: 'Verhandlung',
+ presentation: 'Praesentation',
+ feedback: 'Feedback',
+ delegation: 'Delegation',
+ changeManagement: 'Change Mgmt',
+ custom: 'Individuell',
+ };
+ return labels[category] || category;
+}
+
+function _formatDate(isoStr: string): string {
+ try {
+ const d = new Date(isoStr);
+ return d.toLocaleDateString('de-CH', { day: '2-digit', month: '2-digit', year: 'numeric' });
+ } catch { return isoStr; }
+}
+
+export default CommcoachDashboardView;
diff --git a/src/pages/views/commcoach/CommcoachDossierView.module.css b/src/pages/views/commcoach/CommcoachDossierView.module.css
new file mode 100644
index 0000000..cc79280
--- /dev/null
+++ b/src/pages/views/commcoach/CommcoachDossierView.module.css
@@ -0,0 +1,289 @@
+.dossier {
+ padding: 1rem;
+ max-width: 900px;
+}
+
+.contextSelector {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ margin-bottom: 1.25rem;
+}
+
+.contextChip {
+ padding: 0.4rem 0.9rem;
+ background: var(--bg-card, #fff);
+ border: 1px solid var(--border-color, #ddd);
+ border-radius: 20px;
+ cursor: pointer;
+ font-size: 0.85rem;
+ color: var(--text-primary, #333);
+ transition: all 0.15s;
+}
+
+.contextChip:hover {
+ border-color: var(--primary-color, #F25843);
+ color: var(--primary-color, #F25843);
+}
+
+.contextChipActive {
+ background: var(--primary-color, #F25843);
+ color: #fff;
+ border-color: var(--primary-color, #F25843);
+}
+
+.contextChipActive:hover {
+ color: #fff;
+}
+
+.empty {
+ padding: 3rem;
+ text-align: center;
+ color: var(--text-secondary, #666);
+}
+
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 1.5rem;
+}
+
+.title {
+ font-size: 1.3rem;
+ font-weight: 600;
+ color: var(--text-primary, #333);
+ margin: 0 0 0.25rem;
+}
+
+.description {
+ font-size: 0.9rem;
+ color: var(--text-secondary, #666);
+ margin: 0;
+}
+
+.btnArchive {
+ padding: 0.4rem 0.75rem;
+ background: transparent;
+ border: 1px solid var(--border-color, #ddd);
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 0.8rem;
+ color: var(--text-primary, #333);
+}
+
+.btnArchive:hover:not(:disabled) {
+ color: var(--error-color, #dc2626);
+ border-color: var(--error-color, #dc2626);
+}
+
+/* Tabs */
+.tabs {
+ display: flex;
+ gap: 0;
+ border-bottom: 1px solid var(--border-color, #e0e0e0);
+ margin-bottom: 1rem;
+}
+
+.tab {
+ padding: 0.6rem 1.25rem;
+ background: transparent;
+ border: none;
+ border-bottom: 2px solid transparent;
+ cursor: pointer;
+ font-size: 0.85rem;
+ font-weight: 500;
+ color: var(--text-secondary, #666);
+ transition: all 0.15s;
+}
+
+.tab:hover { color: var(--text-primary, #333); }
+
+.tabActive {
+ color: var(--primary-color, #F25843);
+ border-bottom-color: var(--primary-color, #F25843);
+}
+
+.tabContent { min-height: 200px; }
+.emptyTab { text-align: center; padding: 2rem; color: var(--text-secondary, #888); }
+
+/* Tasks */
+.addTaskRow {
+ display: flex;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+}
+
+.addTaskInput {
+ flex: 1;
+ padding: 0.5rem 0.75rem;
+ border: 1px solid var(--border-color, #ddd);
+ border-radius: 6px;
+ font-size: 0.9rem;
+ background: var(--bg-input, #fff);
+ color: var(--text-primary, #333);
+}
+
+.addTaskBtn {
+ padding: 0.5rem 1rem;
+ background: var(--primary-color, #F25843);
+ color: #fff;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 0.85rem;
+}
+
+.addTaskBtn:hover:not(:disabled) { filter: brightness(1.08); }
+.addTaskBtn:disabled {
+ background: var(--color-medium-gray, #ccc);
+ color: var(--text-secondary, #888);
+ cursor: not-allowed;
+ opacity: 0.8;
+}
+
+.taskList { display: flex; flex-direction: column; gap: 0.5rem; }
+
+.taskItem {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.75rem;
+ padding: 0.75rem;
+ background: var(--bg-card, #fff);
+ border: 1px solid var(--border-color, #e0e0e0);
+ border-radius: 8px;
+}
+
+.taskDone { opacity: 0.6; }
+.taskDone .taskTitle { text-decoration: line-through; }
+
+.taskCheck {
+ width: 28px;
+ height: 28px;
+ border: 2px solid var(--border-color, #ccc);
+ border-radius: 50%;
+ background: transparent;
+ cursor: pointer;
+ font-size: 1rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ color: var(--primary-color, #F25843);
+}
+
+.taskContent { flex: 1; }
+.taskTitle { font-size: 0.9rem; font-weight: 500; color: var(--text-primary, #333); }
+.taskDesc { font-size: 0.8rem; color: var(--text-secondary, #666); margin-top: 0.2rem; }
+
+.taskMeta { font-size: 0.75rem; }
+
+.taskPriority {
+ padding: 0.15rem 0.4rem;
+ border-radius: 4px;
+ font-size: 0.7rem;
+ text-transform: uppercase;
+}
+
+.priority_high { background: #fde8e8; color: #c62828; }
+.priority_medium { background: #fff3e0; color: #e65100; }
+.priority_low { background: #e8f5e9; color: #2e7d32; }
+
+.taskDelete {
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ color: var(--text-secondary, #aaa);
+ font-size: 0.9rem;
+ padding: 0.2rem;
+}
+
+.taskDelete:hover { color: var(--error-color, #dc2626); }
+
+/* Sessions */
+.sessionTimeline { display: flex; flex-direction: column; gap: 1rem; }
+
+.sessionItem {
+ background: var(--bg-card, #fff);
+ border: 1px solid var(--border-color, #e0e0e0);
+ border-radius: 8px;
+ padding: 1rem;
+}
+
+.sessionItemHeader {
+ display: flex;
+ gap: 0.75rem;
+ align-items: center;
+ margin-bottom: 0.5rem;
+}
+
+.sessionStatus {
+ padding: 0.15rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.75rem;
+ font-weight: 500;
+}
+
+.status_completed { background: #e8f5e9; color: #2e7d32; }
+.status_active { background: #e3f2fd; color: #1565c0; }
+.status_cancelled { background: #fde8e8; color: #c62828; }
+
+.sessionDate { font-size: 0.8rem; color: var(--text-secondary, #666); }
+.sessionScore { font-size: 0.8rem; font-weight: 600; color: var(--primary-color, #F25843); }
+
+.sessionSummary {
+ font-size: 0.85rem;
+ line-height: 1.5;
+ color: var(--text-primary, #333);
+ margin-bottom: 0.5rem;
+}
+
+.sessionSummary p { margin: 0 0 0.4rem; }
+
+.sessionMeta { font-size: 0.75rem; color: var(--text-secondary, #888); }
+
+/* Scores */
+.scoreList { display: flex; flex-direction: column; gap: 1rem; }
+
+.scoreGroup {
+ background: var(--bg-card, #fff);
+ border: 1px solid var(--border-color, #e0e0e0);
+ border-radius: 8px;
+ padding: 1rem;
+}
+
+.scoreDimension {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ margin-bottom: 0.5rem;
+}
+
+.scoreDimensionLabel { font-weight: 600; font-size: 0.9rem; flex: 1; }
+.scoreLatest { font-weight: 700; font-size: 1rem; color: var(--primary-color, #F25843); }
+
+.scoreTrend { font-size: 0.75rem; }
+.trend_improving { color: #2e7d32; }
+.trend_stable { color: #e65100; }
+.trend_declining { color: #c62828; }
+
+.scoreBar {
+ height: 6px;
+ background: var(--bg-hover, #eee);
+ border-radius: 3px;
+ overflow: hidden;
+ margin-bottom: 0.5rem;
+}
+
+.scoreBarFill {
+ height: 100%;
+ background: var(--primary-color, #F25843);
+ border-radius: 3px;
+ transition: width 0.3s;
+}
+
+.scoreEvidence {
+ font-size: 0.8rem;
+ color: var(--text-secondary, #666);
+ line-height: 1.4;
+}
diff --git a/src/pages/views/commcoach/CommcoachDossierView.tsx b/src/pages/views/commcoach/CommcoachDossierView.tsx
new file mode 100644
index 0000000..df289e3
--- /dev/null
+++ b/src/pages/views/commcoach/CommcoachDossierView.tsx
@@ -0,0 +1,242 @@
+/**
+ * CommCoach Dossier View
+ *
+ * Shows context detail: sessions timeline, tasks checklist, scores, insights.
+ */
+
+import React, { useState, useCallback, useEffect } from 'react';
+import { useCommcoach } from '../../../hooks/useCommcoach';
+import ReactMarkdown from 'react-markdown';
+import styles from './CommcoachDossierView.module.css';
+
+export const CommcoachDossierView: React.FC = () => {
+ const coach = useCommcoach();
+ const [newTaskTitle, setNewTaskTitle] = useState('');
+ const [activeTab, setActiveTab] = useState<'sessions' | 'tasks' | 'scores'>('tasks');
+
+ useEffect(() => {
+ if (!coach.selectedContextId && coach.contexts.length > 0) {
+ coach.selectContext(coach.contexts[0].id);
+ }
+ }, [coach.contexts, coach.selectedContextId, coach.selectContext]);
+
+ const handleAddTask = useCallback(async () => {
+ if (!newTaskTitle.trim()) return;
+ await coach.addTask(newTaskTitle);
+ setNewTaskTitle('');
+ }, [newTaskTitle, coach]);
+
+ if (coach.loadingContexts) {
+ return ;
+ }
+
+ if (coach.contexts.length === 0) {
+ return (
+
+
Noch keine Coaching-Themen vorhanden. Erstelle zuerst eines im Coaching-Tab.
+
+ );
+ }
+
+ return (
+
+ {/* Context Selector */}
+
+ {coach.contexts.map(ctx => (
+
+ ))}
+
+
+ {!coach.selectedContextId ? (
+
Waehle ein Coaching-Thema.
+ ) : (<>
+ {/* Context Header */}
+
+
+
{coach.selectedContext?.title}
+ {coach.selectedContext?.description && (
+
{coach.selectedContext.description}
+ )}
+
+
+
+
+
+
+ {/* Tab Navigation */}
+
+
+
+
+
+
+ {/* Tasks Tab */}
+ {activeTab === 'tasks' && (
+
+
+ setNewTaskTitle(e.target.value)}
+ onKeyDown={e => e.key === 'Enter' && handleAddTask()}
+ />
+
+
+ {coach.tasks.length === 0 ? (
+
Noch keine Aufgaben. Der Coach schlaegt waehrend Sessions Aufgaben vor.
+ ) : (
+
+ {coach.tasks.map(task => (
+
+
+
+
{task.title}
+ {task.description &&
{task.description}
}
+
+
+
+ {task.priority}
+
+
+
+
+ ))}
+
+ )}
+
+ )}
+
+ {/* Sessions Tab */}
+ {activeTab === 'sessions' && (
+
+ {coach.sessions.length === 0 ? (
+
Noch keine abgeschlossenen Sessions.
+ ) : (
+
+ {coach.sessions.map(s => (
+
+
+
+ {s.status === 'completed' ? 'Abgeschlossen' : s.status === 'active' ? 'Aktiv' : 'Abgebrochen'}
+
+
+ {s.startedAt ? new Date(s.startedAt).toLocaleDateString('de-CH') : ''}
+
+ {s.competenceScore != null && (
+ Score: {Math.round(s.competenceScore)}
+ )}
+
+ {s.summary && (
+
+ {s.summary}
+
+ )}
+
+ {s.messageCount} Nachrichten | {Math.round(s.durationSeconds / 60)} Min.
+
+
+ ))}
+
+ )}
+
+ )}
+
+ {/* Scores Tab */}
+ {activeTab === 'scores' && (
+
+ {coach.scores.length === 0 ? (
+
Noch keine Bewertungen. Schliesse eine Session ab, um Scores zu erhalten.
+ ) : (
+
+ {_groupScoresByDimension(coach.scores).map(group => (
+
+
+ {_dimensionLabel(group.dimension)}
+ {Math.round(group.latest.score)}/100
+
+ {group.latest.trend === 'improving' ? 'steigend' : group.latest.trend === 'declining' ? 'sinkend' : 'stabil'}
+
+
+
+ {group.latest.evidence && (
+
{group.latest.evidence}
+ )}
+
+ ))}
+
+ )}
+
+ )}
+ >)}
+
+ );
+};
+
+interface ScoreGroup {
+ dimension: string;
+ latest: { score: number; trend: string; evidence?: string };
+ history: Array<{ score: number; createdAt?: string }>;
+}
+
+function _groupScoresByDimension(scores: any[]): ScoreGroup[] {
+ const groups: Record = {};
+ for (const s of scores) {
+ const dim = s.dimension;
+ if (!groups[dim]) {
+ groups[dim] = { dimension: dim, latest: s, history: [] };
+ }
+ groups[dim].history.push({ score: s.score, createdAt: s.createdAt });
+ if (s.createdAt > (groups[dim].latest.createdAt || '')) {
+ groups[dim].latest = s;
+ }
+ }
+ return Object.values(groups);
+}
+
+function _dimensionLabel(dim: string): string {
+ const labels: Record = {
+ empathy: 'Einfuehlungsvermoegen',
+ clarity: 'Klarheit',
+ assertiveness: 'Durchsetzung',
+ listening: 'Zuhoeren',
+ selfReflection: 'Selbstreflexion',
+ };
+ return labels[dim] || dim;
+}
+
+export default CommcoachDossierView;
diff --git a/src/pages/views/commcoach/CommcoachSettingsView.module.css b/src/pages/views/commcoach/CommcoachSettingsView.module.css
new file mode 100644
index 0000000..6e8da67
--- /dev/null
+++ b/src/pages/views/commcoach/CommcoachSettingsView.module.css
@@ -0,0 +1,156 @@
+.settings {
+ padding: 1rem;
+ max-width: 600px;
+}
+
+.heading {
+ font-size: 1.2rem;
+ font-weight: 600;
+ margin-bottom: 1.5rem;
+ color: var(--text-primary, #333);
+}
+
+.loading {
+ padding: 2rem;
+ text-align: center;
+ color: var(--text-secondary, #666);
+}
+
+.error {
+ padding: 0.5rem 0.75rem;
+ background: #fde8e8;
+ color: var(--color-error, #d32f2f);
+ border-radius: 6px;
+ margin-bottom: 1rem;
+ font-size: 0.85rem;
+}
+
+.success {
+ padding: 0.5rem 0.75rem;
+ background: #e8f5e9;
+ color: #2e7d32;
+ border-radius: 6px;
+ margin-bottom: 1rem;
+ font-size: 0.85rem;
+}
+
+.section {
+ margin-bottom: 2rem;
+}
+
+.sectionTitle {
+ font-size: 1rem;
+ font-weight: 600;
+ margin-bottom: 0.75rem;
+ color: var(--text-primary, #333);
+}
+
+.field {
+ margin-bottom: 0.75rem;
+}
+
+.label {
+ display: block;
+ font-size: 0.85rem;
+ font-weight: 500;
+ margin-bottom: 0.3rem;
+ color: var(--text-primary, #333);
+}
+
+.select, .input {
+ width: 100%;
+ padding: 0.5rem 0.75rem;
+ border: 1px solid var(--border-color, #ddd);
+ border-radius: 6px;
+ font-size: 0.9rem;
+ background: var(--bg-input, #fff);
+ color: var(--text-primary, #333);
+}
+
+.voiceRow {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.voiceRow .select {
+ flex: 1;
+}
+
+.testBtn {
+ padding: 0.5rem 1rem;
+ background: var(--primary-color, #F25843);
+ color: #fff;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 0.85rem;
+}
+
+.testBtn:hover:not(:disabled) { filter: brightness(1.08); }
+.testBtn:disabled {
+ background: var(--color-medium-gray, #ccc);
+ color: var(--text-secondary, #888);
+ cursor: not-allowed;
+ opacity: 0.8;
+}
+
+.checkboxLabel {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-size: 0.9rem;
+ cursor: pointer;
+ color: var(--text-primary, #333);
+}
+
+.checkboxLabel input[type="checkbox"] {
+ width: 16px;
+ height: 16px;
+}
+
+.statsGrid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 0.75rem;
+}
+
+.statItem {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 0.75rem;
+ background: var(--bg-card, #fff);
+ border: 1px solid var(--border-color, #e0e0e0);
+ border-radius: 8px;
+}
+
+.statValue {
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: var(--primary-color, #F25843);
+}
+
+.statLabel {
+ font-size: 0.75rem;
+ color: var(--text-secondary, #666);
+}
+
+.saveBtn {
+ width: 100%;
+ padding: 0.6rem;
+ background: var(--primary-color, #F25843);
+ color: #fff;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 0.9rem;
+ font-weight: 500;
+}
+
+.saveBtn:hover:not(:disabled) { filter: brightness(1.08); }
+.saveBtn:disabled {
+ background: var(--color-medium-gray, #ccc);
+ color: var(--text-secondary, #888);
+ cursor: not-allowed;
+ opacity: 0.8;
+}
diff --git a/src/pages/views/commcoach/CommcoachSettingsView.tsx b/src/pages/views/commcoach/CommcoachSettingsView.tsx
new file mode 100644
index 0000000..b7af4cd
--- /dev/null
+++ b/src/pages/views/commcoach/CommcoachSettingsView.tsx
@@ -0,0 +1,243 @@
+/**
+ * CommCoach Settings View
+ *
+ * User profile settings: voice preferences, reminders, email notifications.
+ */
+
+import React, { useState, useEffect, useCallback } from 'react';
+import { useApiRequest } from '../../../hooks/useApi';
+import { useInstanceId } from '../../../hooks/useCurrentInstance';
+import {
+ getProfileApi, updateProfileApi,
+ getVoiceLanguagesApi, getVoiceVoicesApi, testVoiceApi,
+ type CoachingUserProfile,
+} from '../../../api/commcoachApi';
+import styles from './CommcoachSettingsView.module.css';
+
+export const CommcoachSettingsView: React.FC = () => {
+ const { request } = useApiRequest();
+ const instanceId = useInstanceId();
+
+ const [profile, setProfile] = useState(null);
+ const [languages, setLanguages] = useState([]);
+ const [voices, setVoices] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [testing, setTesting] = useState(false);
+ const [error, setError] = useState(null);
+ const [success, setSuccess] = useState(null);
+
+ const [language, setLanguage] = useState('de-DE');
+ const [voiceId, setVoiceId] = useState('');
+ const [reminderEnabled, setReminderEnabled] = useState(false);
+ const [reminderTime, setReminderTime] = useState('09:00');
+ const [emailEnabled, setEmailEnabled] = useState(true);
+
+ useEffect(() => {
+ if (!instanceId) return;
+ const loadData = async () => {
+ setLoading(true);
+ try {
+ const [profileData, languagesData] = await Promise.all([
+ getProfileApi(request, instanceId),
+ getVoiceLanguagesApi(request, instanceId),
+ ]);
+ setProfile(profileData);
+ setLanguages(languagesData || []);
+
+ if (profileData) {
+ setLanguage(profileData.preferredLanguage || 'de-DE');
+ setVoiceId(profileData.preferredVoice || '');
+ setReminderEnabled(profileData.dailyReminderEnabled || false);
+ setReminderTime(profileData.dailyReminderTime || '09:00');
+ setEmailEnabled(profileData.emailSummaryEnabled !== false);
+ }
+
+ const voicesData = await getVoiceVoicesApi(request, instanceId, profileData?.preferredLanguage || 'de-DE');
+ setVoices(voicesData || []);
+ } catch (err: any) {
+ setError(err.message || 'Fehler beim Laden');
+ } finally {
+ setLoading(false);
+ }
+ };
+ loadData();
+ }, [request, instanceId]);
+
+ const handleLanguageChange = useCallback(async (newLang: string) => {
+ setLanguage(newLang);
+ if (!instanceId) return;
+ try {
+ const voicesData = await getVoiceVoicesApi(request, instanceId, newLang);
+ setVoices(voicesData || []);
+ setVoiceId('');
+ } catch { /* ignore */ }
+ }, [request, instanceId]);
+
+ const handleSave = useCallback(async () => {
+ if (!instanceId) return;
+ setSaving(true);
+ setError(null);
+ setSuccess(null);
+ try {
+ const updated = await updateProfileApi(request, instanceId, {
+ preferredLanguage: language,
+ preferredVoice: voiceId || null,
+ dailyReminderEnabled: reminderEnabled,
+ dailyReminderTime: reminderTime,
+ emailSummaryEnabled: emailEnabled,
+ });
+ setProfile(updated);
+ setSuccess('Einstellungen gespeichert');
+ setTimeout(() => setSuccess(null), 3000);
+ } catch (err: any) {
+ setError(err.message || 'Fehler beim Speichern');
+ } finally {
+ setSaving(false);
+ }
+ }, [request, instanceId, language, voiceId, reminderEnabled, reminderTime, emailEnabled]);
+
+ const handleTestVoice = useCallback(async () => {
+ if (!instanceId) return;
+ setTesting(true);
+ try {
+ const result = await testVoiceApi(request, instanceId, {
+ language,
+ voiceId: voiceId || undefined,
+ });
+ if (result.success && result.audio) {
+ const audioData = `data:audio/mp3;base64,${result.audio}`;
+ const audio = new Audio(audioData);
+ audio.play();
+ }
+ } catch (err: any) {
+ setError('Sprachtest fehlgeschlagen');
+ } finally {
+ setTesting(false);
+ }
+ }, [request, instanceId, language, voiceId]);
+
+ if (loading) {
+ return Einstellungen werden geladen...
;
+ }
+
+ return (
+
+
Coaching-Einstellungen
+
+ {error &&
{error}
}
+ {success &&
{success}
}
+
+ {/* Voice Settings */}
+
+
Sprache und Stimme
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Reminder Settings */}
+
+
+ {/* Stats */}
+ {profile && (
+
+
Statistik
+
+
+ {profile.totalSessions}
+ Sessions gesamt
+
+
+ {profile.totalMinutes}
+ Minuten gesamt
+
+
+ {profile.streakDays}
+ Aktueller Streak
+
+
+ {profile.longestStreak}
+ Laengster Streak
+
+
+
+ )}
+
+
+
+ );
+};
+
+export default CommcoachSettingsView;
diff --git a/src/pages/views/commcoach/index.ts b/src/pages/views/commcoach/index.ts
new file mode 100644
index 0000000..6f4299b
--- /dev/null
+++ b/src/pages/views/commcoach/index.ts
@@ -0,0 +1,4 @@
+export { CommcoachDashboardView } from './CommcoachDashboardView';
+export { CommcoachCoachingView } from './CommcoachCoachingView';
+export { CommcoachDossierView } from './CommcoachDossierView';
+export { CommcoachSettingsView } from './CommcoachSettingsView';
diff --git a/src/pages/views/teamsbot/TeamsbotSessionView.tsx b/src/pages/views/teamsbot/TeamsbotSessionView.tsx
index b6873f6..adacc36 100644
--- a/src/pages/views/teamsbot/TeamsbotSessionView.tsx
+++ b/src/pages/views/teamsbot/TeamsbotSessionView.tsx
@@ -103,6 +103,10 @@ export const TeamsbotSessionView: React.FC = () => {
const sseEvent: TeamsbotSSEEvent = JSON.parse(event.data);
switch (sseEvent.type) {
+ case 'sessionState':
+ if (sseEvent.data) setSession(prev => prev ? { ...prev, ...sseEvent.data } : sseEvent.data);
+ break;
+
case 'transcript':
setTranscripts(prev => [...prev, sseEvent.data as TeamsbotTranscript]);
break;
@@ -118,6 +122,11 @@ export const TeamsbotSessionView: React.FC = () => {
eventSource.close();
eventSourceRef.current = null;
sseSessionRef.current = null;
+ teamsbotApi.getSession(instanceId, sessionId).then((result) => {
+ setSession(result.session);
+ if (result.transcripts) setTranscripts(result.transcripts);
+ if (result.botResponses) setBotResponses(result.botResponses);
+ }).catch(() => {});
}
break;
diff --git a/src/types/mandate.ts b/src/types/mandate.ts
index 0d64f97..32e1732 100644
--- a/src/types/mandate.ts
+++ b/src/types/mandate.ts
@@ -290,6 +290,17 @@ export const FEATURE_REGISTRY: Record = {
{ code: 'attributes', label: { de: 'Attribute', en: 'Attributes', fr: 'Attributs' }, path: 'attributes' },
]
},
+ commcoach: {
+ code: 'commcoach',
+ label: { de: 'Kommunikations-Coach', en: 'Communication Coach', fr: 'Coach Communication' },
+ icon: 'account_voice',
+ views: [
+ { code: 'dashboard', label: { de: 'Dashboard', en: 'Dashboard', fr: 'Tableau de bord' }, path: 'dashboard' },
+ { code: 'coaching', label: { de: 'Coaching', en: 'Coaching', fr: 'Coaching' }, path: 'coaching' },
+ { code: 'dossier', label: { de: 'Dossier', en: 'Dossier', fr: 'Dossier' }, path: 'dossier' },
+ { code: 'settings', label: { de: 'Einstellungen', en: 'Settings', fr: 'Paramètres' }, path: 'settings' },
+ ]
+ },
};
// =============================================================================