iteration 2 done
This commit is contained in:
parent
cd14e8a6fb
commit
5186e58e00
7 changed files with 483 additions and 5 deletions
|
|
@ -29,6 +29,7 @@ export interface CoachingSession {
|
|||
contextId: string;
|
||||
userId: string;
|
||||
status: string;
|
||||
personaId?: string;
|
||||
summary?: string;
|
||||
durationSeconds: number;
|
||||
messageCount: number;
|
||||
|
|
@ -38,6 +39,38 @@ export interface CoachingSession {
|
|||
endedAt?: string;
|
||||
}
|
||||
|
||||
export interface CoachingPersona {
|
||||
id: string;
|
||||
userId: string;
|
||||
key: string;
|
||||
label: string;
|
||||
description: string;
|
||||
gender?: string;
|
||||
category: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export interface CoachingDocument {
|
||||
id: string;
|
||||
contextId: string;
|
||||
fileName: string;
|
||||
mimeType: string;
|
||||
fileSize: number;
|
||||
extractedText?: string;
|
||||
summary?: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface CoachingBadge {
|
||||
id: string;
|
||||
userId: string;
|
||||
badgeKey: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
awardedAt?: string;
|
||||
}
|
||||
|
||||
export interface CoachingMessage {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
|
|
@ -100,6 +133,8 @@ export interface DashboardData {
|
|||
openTasks: number;
|
||||
completedTasks: number;
|
||||
goalProgress?: number;
|
||||
badges?: CoachingBadge[];
|
||||
level?: { number: number; label: string; totalSessions: number };
|
||||
contexts: Array<{ id: string; title: string; category: string; sessionCount: number; lastSessionAt?: string; goalProgress?: number }>;
|
||||
}
|
||||
|
||||
|
|
@ -180,10 +215,12 @@ export async function startSessionStreamApi(
|
|||
onEvent: (event: SSEEvent) => void,
|
||||
onError?: (error: Error) => void,
|
||||
onComplete?: () => void,
|
||||
personaId?: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const baseURL = api.defaults.baseURL || '';
|
||||
const url = `${baseURL}/api/commcoach/${instanceId}/contexts/${contextId}/sessions/start`;
|
||||
const personaParam = personaId ? `?personaId=${encodeURIComponent(personaId)}` : '';
|
||||
const url = `${baseURL}/api/commcoach/${instanceId}/contexts/${contextId}/sessions/start${personaParam}`;
|
||||
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
const authToken = localStorage.getItem('authToken');
|
||||
|
|
@ -474,3 +511,88 @@ export async function testVoiceApi(request: ApiRequestFunction, instanceId: stri
|
|||
const data = await request({ url: `/api/commcoach/${instanceId}/voice/tts`, method: 'post', data: body });
|
||||
return data;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Persona API (Iteration 2)
|
||||
// ============================================================================
|
||||
|
||||
export async function getPersonasApi(request: ApiRequestFunction, instanceId: string): Promise<CoachingPersona[]> {
|
||||
const data = await request({ url: `/api/commcoach/${instanceId}/personas`, method: 'get' });
|
||||
return data.personas || [];
|
||||
}
|
||||
|
||||
export async function createPersonaApi(request: ApiRequestFunction, instanceId: string, body: {
|
||||
label: string; description: string; gender?: string; systemPromptOverride?: string;
|
||||
}): Promise<CoachingPersona> {
|
||||
const data = await request({ url: `/api/commcoach/${instanceId}/personas`, method: 'post', data: body });
|
||||
return data.persona;
|
||||
}
|
||||
|
||||
export async function deletePersonaApi(request: ApiRequestFunction, instanceId: string, personaId: string): Promise<void> {
|
||||
await request({ url: `/api/commcoach/${instanceId}/personas/${personaId}`, method: 'delete' });
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Document API (Iteration 2)
|
||||
// ============================================================================
|
||||
|
||||
export async function getDocumentsApi(request: ApiRequestFunction, instanceId: string, contextId: string): Promise<CoachingDocument[]> {
|
||||
const data = await request({ url: `/api/commcoach/${instanceId}/contexts/${contextId}/documents`, method: 'get' });
|
||||
return data.documents || [];
|
||||
}
|
||||
|
||||
export async function uploadDocumentApi(instanceId: string, contextId: string, file: File): Promise<CoachingDocument> {
|
||||
const baseURL = api.defaults.baseURL || '';
|
||||
const url = `${baseURL}/api/commcoach/${instanceId}/contexts/${contextId}/documents`;
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
const authToken = localStorage.getItem('authToken');
|
||||
if (authToken) headers['Authorization'] = `Bearer ${authToken}`;
|
||||
if (!getCSRFToken()) generateAndStoreCSRFToken();
|
||||
addCSRFTokenToHeaders(headers);
|
||||
|
||||
const response = await fetch(url, { method: 'POST', headers, body: formData, credentials: 'include' });
|
||||
if (!response.ok) throw new Error(`Upload failed: ${response.status}`);
|
||||
const data = await response.json();
|
||||
return data.document;
|
||||
}
|
||||
|
||||
export async function deleteDocumentApi(request: ApiRequestFunction, instanceId: string, documentId: string): Promise<void> {
|
||||
await request({ url: `/api/commcoach/${instanceId}/documents/${documentId}`, method: 'delete' });
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Badge API (Iteration 2)
|
||||
// ============================================================================
|
||||
|
||||
export async function getBadgesApi(request: ApiRequestFunction, instanceId: string): Promise<CoachingBadge[]> {
|
||||
const data = await request({ url: `/api/commcoach/${instanceId}/badges`, method: 'get' });
|
||||
return data.badges || [];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Export API (Iteration 2)
|
||||
// ============================================================================
|
||||
|
||||
export function getDossierExportUrl(instanceId: string, contextId: string, format: string = 'md'): string {
|
||||
const baseURL = api.defaults.baseURL || '';
|
||||
return `${baseURL}/api/commcoach/${instanceId}/contexts/${contextId}/export?format=${format}`;
|
||||
}
|
||||
|
||||
export function getSessionExportUrl(instanceId: string, sessionId: string, format: string = 'md'): string {
|
||||
const baseURL = api.defaults.baseURL || '';
|
||||
return `${baseURL}/api/commcoach/${instanceId}/sessions/${sessionId}/export?format=${format}`;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Score History API (Iteration 2)
|
||||
// ============================================================================
|
||||
|
||||
export async function getScoreHistoryApi(request: ApiRequestFunction, instanceId: string, contextId: string): Promise<Record<string, Array<{
|
||||
score: number; trend: string; evidence?: string; createdAt?: string; sessionId?: string;
|
||||
}>>> {
|
||||
const data = await request({ url: `/api/commcoach/${instanceId}/contexts/${contextId}/scores/history`, method: 'get' });
|
||||
return data.history || {};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export interface CommcoachHookReturn {
|
|||
createContext: (title: string, description?: string, category?: string, goals?: string[]) => Promise<void>;
|
||||
archiveContext: (contextId: string) => Promise<void>;
|
||||
|
||||
startSession: () => Promise<void>;
|
||||
startSession: (personaId?: string) => Promise<void>;
|
||||
sendMessage: (content: string) => Promise<void>;
|
||||
sendAudio: (audioBlob: Blob) => Promise<void>;
|
||||
completeSession: () => Promise<void>;
|
||||
|
|
@ -212,7 +212,7 @@ export function useCommcoach(): CommcoachHookReturn {
|
|||
}
|
||||
}, [request, instanceId, selectedContextId, refreshContexts]);
|
||||
|
||||
const startSessionCb = useCallback(async () => {
|
||||
const startSessionCb = useCallback(async (personaId?: string) => {
|
||||
if (!instanceId || !selectedContextId) return;
|
||||
await _unlockAudioForTts();
|
||||
setError(null);
|
||||
|
|
@ -278,6 +278,7 @@ export function useCommcoach(): CommcoachHookReturn {
|
|||
setStreamingMessage(null);
|
||||
}
|
||||
},
|
||||
personaId,
|
||||
);
|
||||
} catch (err: any) {
|
||||
if (isMountedRef.current) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@
|
|||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useCommcoach } from '../../../hooks/useCommcoach';
|
||||
import { useApiRequest } from '../../../hooks/useApi';
|
||||
import { useInstanceId } from '../../../hooks/useCurrentInstance';
|
||||
import { getPersonasApi, type CoachingPersona } from '../../../api/commcoachApi';
|
||||
import AutoScroll from '../../../components/UiComponents/AutoScroll/AutoScroll';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
|
|
@ -17,11 +20,15 @@ import styles from './CommcoachCoachingView.module.css';
|
|||
export const CommcoachCoachingView: React.FC = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const coach = useCommcoach();
|
||||
const { request } = useApiRequest();
|
||||
const instanceId = useInstanceId();
|
||||
const [showNewContext, setShowNewContext] = useState(false);
|
||||
const [newTitle, setNewTitle] = useState('');
|
||||
const [newDescription, setNewDescription] = useState('');
|
||||
const [newCategory, setNewCategory] = useState('custom');
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [personas, setPersonas] = useState<CoachingPersona[]>([]);
|
||||
const [selectedPersonaId, setSelectedPersonaId] = useState<string | undefined>(undefined);
|
||||
|
||||
const streamRef = useRef<MediaStream | null>(null);
|
||||
const speechRecognitionRef = useRef<SpeechRecognition | null>(null);
|
||||
|
|
@ -71,6 +78,13 @@ export const CommcoachCoachingView: React.FC = () => {
|
|||
}
|
||||
}, [coach.session]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!instanceId) return;
|
||||
getPersonasApi(request, instanceId)
|
||||
.then(p => setPersonas(p))
|
||||
.catch(() => {});
|
||||
}, [instanceId, request]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!coach.session || coach.isMuted) {
|
||||
if (speechRecognitionRef.current) {
|
||||
|
|
@ -284,8 +298,36 @@ export const CommcoachCoachingView: React.FC = () => {
|
|||
<div className={styles.sessionStart}>
|
||||
<h3>{coach.selectedContext?.title}</h3>
|
||||
<p>{coach.selectedContext?.description || 'Starte eine neue Coaching-Session zu diesem Thema.'}</p>
|
||||
<button className={styles.btnPrimary} onClick={coach.startSession}>
|
||||
|
||||
{personas.length > 0 && (
|
||||
<div className={styles.personaSelector}>
|
||||
<label className={styles.personaLabel}>Gespraechspartner waehlen:</label>
|
||||
<div className={styles.personaGrid}>
|
||||
{personas.map(p => (
|
||||
<button
|
||||
key={p.id}
|
||||
className={`${styles.personaChip} ${selectedPersonaId === p.id ? styles.personaChipActive : ''}`}
|
||||
onClick={() => setSelectedPersonaId(selectedPersonaId === p.id ? undefined : p.id)}
|
||||
title={p.description}
|
||||
>
|
||||
<span className={styles.personaGender}>
|
||||
{p.gender === 'f' ? '\u2640' : p.gender === 'm' ? '\u2642' : '\u25CB'}
|
||||
</span>
|
||||
<span className={styles.personaName}>{p.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
className={styles.btnPrimary}
|
||||
onClick={() => coach.startSession(selectedPersonaId)}
|
||||
>
|
||||
Session starten
|
||||
{selectedPersonaId && personas.find(p => p.id === selectedPersonaId)
|
||||
? ` mit ${personas.find(p => p.id === selectedPersonaId)!.label}`
|
||||
: ''}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -114,6 +114,32 @@
|
|||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.badgeGrid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.badgeCard {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.9rem;
|
||||
background: var(--bg-card, #fff);
|
||||
border: 1px solid var(--border-color, #e0e0e0);
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.badgeIcon {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.badgeLabel {
|
||||
font-weight: 500;
|
||||
color: var(--text-primary, #333);
|
||||
}
|
||||
|
||||
.tipCard {
|
||||
background: var(--bg-card, #fff);
|
||||
border: 1px solid var(--border-color, #e0e0e0);
|
||||
|
|
|
|||
|
|
@ -99,6 +99,27 @@ export const CommcoachDashboardView: React.FC = () => {
|
|||
)}
|
||||
</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>
|
||||
|
|
@ -125,6 +146,15 @@ function _categoryLabel(category: string): string {
|
|||
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);
|
||||
|
|
|
|||
|
|
@ -287,3 +287,116 @@
|
|||
color: var(--text-secondary, #666);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Score History */
|
||||
.scoreHistory {
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.scoreHistoryLabel {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary, #888);
|
||||
}
|
||||
|
||||
.scoreHistoryPoints {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.scoreHistoryPoint {
|
||||
padding: 0.15rem 0.4rem;
|
||||
background: var(--bg-hover, #f0f0f0);
|
||||
border-radius: 4px;
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-secondary, #666);
|
||||
}
|
||||
|
||||
/* Export Button */
|
||||
.btnExport {
|
||||
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);
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btnExport:hover {
|
||||
border-color: var(--primary-color, #F25843);
|
||||
color: var(--primary-color, #F25843);
|
||||
}
|
||||
|
||||
.headerActions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Session Export */
|
||||
.sessionExport {
|
||||
margin-left: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--primary-color, #F25843);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.sessionExport:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Documents */
|
||||
.uploadLabel {
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--primary-color, #F25843);
|
||||
color: #fff;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.uploadLabel:hover { filter: brightness(1.08); }
|
||||
|
||||
.documentList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.documentItem {
|
||||
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;
|
||||
}
|
||||
|
||||
.documentInfo { flex: 1; }
|
||||
|
||||
.documentName {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary, #333);
|
||||
}
|
||||
|
||||
.documentMeta {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary, #888);
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.documentSummary {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary, #666);
|
||||
margin-top: 0.4rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,13 +6,26 @@
|
|||
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { useCommcoach } from '../../../hooks/useCommcoach';
|
||||
import { useApiRequest } from '../../../hooks/useApi';
|
||||
import { useInstanceId } from '../../../hooks/useCurrentInstance';
|
||||
import {
|
||||
getDossierExportUrl, getSessionExportUrl,
|
||||
getDocumentsApi, uploadDocumentApi, deleteDocumentApi,
|
||||
getScoreHistoryApi,
|
||||
type CoachingDocument,
|
||||
} from '../../../api/commcoachApi';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import styles from './CommcoachDossierView.module.css';
|
||||
|
||||
export const CommcoachDossierView: React.FC = () => {
|
||||
const coach = useCommcoach();
|
||||
const { request } = useApiRequest();
|
||||
const instanceId = useInstanceId();
|
||||
const [newTaskTitle, setNewTaskTitle] = useState('');
|
||||
const [activeTab, setActiveTab] = useState<'sessions' | 'tasks' | 'scores'>('tasks');
|
||||
const [activeTab, setActiveTab] = useState<'sessions' | 'tasks' | 'scores' | 'documents'>('tasks');
|
||||
const [documents, setDocuments] = useState<CoachingDocument[]>([]);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [scoreHistory, setScoreHistory] = useState<Record<string, Array<{ score: number; createdAt?: string }>>>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (!coach.selectedContextId && coach.contexts.length > 0) {
|
||||
|
|
@ -20,6 +33,41 @@ export const CommcoachDossierView: React.FC = () => {
|
|||
}
|
||||
}, [coach.contexts, coach.selectedContextId, coach.selectContext]);
|
||||
|
||||
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]);
|
||||
|
||||
const handleUpload = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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 handleAddTask = useCallback(async () => {
|
||||
if (!newTaskTitle.trim()) return;
|
||||
await coach.addTask(newTaskTitle);
|
||||
|
|
@ -65,6 +113,26 @@ export const CommcoachDossierView: React.FC = () => {
|
|||
)}
|
||||
</div>
|
||||
<div className={styles.headerActions}>
|
||||
{instanceId && coach.selectedContextId && (
|
||||
<>
|
||||
<a
|
||||
className={styles.btnExport}
|
||||
href={getDossierExportUrl(instanceId, coach.selectedContextId, 'md')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Export MD
|
||||
</a>
|
||||
<a
|
||||
className={styles.btnExport}
|
||||
href={getDossierExportUrl(instanceId, coach.selectedContextId, 'pdf')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Export PDF
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
<button className={styles.btnArchive} onClick={() => coach.archiveContext(coach.selectedContextId!)}>
|
||||
Archivieren
|
||||
</button>
|
||||
|
|
@ -91,6 +159,12 @@ export const CommcoachDossierView: React.FC = () => {
|
|||
>
|
||||
Bewertungen ({coach.scores.length})
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.tab} ${activeTab === 'documents' ? styles.tabActive : ''}`}
|
||||
onClick={() => setActiveTab('documents')}
|
||||
>
|
||||
Dokumente ({documents.length})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tasks Tab */}
|
||||
|
|
@ -166,6 +240,18 @@ export const CommcoachDossierView: React.FC = () => {
|
|||
)}
|
||||
<div className={styles.sessionMeta}>
|
||||
{s.messageCount} Nachrichten | {Math.round(s.durationSeconds / 60)} Min.
|
||||
{s.personaId && <span> | Persona</span>}
|
||||
{instanceId && s.status === 'completed' && (
|
||||
<a
|
||||
className={styles.sessionExport}
|
||||
href={getSessionExportUrl(instanceId, s.id, 'md')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
Export
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -196,6 +282,58 @@ export const CommcoachDossierView: React.FC = () => {
|
|||
{group.latest.evidence && (
|
||||
<div className={styles.scoreEvidence}>{group.latest.evidence}</div>
|
||||
)}
|
||||
{scoreHistory[group.dimension] && scoreHistory[group.dimension].length > 1 && (
|
||||
<div className={styles.scoreHistory}>
|
||||
<div className={styles.scoreHistoryLabel}>Verlauf:</div>
|
||||
<div className={styles.scoreHistoryPoints}>
|
||||
{scoreHistory[group.dimension].map((entry, i) => (
|
||||
<span key={i} className={styles.scoreHistoryPoint} title={entry.createdAt || ''}>
|
||||
{Math.round(entry.score)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Documents Tab */}
|
||||
{activeTab === 'documents' && (
|
||||
<div className={styles.tabContent}>
|
||||
<div className={styles.addTaskRow}>
|
||||
<label className={styles.uploadLabel}>
|
||||
{uploading ? 'Wird hochgeladen...' : 'Dokument hochladen'}
|
||||
<input
|
||||
type="file"
|
||||
accept=".txt,.md,.pdf,.doc,.docx"
|
||||
onChange={handleUpload}
|
||||
disabled={uploading}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{documents.length === 0 ? (
|
||||
<div className={styles.emptyTab}>Keine Dokumente. Lade Dateien hoch, um sie mit diesem Kontext zu verknuepfen.</div>
|
||||
) : (
|
||||
<div className={styles.documentList}>
|
||||
{documents.map(doc => (
|
||||
<div key={doc.id} className={styles.documentItem}>
|
||||
<div className={styles.documentInfo}>
|
||||
<div className={styles.documentName}>{doc.fileName}</div>
|
||||
<div className={styles.documentMeta}>
|
||||
{_formatFileSize(doc.fileSize)} | {doc.createdAt ? new Date(doc.createdAt).toLocaleDateString('de-CH') : ''}
|
||||
</div>
|
||||
{doc.summary && (
|
||||
<div className={styles.documentSummary}>{doc.summary}</div>
|
||||
)}
|
||||
</div>
|
||||
<button className={styles.taskDelete} onClick={() => handleDeleteDocument(doc.id)}>
|
||||
x
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -228,6 +366,12 @@ function _groupScoresByDimension(scores: any[]): ScoreGroup[] {
|
|||
return Object.values(groups);
|
||||
}
|
||||
|
||||
function _formatFileSize(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
function _dimensionLabel(dim: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
empathy: 'Einfuehlungsvermoegen',
|
||||
|
|
|
|||
Loading…
Reference in a new issue