diff --git a/src/App.tsx b/src/App.tsx index aac8210..c81e8fb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -179,12 +179,15 @@ function App() { } /> } /> + {/* Shared: assistant + modules routes (ComCoach + TeamsBot) */} + } /> + } /> + {/* Neutralization Feature Views */} } /> {/* CommCoach Feature Views */} - } /> - } /> + } /> {/* Redmine Feature Views */} } /> diff --git a/src/api/commcoachApi.ts b/src/api/commcoachApi.ts index df0ed6c..5d758a1 100644 --- a/src/api/commcoachApi.ts +++ b/src/api/commcoachApi.ts @@ -109,8 +109,8 @@ export interface CoachingUserProfile { } export interface DashboardData { - totalContexts: number; - activeContexts: number; + totalModules: number; + activeModules: number; totalSessions: number; totalMinutes: number; streakDays: number; @@ -122,7 +122,11 @@ export interface DashboardData { 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 }>; + modules: Array<{ id: string; title: string; moduleType: string; sessionCount: number; lastSessionAt?: string; goalProgress?: number }>; + /** @deprecated Use totalModules/activeModules/modules instead */ + totalContexts?: number; + activeContexts?: number; + contexts?: Array<{ id: string; title: string; category: string; sessionCount: number; lastSessionAt?: string; goalProgress?: number }>; } export interface SSEEvent { @@ -133,31 +137,73 @@ export interface SSEEvent { export type ApiRequestFunction = (options: ApiRequestOptions) => Promise; +export function getApiRequest(): ApiRequestFunction { + return async (options: ApiRequestOptions) => { + const response = await api(options); + return response.data; + }; +} + // ============================================================================ -// Context API +// Module API (TrainingModule — replaces Context API) +// ============================================================================ + +export async function listModulesApi(request: ApiRequestFunction, instanceId: string): Promise { + const data = await request({ url: `/api/commcoach/${instanceId}/modules`, method: 'get' }); + return data.modules || []; +} + +export async function createModuleApi(request: ApiRequestFunction, instanceId: string, body: { + title: string; moduleType?: string; goals?: string; personaId?: string; kpiTargets?: string; +}): Promise { + const data = await request({ url: `/api/commcoach/${instanceId}/modules`, method: 'post', data: body }); + return data.module; +} + +export async function getModuleDetailApi(request: ApiRequestFunction, instanceId: string, moduleId: string): Promise { + const data = await request({ url: `/api/commcoach/${instanceId}/modules/${moduleId}`, method: 'get' }); + return data; +} + +export async function updateModuleApi(request: ApiRequestFunction, instanceId: string, moduleId: string, body: any): Promise { + const data = await request({ url: `/api/commcoach/${instanceId}/modules/${moduleId}`, method: 'put', data: body }); + return data.module; +} + +export async function deleteModuleApi(request: ApiRequestFunction, instanceId: string, moduleId: string): Promise { + await request({ url: `/api/commcoach/${instanceId}/modules/${moduleId}`, method: 'delete' }); +} + +export async function listSessionsApi(request: ApiRequestFunction, instanceId: string, moduleId: string): Promise { + const data = await request({ url: `/api/commcoach/${instanceId}/modules/${moduleId}/sessions`, method: 'get' }); + return data.sessions || []; +} + +// ============================================================================ +// Context / Module API (uses /modules/ endpoints) // ============================================================================ export async function getContextsApi(request: ApiRequestFunction, instanceId: string): Promise { - const data = await request({ url: `/api/commcoach/${instanceId}/contexts`, method: 'get' }); - return data.contexts || []; + const data = await request({ url: `/api/commcoach/${instanceId}/modules`, method: 'get' }); + return data.modules || []; } export async function createContextApi(request: ApiRequestFunction, instanceId: string, body: { title: string; description?: string; category?: string; goals?: string[]; }): Promise { - const data = await request({ url: `/api/commcoach/${instanceId}/contexts`, method: 'post', data: body }); - return data.context; + const data = await request({ url: `/api/commcoach/${instanceId}/modules`, method: 'post', data: body }); + return data.module; } export async function getContextDetailApi(request: ApiRequestFunction, instanceId: string, contextId: string): Promise<{ context: CoachingContext; tasks: CoachingTask[]; scores: CoachingScore[]; sessions: CoachingSession[]; }> { const data = await request({ - url: `/api/commcoach/${instanceId}/contexts/${contextId}`, + url: `/api/commcoach/${instanceId}/modules/${contextId}`, method: 'get', params: { _t: Date.now() }, }); - const ctx = data?.context ?? data; + const ctx = data?.module ?? data; return { context: ctx, tasks: data?.tasks ?? [], @@ -167,22 +213,22 @@ export async function getContextDetailApi(request: ApiRequestFunction, instanceI } export async function updateContextApi(request: ApiRequestFunction, instanceId: string, contextId: string, body: any): Promise { - const data = await request({ url: `/api/commcoach/${instanceId}/contexts/${contextId}`, method: 'put', data: body }); - return data.context; + const data = await request({ url: `/api/commcoach/${instanceId}/modules/${contextId}`, method: 'put', data: body }); + return data.module; } export async function deleteContextApi(request: ApiRequestFunction, instanceId: string, contextId: string): Promise { - await request({ url: `/api/commcoach/${instanceId}/contexts/${contextId}`, method: 'delete' }); + await request({ url: `/api/commcoach/${instanceId}/modules/${contextId}`, method: 'delete' }); } export async function archiveContextApi(request: ApiRequestFunction, instanceId: string, contextId: string): Promise { - const data = await request({ url: `/api/commcoach/${instanceId}/contexts/${contextId}/archive`, method: 'post' }); - return data.context; + const data = await request({ url: `/api/commcoach/${instanceId}/modules/${contextId}/archive`, method: 'post' }); + return data.module; } export async function activateContextApi(request: ApiRequestFunction, instanceId: string, contextId: string): Promise { - const data = await request({ url: `/api/commcoach/${instanceId}/contexts/${contextId}/activate`, method: 'post' }); - return data.context; + const data = await request({ url: `/api/commcoach/${instanceId}/modules/${contextId}/activate`, method: 'post' }); + return data.module; } // ============================================================================ @@ -192,7 +238,7 @@ export async function activateContextApi(request: ApiRequestFunction, instanceId export async function startSessionApi(request: ApiRequestFunction, instanceId: string, contextId: string): Promise<{ session: CoachingSession; messages: CoachingMessage[]; resumed: boolean; }> { - const data = await request({ url: `/api/commcoach/${instanceId}/contexts/${contextId}/sessions/start`, method: 'post' }); + const data = await request({ url: `/api/commcoach/${instanceId}/modules/${contextId}/sessions/start`, method: 'post' }); return data; } @@ -207,7 +253,7 @@ export async function startSessionStreamApi( try { const baseURL = api.defaults.baseURL || ''; const personaParam = personaId ? `?personaId=${encodeURIComponent(personaId)}` : ''; - const url = `${baseURL}/api/commcoach/${instanceId}/contexts/${contextId}/sessions/start${personaParam}`; + const url = `${baseURL}/api/commcoach/${instanceId}/modules/${contextId}/sessions/start${personaParam}`; const headers: Record = { 'Content-Type': 'application/json' }; const authToken = localStorage.getItem('authToken'); @@ -243,14 +289,11 @@ export async function startSessionStreamApi( for (const line of lines) { if (line.startsWith('data: ')) { - try { - const jsonStr = line.slice(6); - if (jsonStr.trim()) { - const event: SSEEvent = JSON.parse(jsonStr); - onEvent(event); - } - } catch { - // skip malformed lines + const jsonStr = line.slice(6); + if (jsonStr.trim()) { + let event: SSEEvent; + try { event = JSON.parse(jsonStr); } catch { continue; } + onEvent(event); } } } @@ -348,14 +391,11 @@ export async function sendMessageStreamApi( for (const line of lines) { if (line.startsWith('data: ')) { - try { - const jsonStr = line.slice(6); - if (jsonStr.trim()) { - const event: SSEEvent = JSON.parse(jsonStr); - onEvent(event); - } - } catch { - // skip malformed lines + const jsonStr = line.slice(6); + if (jsonStr.trim()) { + let event: SSEEvent; + try { event = JSON.parse(jsonStr); } catch { continue; } + onEvent(event); } } } @@ -424,10 +464,12 @@ export async function sendAudioStreamApi( for (const line of lines) { if (line.startsWith('data: ')) { - try { - const jsonStr = line.slice(6); - if (jsonStr.trim()) onEvent(JSON.parse(jsonStr)); - } catch { /* skip */ } + const jsonStr = line.slice(6); + if (jsonStr.trim()) { + let event: SSEEvent; + try { event = JSON.parse(jsonStr); } catch { continue; } + onEvent(event); + } } } } @@ -446,14 +488,14 @@ export async function sendAudioStreamApi( // ============================================================================ export async function getTasksApi(request: ApiRequestFunction, instanceId: string, contextId: string): Promise { - const data = await request({ url: `/api/commcoach/${instanceId}/contexts/${contextId}/tasks`, method: 'get' }); + const data = await request({ url: `/api/commcoach/${instanceId}/modules/${contextId}/tasks`, method: 'get' }); return data.tasks || []; } export async function createTaskApi(request: ApiRequestFunction, instanceId: string, contextId: string, body: { title: string; description?: string; priority?: string; dueDate?: string; }): Promise { - const data = await request({ url: `/api/commcoach/${instanceId}/contexts/${contextId}/tasks`, method: 'post', data: body }); + const data = await request({ url: `/api/commcoach/${instanceId}/modules/${contextId}/tasks`, method: 'post', data: body }); return data.task; } @@ -500,7 +542,14 @@ export async function updateProfileApi(request: ApiRequestFunction, instanceId: export async function getPersonasApi(request: ApiRequestFunction, instanceId: string): Promise { const data = await request({ url: `/api/commcoach/${instanceId}/personas`, method: 'get' }); - return data.personas || []; + return data.items || data.personas || []; +} + +export async function fetchPersonasPaginated(request: ApiRequestFunction, instanceId: string, params?: any): Promise { + const queryParams: Record = {}; + if (params) queryParams.pagination = JSON.stringify(params); + const data = await request({ url: `/api/commcoach/${instanceId}/personas`, method: 'get', params: queryParams }); + return data; } export async function createPersonaApi(request: ApiRequestFunction, instanceId: string, body: { @@ -510,10 +559,31 @@ export async function createPersonaApi(request: ApiRequestFunction, instanceId: return data.persona; } +export async function updatePersonaApi(request: ApiRequestFunction, instanceId: string, personaId: string, body: { + label?: string; description?: string; gender?: string; systemPromptOverride?: string; isActive?: boolean; +}): Promise { + const data = await request({ url: `/api/commcoach/${instanceId}/personas/${personaId}`, method: 'put', data: body }); + return data.persona; +} + export async function deletePersonaApi(request: ApiRequestFunction, instanceId: string, personaId: string): Promise { await request({ url: `/api/commcoach/${instanceId}/personas/${personaId}`, method: 'delete' }); } +// ============================================================================ +// Module-Persona Mapping API +// ============================================================================ + +export async function getModulePersonasApi(request: ApiRequestFunction, instanceId: string, moduleId: string): Promise { + const data = await request({ url: `/api/commcoach/${instanceId}/modules/${moduleId}/personas`, method: 'get' }); + return data.personaIds || []; +} + +export async function setModulePersonasApi(request: ApiRequestFunction, instanceId: string, moduleId: string, personaIds: string[]): Promise { + const data = await request({ url: `/api/commcoach/${instanceId}/modules/${moduleId}/personas`, method: 'put', data: { personaIds } }); + return data.personaIds || []; +} + // ============================================================================ // Badge API (Iteration 2) // ============================================================================ @@ -529,7 +599,7 @@ export async function getBadgesApi(request: ApiRequestFunction, instanceId: stri 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}`; + return `${baseURL}/api/commcoach/${instanceId}/modules/${contextId}/export?format=${format}`; } export function getSessionExportUrl(instanceId: string, sessionId: string, format: string = 'md'): string { @@ -544,6 +614,6 @@ export function getSessionExportUrl(instanceId: string, sessionId: string, forma export async function getScoreHistoryApi(request: ApiRequestFunction, instanceId: string, contextId: string): Promise>> { - const data = await request({ url: `/api/commcoach/${instanceId}/contexts/${contextId}/scores/history`, method: 'get' }); + const data = await request({ url: `/api/commcoach/${instanceId}/modules/${contextId}/scores/history`, method: 'get' }); return data.history || {}; } diff --git a/src/api/teamsbotApi.ts b/src/api/teamsbotApi.ts index 462268d..3918f7c 100644 --- a/src/api/teamsbotApi.ts +++ b/src/api/teamsbotApi.ts @@ -9,6 +9,7 @@ export interface TeamsbotSession { id: string; instanceId: string; mandateId: string; + moduleId?: string; meetingLink: string; botName: string; status: 'pending' | 'joining' | 'active' | 'leaving' | 'ended' | 'error'; @@ -574,3 +575,48 @@ export async function deleteDirectorPrompt( ); return response.data; } + + +// ============================================================================ +// Meeting Module API +// ============================================================================ + +export interface MeetingModule { + id: string; + instanceId: string; + mandateId: string; + ownerUserId: string; + title: string; + seriesType: string; + defaultBotId?: string; + defaultDirectorPrompts?: string; + goals?: string; + kpiTargets?: string; + status: string; +} + +export async function listModules(instanceId: string): Promise { + const response = await api.get(`/api/teamsbot/${instanceId}/modules`); + return response.data?.modules || []; +} + +export async function createModule(instanceId: string, body: { + title: string; seriesType?: string; defaultBotId?: string; goals?: string; kpiTargets?: string; +}): Promise { + const response = await api.post(`/api/teamsbot/${instanceId}/modules`, body); + return response.data?.module; +} + +export async function getModuleDetail(instanceId: string, moduleId: string): Promise<{ module: MeetingModule; sessions: TeamsbotSession[] }> { + const response = await api.get(`/api/teamsbot/${instanceId}/modules/${moduleId}`); + return response.data; +} + +export async function updateModule(instanceId: string, moduleId: string, body: Partial): Promise { + const response = await api.put(`/api/teamsbot/${instanceId}/modules/${moduleId}`, body); + return response.data?.module; +} + +export async function deleteModule(instanceId: string, moduleId: string): Promise { + await api.delete(`/api/teamsbot/${instanceId}/modules/${moduleId}`); +} diff --git a/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.module.css b/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.module.css index 97a8592..e0ab989 100644 --- a/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.module.css +++ b/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.module.css @@ -352,10 +352,18 @@ min-width: 0; } +/* File size + hover actions group (overlapping layout to save width) */ +.nodeSizeGroup { + position: relative; + flex-shrink: 0; + width: 52px; + display: flex; + align-items: center; + justify-content: flex-end; +} + /* File size column */ .nodeSize { - width: 52px; - flex-shrink: 0; font-size: 10px; color: var(--color-text-muted, #94a3b8); text-align: right; @@ -388,20 +396,29 @@ min-width: 0; } -/* Hover action icons (download, delete) -- only visible on hover, left of persistent */ +/* Hover action icons -- overlay on top of file size to save width */ .nodeActionsHover { + position: absolute; + right: 0; + top: 0; + bottom: 0; display: flex; align-items: center; + justify-content: flex-end; gap: 2px; - flex-shrink: 0; opacity: 0; transition: opacity 0.15s ease; + z-index: 1; } .nodeRow:hover .nodeActionsHover { opacity: 1; } +.nodeRow:hover .nodeSize { + visibility: hidden; +} + /* Persistent action icons (scope, neutralize) -- always visible, right-aligned */ .nodeActionsPersistent { display: flex; @@ -626,6 +643,10 @@ .nodeActionsHover { opacity: 1; } + + .nodeSize { + visibility: hidden; + } } /* Accessibility */ diff --git a/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx b/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx index e480963..189a0f0 100644 --- a/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx +++ b/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx @@ -12,6 +12,8 @@ import type { ScopeValue, TreeBatchAction, } from './types'; +import { useConfirm } from '../../../hooks/useConfirm'; +import { useLanguage } from '../../../providers/language/LanguageContext'; import styles from './FormGeneratorTree.module.css'; const INDENT_PX = 24; @@ -290,43 +292,45 @@ const TreeNodeRow = React.memo(function TreeNodeRow({ )} - - {node.sizeBytes != null ? _formatSize(node.sizeBytes) : ''} - +
+ + {node.sizeBytes != null ? _formatSize(node.sizeBytes) : ''} + -
- {canRename && ( - - )} +
+ {canRename && ( + + )} - {node.type !== 'folder' && ( - - )} + {node.type !== 'folder' && ( + + )} - {canDelete && ( - - )} + {canDelete && ( + + )} +
@@ -392,6 +396,8 @@ export function FormGeneratorTree({ onSendToChat, className, }: FormGeneratorTreeProps) { + const { t } = useLanguage(); + const { confirm, ConfirmDialog } = useConfirm(); const [nodes, setNodes] = useState[]>([]); const [expandedIds, setExpandedIds] = useState>(new Set()); const [selectedIds, setSelectedIds] = useState>(new Set()); @@ -581,7 +587,11 @@ export function FormGeneratorTree({ async (id: string) => { const node = nodes.find((n) => n.id === id); const label = node?.name ?? id; - if (!window.confirm(`"${label}" wirklich loeschen?`)) return; + const ok = await confirm( + t('"{label}" wirklich loeschen?', { label }), + { confirmLabel: t('Loeschen'), variant: 'danger' }, + ); + if (!ok) return; await provider.deleteNodes?.([id]); setNodes((prev) => { const toRemove = new Set([id, ..._collectDescendantIds(id, prev)]); @@ -811,6 +821,7 @@ export function FormGeneratorTree({ return (
+ {title && (
({ className={`${styles.batchButton} ${action.danger ? styles.batchButtonDanger : ''}`} onClick={async () => { if (action.danger) { - if (!window.confirm(`${ids.length} ${action.label} wirklich loeschen?`)) return; + const ok = await confirm( + t('{count} {label} wirklich loeschen?', { count: String(ids.length), label: action.label }), + { confirmLabel: t('Loeschen'), variant: 'danger' }, + ); + if (!ok) return; } await action.onClick(ids); await _handleRefresh(); diff --git a/src/components/UnifiedDataBar/FilesTab.tsx b/src/components/UnifiedDataBar/FilesTab.tsx index 43a20e4..eb9f713 100644 --- a/src/components/UnifiedDataBar/FilesTab.tsx +++ b/src/components/UnifiedDataBar/FilesTab.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useRef, useMemo, useState } from 'react'; +import React, { useCallback, useRef, useMemo, useState, useEffect } from 'react'; import type { UdbContext } from './UnifiedDataBar'; import api from '../../api'; import { useApiRequest } from '../../hooks/useApi'; @@ -45,6 +45,12 @@ const FilesTab: React.FC = ({ context, onFileSelect, onSendToChat setSharedTreeKey(k => k + 1); }, []); + useEffect(() => { + const _onFileUploaded = () => _handleRefresh(); + window.addEventListener('fileUploaded', _onFileUploaded); + return () => window.removeEventListener('fileUploaded', _onFileUploaded); + }, [_handleRefresh]); + const _uploadFiles = useCallback(async (fileList: FileList | File[]) => { if (!context.instanceId || uploading) return; setUploading(true); @@ -76,7 +82,9 @@ const FilesTab: React.FC = ({ context, onFileSelect, onSendToChat const _handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); - setIsDragOver(false); + if (!e.relatedTarget || !(e.currentTarget as Node).contains(e.relatedTarget as Node)) { + setIsDragOver(false); + } }, []); const _handleDrop = useCallback((e: React.DragEvent) => { diff --git a/src/components/UnifiedDataBar/UnifiedDataBar.tsx b/src/components/UnifiedDataBar/UnifiedDataBar.tsx index bc7f0a4..4eaf16a 100644 --- a/src/components/UnifiedDataBar/UnifiedDataBar.tsx +++ b/src/components/UnifiedDataBar/UnifiedDataBar.tsx @@ -64,7 +64,7 @@ interface UnifiedDataBarProps { function _tabLabel(tab: UdbTab, t: (k: string) => string): string { switch (tab) { - case 'chats': return t('Chatverläufe'); + case 'chats': return t('Dossiers'); case 'files': return t('Dateien'); case 'sources': return t('Quellen'); default: return tab; diff --git a/src/hooks/useCommcoach.ts b/src/hooks/useCommcoach.ts index 62ec296..6c49a67 100644 --- a/src/hooks/useCommcoach.ts +++ b/src/hooks/useCommcoach.ts @@ -289,6 +289,16 @@ export function useCommcoach(instanceIdOverride?: string): CommcoachHookReturn { setTasks(prev => [eventData, ...prev]); } else if (eventType === 'documentCreated' && eventData) { onDocumentCreatedRef.current?.(eventData); + const docMsg: CoachingMessage = { + id: `doc-${Date.now()}`, + sessionId: session.id, + contextId: session.contextId, + role: 'assistant', + content: `📄 **Dokument erstellt:** ${eventData.fileName || 'Dokument'}\n\n_Das Dokument ist in der Seitenleiste unter "Dateien" verfuegbar._`, + contentType: 'systemNote', + createdAt: new Date().toISOString(), + }; + setMessages(prev => [...prev, docMsg]); } else if (eventType === 'error' && eventData) { setError(eventData.message || 'Stream-Fehler'); } @@ -397,6 +407,16 @@ export function useCommcoach(instanceIdOverride?: string): CommcoachHookReturn { setTasks(prev => [eventData, ...prev]); } else if (eventType === 'documentCreated' && eventData) { onDocumentCreatedRef.current?.(eventData); + const docMsg: CoachingMessage = { + id: `doc-${Date.now()}`, + sessionId: session.id, + contextId: session.contextId, + role: 'assistant', + content: `📄 **Dokument erstellt:** ${eventData.fileName || 'Dokument'}\n\n_Das Dokument ist in der Seitenleiste unter "Dateien" verfuegbar._`, + contentType: 'systemNote', + createdAt: new Date().toISOString(), + }; + setMessages(prev => [...prev, docMsg]); } else if (eventType === 'scoreUpdate') { // Will refresh on complete } else if (eventType === 'error' && eventData) { @@ -474,6 +494,16 @@ export function useCommcoach(instanceIdOverride?: string): CommcoachHookReturn { setTasks(prev => [eventData, ...prev]); } else if (eventType === 'documentCreated' && eventData) { onDocumentCreatedRef.current?.(eventData); + const docMsg: CoachingMessage = { + id: `doc-${Date.now()}`, + sessionId: session.id, + contextId: session.contextId, + role: 'assistant', + content: `📄 **Dokument erstellt:** ${eventData.fileName || 'Dokument'}\n\n_Das Dokument ist in der Seitenleiste unter "Dateien" verfuegbar._`, + contentType: 'systemNote', + createdAt: new Date().toISOString(), + }; + setMessages(prev => [...prev, docMsg]); } else if (eventType === 'error' && eventData) { setError(eventData.message || 'Audio-Fehler'); } diff --git a/src/hooks/useTtsPlayback.ts b/src/hooks/useTtsPlayback.ts index ecb3edd..cbb3d99 100644 --- a/src/hooks/useTtsPlayback.ts +++ b/src/hooks/useTtsPlayback.ts @@ -41,8 +41,11 @@ export function useTtsPlayback(callbacks?: TtsPlaybackCallbacks): TtsPlaybackApi const stop = useCallback(() => { if (audioRef.current) { - audioRef.current.pause(); + const audio = audioRef.current; + audio.onpause = null; + audio.onended = null; audioRef.current = null; + audio.pause(); } setIsPlaying(false); setIsPaused(false); diff --git a/src/layouts/MainLayout.tsx b/src/layouts/MainLayout.tsx index 2f96ac0..880c702 100644 --- a/src/layouts/MainLayout.tsx +++ b/src/layouts/MainLayout.tsx @@ -19,7 +19,7 @@ import styles from './MainLayout.module.css'; import { useLanguage } from '../providers/language/LanguageContext'; const _WORKSPACE_ROUTE_RE = /\/mandates\/[^/]+\/workspace\/[^/]+\/dashboard/; -const _COMMCOACH_ROUTE_RE = /\/mandates\/[^/]+\/commcoach\/[^/]+\/(?:coaching|dossier)/; +const _COMMCOACH_ROUTE_RE = /\/mandates\/[^/]+\/commcoach\/[^/]+\/session/; const _GE_EDITOR_ROUTE_RE = /\/mandates\/[^/]+\/graphicalEditor\/[^/]+\/editor/; const _ADMIN_LANGUAGES_RE = /\/admin\/languages(?:$|\/)/; diff --git a/src/pages/FeatureView.tsx b/src/pages/FeatureView.tsx index 7fb75e3..8497fee 100644 --- a/src/pages/FeatureView.tsx +++ b/src/pages/FeatureView.tsx @@ -39,6 +39,8 @@ import { WorkspaceRagInsightsPage } from './views/workspace/WorkspaceRagInsights // Teamsbot Views import { TeamsbotDashboardView } from './views/teamsbot/TeamsbotDashboardView'; +import { TeamsbotAssistantView } from './views/teamsbot/TeamsbotAssistantView'; +import { TeamsbotModulesView } from './views/teamsbot/TeamsbotModulesView'; import { TeamsbotSessionView } from './views/teamsbot/TeamsbotSessionView'; import { TeamsbotSettingsView } from './views/teamsbot/TeamsbotSettingsView'; @@ -46,7 +48,7 @@ import { TeamsbotSettingsView } from './views/teamsbot/TeamsbotSettingsView'; import { NeutralizationView } from './views/neutralization'; // CommCoach Views -import { CommcoachDashboardView, CommcoachDossierView, CommcoachSettingsView } from './views/commcoach'; +import { CommcoachDashboardView, CommcoachAssistantView, CommcoachModulesView, CommcoachSessionView, CommcoachDossierView, CommcoachSettingsView } from './views/commcoach'; // Redmine Views import { RedmineSettingsView, RedmineStatsView, RedmineBrowserView } from './views/redmine'; @@ -158,6 +160,8 @@ const VIEW_COMPONENTS: Record> = { }, teamsbot: { dashboard: TeamsbotDashboardView, + assistant: TeamsbotAssistantView, + modules: TeamsbotModulesView, sessions: TeamsbotSessionView, settings: TeamsbotSettingsView, }, @@ -167,7 +171,9 @@ const VIEW_COMPONENTS: Record> = { }, commcoach: { dashboard: CommcoachDashboardView, - coaching: CommcoachDossierView, + assistant: CommcoachAssistantView, + modules: CommcoachModulesView, + session: CommcoachSessionView, dossier: CommcoachDossierView, settings: CommcoachSettingsView, }, @@ -228,8 +234,8 @@ export const FeatureViewPage: React.FC = ({ view }) => { return null; } - // CommCoach coaching/dossier is rendered persistently by CommcoachKeepAlive at MainLayout level. - if (featureCode === 'commcoach' && (view === 'coaching' || view === 'dossier')) { + // CommCoach session is rendered persistently by CommcoachKeepAlive at MainLayout level. + if (featureCode === 'commcoach' && view === 'session') { return null; } diff --git a/src/pages/views/commcoach/Commcoach.module.css b/src/pages/views/commcoach/Commcoach.module.css new file mode 100644 index 0000000..4101cbf --- /dev/null +++ b/src/pages/views/commcoach/Commcoach.module.css @@ -0,0 +1,349 @@ +/* CommCoach Shared Styles — Assistant, Modules, Session views */ + +.assistantContainer, +.modulesContainer { + padding: 1.5rem; + display: flex; + flex-direction: column; + gap: 1rem; + height: 100%; + overflow-y: auto; +} + +.wizardHeader { + display: flex; + align-items: center; + justify-content: space-between; +} + +.stepIndicator { + display: flex; + gap: 0.5rem; +} + +.stepDot { + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--border-color, #ccc); +} + +.stepActive { + background: var(--primary-color, #F25843); +} + +.wizardContent { + flex: 1; + display: flex; + flex-direction: column; +} + +.wizardStep { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.typeGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 1rem; +} + +.typeCard { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + padding: 1.25rem; + border-radius: 12px; + border: 2px solid var(--border-color, #ddd); + background: var(--bg-card, #fff); + color: var(--text-primary, #333); + cursor: pointer; + transition: border-color 0.15s; +} + +.typeCard:hover { + border-color: var(--primary-color, #F25843); +} + +.typeCardActive { + border-color: var(--primary-color, #F25843); + background: rgba(242, 88, 67, 0.06); +} + +.typeIcon { + font-size: 2rem; +} + +.wizardInput { + padding: 0.75rem; + border-radius: 8px; + border: 1px solid var(--border-color, #ddd); + background: var(--bg-input, #fff); + color: var(--text-primary, #333); + font-size: 1rem; +} + +.wizardTextarea { + padding: 0.75rem; + border-radius: 8px; + border: 1px solid var(--border-color, #ddd); + background: var(--bg-input, #fff); + color: var(--text-primary, #333); + font-size: 1rem; + resize: vertical; +} + +.wizardActions { + display: flex; + gap: 1rem; + padding-top: 1rem; + border-top: 1px solid var(--border-color, #e0e0e0); +} + +.wizardHint { + color: var(--text-secondary, #666); + font-size: 0.9rem; +} + +.confirmSummary { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 1rem; + background: var(--surface-color, #f5f5f5); + border-radius: 8px; +} + +.errorBanner { + background: rgba(241, 76, 76, 0.1); + color: #f14c4c; + padding: 0.75rem 1rem; + border-radius: 8px; +} + +.modulesHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; +} + +.modulesFilters { + display: flex; + gap: 0.5rem; +} + +.modulesFilters select { + padding: 0.4rem 0.75rem; + border-radius: 6px; + border: 1px solid var(--border-color, #ddd); + background: var(--bg-input, #fff); + color: var(--text-primary, #333); + font-size: 0.85rem; +} + +.modulesList { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.moduleCard { + background: var(--bg-card, #fff); + border-radius: 8px; + overflow: hidden; + border: 1px solid var(--border-color, #e0e0e0); +} + +.moduleExpanded { + border-color: var(--primary-color, #F25843); +} + +.moduleHighlighted { + box-shadow: 0 0 0 2px var(--primary-color, #F25843); +} + +.moduleRow { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.75rem 1rem; + cursor: pointer; +} + +.moduleRow:hover { + background: var(--bg-hover, #f5f5f5); +} + +.moduleType { + font-size: 0.75rem; + padding: 0.2rem 0.5rem; + border-radius: 4px; + background: rgba(242, 88, 67, 0.1); + color: var(--primary-color, #F25843); + white-space: nowrap; +} + +.moduleTitle { + flex: 1; + font-weight: 500; +} + +.moduleStatus { + font-size: 0.8rem; + color: var(--text-secondary, #666); +} + +.moduleSessions { + font-size: 0.85rem; + color: var(--text-secondary, #666); +} + +.moduleActions { + display: flex; + gap: 0.5rem; +} + +.sessionList { + padding: 0.5rem 0 0 1rem; + border-top: 1px solid var(--border-color, #e0e0e0); +} + +.sessionRow { + display: flex; + gap: 1rem; + padding: 0.4rem 0; + font-size: 0.9rem; +} + +.sessionStatus { + font-size: 0.8rem; + color: var(--text-secondary, #666); +} + +.sessionDate { + font-size: 0.8rem; + color: var(--text-secondary, #666); +} + +.noSessions { + color: var(--text-secondary, #666); + font-style: italic; + font-size: 0.9rem; + padding: 0.5rem 0; +} + +.confirmOverlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.4); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.confirmDialog, +.editDialog { + background: var(--bg-card, #fff); + border: 1px solid var(--border-color, #e0e0e0); + border-radius: 12px; + padding: 1.5rem; + max-width: 400px; + width: 90%; + display: flex; + flex-direction: column; + gap: 1rem; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); +} + +.confirmActions { + display: flex; + gap: 0.75rem; + justify-content: flex-end; +} + +.btnPrimary { + padding: 0.6rem 1.2rem; + border-radius: 8px; + border: none; + background: var(--primary-color, #F25843); + color: #fff; + font-weight: 500; + cursor: pointer; +} + +.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.6rem 1.2rem; + border-radius: 8px; + border: 1px solid var(--border-color, #ddd); + background: transparent; + color: var(--text-primary, #333); + cursor: pointer; +} + +.btnSecondary:hover:not(:disabled) { background: var(--hover-bg, #f5f5f5); border-color: var(--primary-color, #F25843); color: var(--primary-color, #F25843); } + +.btnDanger { + padding: 0.6rem 1.2rem; + border-radius: 8px; + border: none; + background: #f14c4c; + color: #fff; + font-weight: 500; + cursor: pointer; +} + +.btnDanger:hover:not(:disabled) { filter: brightness(1.08); } + +.btnSmall { + padding: 0.3rem 0.7rem; + border-radius: 4px; + border: 1px solid var(--border-color, #ddd); + background: transparent; + color: var(--text-primary, #333); + font-size: 0.8rem; + cursor: pointer; +} + +.btnSmall:hover:not(:disabled) { border-color: var(--primary-color, #F25843); color: var(--primary-color, #F25843); } + +.btnSmall.btnSmallActive { + background: var(--primary-color, #F25843); + border-color: var(--primary-color, #F25843); + color: #fff; +} + +.btnSmall.btnSmallActive:hover:not(:disabled) { filter: brightness(1.08); color: #fff; } + +.btnSmallDanger { + padding: 0.3rem 0.7rem; + border-radius: 4px; + border: 1px solid #f14c4c; + background: transparent; + color: #f14c4c; + font-size: 0.8rem; + cursor: pointer; +} + +.btnSmallDanger:hover:not(:disabled) { background: var(--error-color, #dc2626); color: #fff; } + +.loading { + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; + color: var(--text-secondary, #666); +} diff --git a/src/pages/views/commcoach/CommcoachAssistantView.tsx b/src/pages/views/commcoach/CommcoachAssistantView.tsx new file mode 100644 index 0000000..c0352e9 --- /dev/null +++ b/src/pages/views/commcoach/CommcoachAssistantView.tsx @@ -0,0 +1,175 @@ +/** + * CommCoach Assistant View + * + * Wizard flow: Module type → Topic → Persona → KPIs → "Start first session" + */ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useCurrentInstance } from '../../../hooks/useCurrentInstance'; +import * as commcoachApi from '../../../api/commcoachApi'; +import { useLanguage } from '../../../providers/language/LanguageContext'; +import styles from './Commcoach.module.css'; + +type WizardStep = 'type' | 'topic' | 'persona' | 'kpis' | 'confirm'; + +const STEPS: WizardStep[] = ['type', 'topic', 'persona', 'kpis', 'confirm']; + +const MODULE_TYPES = [ + { value: 'coaching', label: 'Coaching', icon: '🎯' }, + { value: 'training', label: 'Training', icon: '📚' }, + { value: 'exam', label: 'Prüfung', icon: '✍️' }, + { value: 'elearning', label: 'E-Learning', icon: '💻' }, +]; + +export const CommcoachAssistantView: React.FC = () => { + const { t } = useLanguage(); + const { instance, mandateId } = useCurrentInstance(); + const instanceId = instance?.id || ''; + const navigate = useNavigate(); + + const [step, setStep] = useState('type'); + const [moduleType, setModuleType] = useState('coaching'); + const [title, setTitle] = useState(''); + const [goals, setGoals] = useState(''); + const [personaId, setPersonaId] = useState(null); + const [personas, setPersonas] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const stepIdx = STEPS.indexOf(step); + + const _handleNext = () => { + const nextIdx = stepIdx + 1; + if (nextIdx < STEPS.length) setStep(STEPS[nextIdx]); + }; + + const _handleBack = () => { + const prevIdx = stepIdx - 1; + if (prevIdx >= 0) setStep(STEPS[prevIdx]); + }; + + const _handleCreate = async () => { + if (!title.trim()) { + setError(t('Bitte einen Titel eingeben')); + return; + } + setLoading(true); + setError(null); + try { + const params = new URLSearchParams(window.location.search); + const apiRequest = commcoachApi.getApiRequest(); + const module = await commcoachApi.createModuleApi(apiRequest, instanceId, { + title: title.trim(), + moduleType, + goals: goals.trim() || undefined, + personaId: personaId || undefined, + }); + navigate(`/mandates/${mandateId}/commcoach/${instanceId}/session?moduleId=${module.id}`); + } catch (err: any) { + setError(err?.message || t('Fehler beim Erstellen')); + } finally { + setLoading(false); + } + }; + + return ( +
+
+

{t('Neues Modul erstellen')}

+
+ {STEPS.map((s, i) => ( +
+ ))} +
+
+ + {error &&
{error}
} + +
+ {step === 'type' && ( +
+

{t('Modul-Typ wählen')}

+
+ {MODULE_TYPES.map(mt => ( + + ))} +
+
+ )} + + {step === 'topic' && ( +
+

{t('Thema & Titel')}

+ setTitle(e.target.value)} + autoFocus + /> +