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->ü.
165 lines
6 KiB
TypeScript
165 lines
6 KiB
TypeScript
/**
|
|
* 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, loading, error } = useCommcoachDashboard();
|
|
|
|
const handleContextClick = (contextId: string) => {
|
|
if (mandateId && instanceId) {
|
|
navigate(`/mandates/${mandateId}/commcoach/${instanceId}/coaching?context=${contextId}`);
|
|
}
|
|
};
|
|
|
|
if (loading && !dashboard) {
|
|
return <div className={styles.loading}>Dashboard wird geladen...</div>;
|
|
}
|
|
|
|
if (error) {
|
|
return <div className={styles.error}>{error}</div>;
|
|
}
|
|
|
|
if (!dashboard) {
|
|
return <div className={styles.empty}>Keine Daten verfügbar.</div>;
|
|
}
|
|
|
|
return (
|
|
<div className={styles.dashboard}>
|
|
{/* KPI Cards */}
|
|
<div className={styles.kpiGrid}>
|
|
<div className={styles.kpiCard}>
|
|
<div className={styles.kpiValue}>{dashboard.streakDays}</div>
|
|
<div className={styles.kpiLabel}>Tage Streak</div>
|
|
<div className={styles.kpiSub}>Rekord: {dashboard.longestStreak}</div>
|
|
</div>
|
|
<div className={styles.kpiCard}>
|
|
<div className={styles.kpiValue}>{dashboard.totalSessions}</div>
|
|
<div className={styles.kpiLabel}>Sessions</div>
|
|
<div className={styles.kpiSub}>{dashboard.totalMinutes} Min. gesamt</div>
|
|
</div>
|
|
<div className={styles.kpiCard}>
|
|
<div className={styles.kpiValue}>
|
|
{dashboard.averageScore != null ? Math.round(dashboard.averageScore) : '--'}
|
|
</div>
|
|
<div className={styles.kpiLabel}>Kompetenz-Score</div>
|
|
<div className={styles.kpiSub}>Durchschnitt</div>
|
|
</div>
|
|
<div className={styles.kpiCard}>
|
|
<div className={styles.kpiValue}>
|
|
{dashboard.goalProgress != null ? `${dashboard.goalProgress}%` : '--'}
|
|
</div>
|
|
<div className={styles.kpiLabel}>Zielfortschritt</div>
|
|
<div className={styles.kpiSub}>{dashboard.openTasks} offene Aufgaben</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Active Contexts */}
|
|
<div className={styles.section}>
|
|
<h3 className={styles.sectionTitle}>Aktive Coaching-Themen</h3>
|
|
{dashboard.contexts.length === 0 ? (
|
|
<div className={styles.emptyState}>
|
|
<p>Noch keine Coaching-Themen erstellt.</p>
|
|
<p>Wechsle zum Coaching-Tab, um dein erstes Thema anzulegen.</p>
|
|
</div>
|
|
) : (
|
|
<div className={styles.contextGrid}>
|
|
{dashboard.contexts.map(ctx => (
|
|
<div
|
|
key={ctx.id}
|
|
className={styles.contextCard}
|
|
onClick={() => handleContextClick(ctx.id)}
|
|
role="button"
|
|
tabIndex={0}
|
|
onKeyDown={e => e.key === 'Enter' && handleContextClick(ctx.id)}
|
|
>
|
|
<div className={styles.contextTitle}>{ctx.title}</div>
|
|
<div className={styles.contextMeta}>
|
|
<span className={styles.contextCategory}>{_categoryLabel(ctx.category)}</span>
|
|
<span>{ctx.sessionCount} Sessions</span>
|
|
{ctx.goalProgress != null && <span>Ziele: {ctx.goalProgress}%</span>}
|
|
</div>
|
|
{ctx.lastSessionAt && (
|
|
<div className={styles.contextLast}>
|
|
Letzte Session: {_formatDate(ctx.lastSessionAt)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Level + Badges */}
|
|
{(dashboard.level || (dashboard.badges && dashboard.badges.length > 0)) && (
|
|
<div className={styles.section}>
|
|
<h3 className={styles.sectionTitle}>
|
|
{dashboard.level
|
|
? `Level ${dashboard.level.number}: ${dashboard.level.label}`
|
|
: 'Auszeichnungen'}
|
|
</h3>
|
|
{dashboard.badges && dashboard.badges.length > 0 && (
|
|
<div className={styles.badgeGrid}>
|
|
{dashboard.badges.map(b => (
|
|
<div key={b.id} className={styles.badgeCard} title={b.description || b.badgeKey}>
|
|
<div className={styles.badgeIcon}>{_badgeIcon(b.icon)}</div>
|
|
<div className={styles.badgeLabel}>{b.label || b.badgeKey}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Quick Start */}
|
|
<div className={styles.section}>
|
|
<h3 className={styles.sectionTitle}>Tipp des Tages</h3>
|
|
<div className={styles.tipCard}>
|
|
<p>Konsistenz schlägt Intensität. Auch 10 Minuten tägliches Coaching-Gespräch
|
|
bringt messbare Fortschritte in deiner Kommunikationskompetenz.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
function _categoryLabel(category: string): string {
|
|
const labels: Record<string, string> = {
|
|
leadership: 'Führung',
|
|
conflict: 'Konflikt',
|
|
negotiation: 'Verhandlung',
|
|
presentation: 'Präsentation',
|
|
feedback: 'Feedback',
|
|
delegation: 'Delegation',
|
|
changeManagement: 'Change Mgmt',
|
|
custom: 'Individuell',
|
|
};
|
|
return labels[category] || category;
|
|
}
|
|
|
|
function _badgeIcon(icon?: string): string {
|
|
const icons: Record<string, string> = {
|
|
star: '\u2605', fire: '\u{1F525}', trophy: '\u{1F3C6}',
|
|
medal: '\u{1F3C5}', layers: '\u{1F4DA}', theater: '\u{1F3AD}',
|
|
compass: '\u{1F9ED}', 'check-circle': '\u2714',
|
|
};
|
|
return icons[icon || 'star'] || '\u2605';
|
|
}
|
|
|
|
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;
|