From 012d4ef4d3d6e7a708b143dd6c63f0e7e6999dd4 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Fri, 13 Feb 2026 00:00:26 +0100 Subject: [PATCH 1/2] teamsbot --- src/App.tsx | 3 + src/api/teamsbotApi.ts | 183 +++++++ src/config/pageRegistry.tsx | 9 +- src/pages/FeatureView.tsx | 10 + src/pages/views/teamsbot/Teamsbot.module.css | 497 ++++++++++++++++++ .../views/teamsbot/TeamsbotDashboardView.tsx | 229 ++++++++ .../views/teamsbot/TeamsbotSessionView.tsx | 225 ++++++++ .../views/teamsbot/TeamsbotSettingsView.tsx | 240 +++++++++ 8 files changed, 1395 insertions(+), 1 deletion(-) create mode 100644 src/api/teamsbotApi.ts create mode 100644 src/pages/views/teamsbot/Teamsbot.module.css create mode 100644 src/pages/views/teamsbot/TeamsbotDashboardView.tsx create mode 100644 src/pages/views/teamsbot/TeamsbotSessionView.tsx create mode 100644 src/pages/views/teamsbot/TeamsbotSettingsView.tsx diff --git a/src/App.tsx b/src/App.tsx index 17aa5ef..dc505aa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -157,6 +157,9 @@ function App() { } /> } /> + {/* Teams Bot Feature Views */} + } /> + {/* Catch-all für unbekannte Sub-Pfade */} } /> diff --git a/src/api/teamsbotApi.ts b/src/api/teamsbotApi.ts new file mode 100644 index 0000000..ded65f4 --- /dev/null +++ b/src/api/teamsbotApi.ts @@ -0,0 +1,183 @@ +import api from '../api'; + +// ============================================================================ +// TYPES & INTERFACES +// ============================================================================ + +export interface TeamsbotSession { + id: string; + instanceId: string; + mandateId: string; + meetingLink: string; + botName: string; + backgroundImageUrl?: string; + status: 'pending' | 'joining' | 'active' | 'leaving' | 'ended' | 'error'; + startedAt?: string; + endedAt?: string; + startedByUserId: string; + bridgeSessionId?: string; + meetingChatId?: string; + summary?: string; + errorMessage?: string; + transcriptSegmentCount: number; + botResponseCount: number; + creationDate?: string; + lastModified?: string; +} + +export interface TeamsbotTranscript { + id: string; + sessionId: string; + speaker?: string; + text: string; + timestamp: string; + confidence: number; + language?: string; + isFinal: boolean; +} + +export interface TeamsbotBotResponse { + id: string; + sessionId: string; + responseText: string; + responseType: 'audio' | 'chat' | 'both'; + detectedIntent: 'addressed' | 'question' | 'proactive' | 'none'; + reasoning?: string; + triggeredByTranscriptId?: string; + modelName?: string; + processingTime: number; + priceCHF: number; + timestamp?: string; +} + +export interface TeamsbotConfig { + botName: string; + backgroundImageUrl?: string; + aiSystemPrompt: string; + responseMode: 'auto' | 'manual' | 'transcribeOnly'; + language: string; + voiceId?: string; + bridgeUrl?: string; + triggerIntervalSeconds: number; + triggerCooldownSeconds: number; + contextWindowSegments: number; +} + +export interface TeamsbotSessionStats { + transcriptSegments: number; + botResponses: number; + totalCostCHF: number; + totalProcessingTime: number; + speakers: string[]; +} + +export interface StartSessionRequest { + meetingLink: string; + botName?: string; + backgroundImageUrl?: string; + connectionId?: string; +} + +export interface ConfigUpdateRequest { + botName?: string; + backgroundImageUrl?: string; + aiSystemPrompt?: string; + responseMode?: 'auto' | 'manual' | 'transcribeOnly'; + language?: string; + voiceId?: string; + bridgeUrl?: string; + triggerIntervalSeconds?: number; + triggerCooldownSeconds?: number; + contextWindowSegments?: number; +} + +// SSE Event Types +export interface TeamsbotSSEEvent { + type: 'transcript' | 'botResponse' | 'analysis' | 'suggestedResponse' | 'statusChange' | 'error' | 'ping' | 'sessionState'; + data: any; + timestamp?: string; +} + +// ============================================================================ +// API FUNCTIONS +// ============================================================================ + +/** + * Start a new Teams Bot session. + */ +export async function startSession(instanceId: string, request: StartSessionRequest): Promise<{ session: TeamsbotSession }> { + const response = await api.post(`/api/teamsbot/${instanceId}/sessions`, request); + return response.data; +} + +/** + * List all sessions for a feature instance. + */ +export async function listSessions(instanceId: string, includeEnded = true): Promise<{ sessions: TeamsbotSession[] }> { + const response = await api.get(`/api/teamsbot/${instanceId}/sessions`, { + params: { includeEnded }, + }); + return response.data; +} + +/** + * Get session details with transcripts and bot responses. + */ +export async function getSession( + instanceId: string, + sessionId: string, + includeTranscripts = true, + includeResponses = true, +): Promise<{ + session: TeamsbotSession; + transcripts?: TeamsbotTranscript[]; + botResponses?: TeamsbotBotResponse[]; + stats?: TeamsbotSessionStats; +}> { + const response = await api.get(`/api/teamsbot/${instanceId}/sessions/${sessionId}`, { + params: { includeTranscripts, includeResponses }, + }); + return response.data; +} + +/** + * Stop an active session. + */ +export async function stopSession(instanceId: string, sessionId: string): Promise<{ status: string; sessionId: string }> { + const response = await api.post(`/api/teamsbot/${instanceId}/sessions/${sessionId}/stop`); + return response.data; +} + +/** + * Delete a session and all related data. + */ +export async function deleteSession(instanceId: string, sessionId: string): Promise<{ deleted: boolean }> { + const response = await api.delete(`/api/teamsbot/${instanceId}/sessions/${sessionId}`); + return response.data; +} + +/** + * Get teamsbot configuration. + */ +export async function getConfig(instanceId: string): Promise<{ config: TeamsbotConfig }> { + const response = await api.get(`/api/teamsbot/${instanceId}/config`); + return response.data; +} + +/** + * Update teamsbot configuration. + */ +export async function updateConfig(instanceId: string, updates: ConfigUpdateRequest): Promise<{ config: TeamsbotConfig }> { + const response = await api.put(`/api/teamsbot/${instanceId}/config`, updates); + return response.data; +} + +/** + * Create an SSE EventSource for live session streaming. + * Returns the EventSource instance for the caller to manage. + */ +export function createSessionStream(instanceId: string, sessionId: string): EventSource { + const baseUrl = api.defaults.baseURL || ''; + const url = `${baseUrl}/api/teamsbot/${instanceId}/sessions/${sessionId}/stream`; + return new EventSource(url, { withCredentials: true }); +} diff --git a/src/config/pageRegistry.tsx b/src/config/pageRegistry.tsx index 190408e..0ee9aba 100644 --- a/src/config/pageRegistry.tsx +++ b/src/config/pageRegistry.tsx @@ -20,7 +20,8 @@ import { FaCubes, FaEnvelopeOpenText, FaKey, FaUsersCog, FaCube, FaShieldAlt, FaLightbulb, FaRegFileAlt, FaLink, FaComments, FaListAlt, FaCogs, FaChartLine, FaFileAlt, FaUserShield, FaDatabase, - FaProjectDiagram, FaMapMarkedAlt, FaWallet, FaMoneyBillAlt, FaClock + FaProjectDiagram, FaMapMarkedAlt, FaWallet, FaMoneyBillAlt, FaClock, + FaHeadset, FaVideo, } from 'react-icons/fa'; // ============================================================================= @@ -79,6 +80,11 @@ export const PAGE_ICONS: Record = { 'page.feature.realestate.projects': , 'page.feature.realestate.parcels': , + // Feature pages - Teams Bot + 'page.feature.teamsbot.dashboard': , + 'page.feature.teamsbot.sessions': , + 'page.feature.teamsbot.settings': , + // Feature icons (for feature grouping in navigation) 'feature.trustee': , 'feature.realestate': , @@ -86,6 +92,7 @@ export const PAGE_ICONS: Record = { 'feature.chatplayground': , 'feature.automation': , 'feature.chatbot': , + 'feature.teamsbot': , }; // ============================================================================= diff --git a/src/pages/FeatureView.tsx b/src/pages/FeatureView.tsx index 7314af7..7f131e7 100644 --- a/src/pages/FeatureView.tsx +++ b/src/pages/FeatureView.tsx @@ -31,6 +31,11 @@ import { PlaygroundPage, WorkflowsPage } from './workflows'; // Automation Views (reusing existing workflow pages) import { AutomationsPage, AutomationTemplatesPage } from './workflows'; +// Teamsbot Views +import { TeamsbotDashboardView } from './views/teamsbot/TeamsbotDashboardView'; +import { TeamsbotSessionView } from './views/teamsbot/TeamsbotSessionView'; +import { TeamsbotSettingsView } from './views/teamsbot/TeamsbotSettingsView'; + import styles from './FeatureView.module.css'; // ============================================================================= @@ -118,6 +123,11 @@ const VIEW_COMPONENTS: Record> = { templates: AutomationTemplatesPage, logs: () => , }, + teamsbot: { + dashboard: TeamsbotDashboardView, + sessions: TeamsbotSessionView, + settings: TeamsbotSettingsView, + }, }; // ============================================================================= diff --git a/src/pages/views/teamsbot/Teamsbot.module.css b/src/pages/views/teamsbot/Teamsbot.module.css new file mode 100644 index 0000000..deab067 --- /dev/null +++ b/src/pages/views/teamsbot/Teamsbot.module.css @@ -0,0 +1,497 @@ +/* ============================================================================ + Teamsbot Feature Styles + ============================================================================ */ + +/* Dashboard */ +.dashboardContainer { + display: flex; + flex-direction: column; + gap: 1.5rem; + padding: 1rem; + max-width: 960px; +} + +.startSessionCard { + background: var(--surface-color, #fff); + border: 1px solid var(--border-color, #e0e0e0); + border-radius: 8px; + padding: 1.5rem; +} + +.cardTitle { + margin: 0 0 0.5rem 0; + font-size: 1.1rem; + font-weight: 600; +} + +.cardDescription { + margin: 0 0 1rem 0; + color: var(--text-secondary, #666); + font-size: 0.9rem; +} + +.formGroup { + margin-bottom: 1rem; +} + +.formRow { + display: flex; + gap: 1rem; +} + +.formRow .formGroup { + flex: 1; +} + +.label { + display: block; + margin-bottom: 0.25rem; + font-size: 0.85rem; + font-weight: 500; + color: var(--text-primary, #333); +} + +.hint { + display: block; + margin-top: 0.25rem; + font-size: 0.75rem; + color: var(--text-tertiary, #999); +} + +.input, +.select, +.textarea { + width: 100%; + padding: 0.5rem 0.75rem; + border: 1px solid var(--border-color, #e0e0e0); + border-radius: 6px; + font-size: 0.9rem; + background: var(--input-bg, #fff); + color: var(--text-primary, #333); + box-sizing: border-box; +} + +.input:focus, +.select:focus, +.textarea:focus { + outline: none; + border-color: var(--primary-color, #4A90D9); + box-shadow: 0 0 0 2px rgba(74, 144, 217, 0.15); +} + +.textarea { + resize: vertical; + font-family: inherit; + min-height: 100px; +} + +.startButton, +.saveButton { + padding: 0.6rem 1.5rem; + background: var(--primary-color, #4A90D9); + color: #fff; + border: none; + border-radius: 6px; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: background 0.2s; +} + +.startButton:hover, +.saveButton:hover { + background: var(--primary-hover, #3A7BC8); +} + +.startButton:disabled, +.saveButton:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.stopButton { + padding: 0.4rem 1rem; + background: var(--danger-color, #D94A4A); + color: #fff; + border: none; + border-radius: 6px; + font-size: 0.85rem; + cursor: pointer; +} + +.stopButton:hover { + background: var(--danger-hover, #C83A3A); +} + +.viewButton { + padding: 0.4rem 1rem; + background: var(--surface-color, #f5f5f5); + color: var(--primary-color, #4A90D9); + border: 1px solid var(--border-color, #e0e0e0); + border-radius: 6px; + font-size: 0.85rem; + text-decoration: none; + cursor: pointer; +} + +.deleteButton { + padding: 0.4rem 1rem; + background: transparent; + color: var(--danger-color, #D94A4A); + border: 1px solid var(--danger-color, #D94A4A); + border-radius: 6px; + font-size: 0.85rem; + cursor: pointer; +} + +/* Error/Success Banners */ +.errorBanner { + background: rgba(217, 74, 74, 0.1); + color: var(--danger-color, #D94A4A); + padding: 0.75rem 1rem; + border-radius: 6px; + font-size: 0.9rem; + border-left: 3px solid var(--danger-color, #D94A4A); +} + +.successBanner { + background: rgba(74, 217, 154, 0.1); + color: var(--success-color, #2D8E5C); + padding: 0.75rem 1rem; + border-radius: 6px; + font-size: 0.9rem; + border-left: 3px solid var(--success-color, #2D8E5C); +} + +/* Section */ +.sectionContainer { + background: var(--surface-color, #fff); + border: 1px solid var(--border-color, #e0e0e0); + border-radius: 8px; + padding: 1.5rem; +} + +.sectionTitle { + margin: 0 0 1rem 0; + font-size: 1rem; + font-weight: 600; + color: var(--text-primary, #333); +} + +/* Session List */ +.sessionList { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.sessionCard { + border: 1px solid var(--border-color, #e0e0e0); + border-radius: 6px; + padding: 1rem; + background: var(--surface-alt, #fafafa); +} + +.sessionHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.sessionBotName { + font-weight: 600; + font-size: 0.95rem; +} + +.sessionMeta { + display: flex; + gap: 1rem; + font-size: 0.8rem; + color: var(--text-secondary, #666); + margin-bottom: 0.5rem; +} + +.sessionActions { + display: flex; + gap: 0.5rem; + margin-top: 0.75rem; +} + +.sessionSummary { + font-size: 0.85rem; + color: var(--text-secondary, #666); + background: var(--surface-color, #fff); + padding: 0.5rem; + border-radius: 4px; + margin-top: 0.5rem; +} + +.sessionError { + font-size: 0.85rem; + color: var(--danger-color, #D94A4A); + margin-top: 0.5rem; +} + +/* Status Badges */ +.statusBadge { + display: inline-block; + padding: 0.15rem 0.6rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 500; + text-transform: uppercase; +} + +.statusActive { + background: rgba(74, 217, 154, 0.15); + color: #2D8E5C; +} + +.statusJoining, +.statusPending { + background: rgba(74, 144, 217, 0.15); + color: #4A90D9; +} + +.statusEnded { + background: rgba(128, 128, 128, 0.15); + color: #666; +} + +.statusError { + background: rgba(217, 74, 74, 0.15); + color: #D94A4A; +} + +.statusLeaving { + background: rgba(217, 168, 74, 0.15); + color: #B8860B; +} + +.liveBadge { + display: inline-block; + padding: 0.15rem 0.5rem; + border-radius: 4px; + font-size: 0.7rem; + font-weight: 700; + color: #fff; + background: #D94A4A; + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.6; } +} + +.emptyState { + color: var(--text-tertiary, #999); + font-size: 0.9rem; + padding: 2rem; + text-align: center; +} + +.loading { + padding: 2rem; + text-align: center; + color: var(--text-secondary, #666); +} + +/* ============================================================================ + Session View + ============================================================================ */ + +.sessionContainer { + display: flex; + flex-direction: column; + gap: 1rem; + padding: 1rem; + height: 100%; +} + +.sessionViewHeader { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 1rem; + background: var(--surface-color, #fff); + border: 1px solid var(--border-color, #e0e0e0); + border-radius: 8px; +} + +.sessionInfo { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.sessionTitle { + margin: 0; + font-size: 1.1rem; +} + +.sessionControls { + display: flex; + gap: 0.5rem; +} + +.sessionContent { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + flex: 1; + min-height: 0; +} + +/* Transcript Panel */ +.transcriptPanel, +.responsesPanel { + background: var(--surface-color, #fff); + border: 1px solid var(--border-color, #e0e0e0); + border-radius: 8px; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.panelTitle { + margin: 0; + padding: 0.75rem 1rem; + font-size: 0.9rem; + font-weight: 600; + border-bottom: 1px solid var(--border-color, #e0e0e0); + background: var(--surface-alt, #fafafa); +} + +.transcriptList, +.responseList { + flex: 1; + overflow-y: auto; + padding: 0.5rem; +} + +.transcriptItem { + display: flex; + gap: 0.5rem; + padding: 0.35rem 0.5rem; + border-radius: 4px; + font-size: 0.85rem; + line-height: 1.4; +} + +.transcriptItem:hover { + background: var(--surface-alt, #f5f5f5); +} + +.transcriptTime { + color: var(--text-tertiary, #999); + font-size: 0.75rem; + flex-shrink: 0; + min-width: 55px; +} + +.transcriptSpeaker { + font-weight: 600; + flex-shrink: 0; +} + +.transcriptText { + color: var(--text-primary, #333); +} + +/* Response Items */ +.responseItem { + border: 1px solid var(--border-color, #e0e0e0); + border-radius: 6px; + padding: 0.75rem; + margin-bottom: 0.5rem; + background: var(--surface-alt, #fafafa); +} + +.responseHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.responseIntent { + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + color: var(--primary-color, #4A90D9); +} + +.responseTime { + font-size: 0.75rem; + color: var(--text-tertiary, #999); +} + +.responseText { + font-size: 0.9rem; + line-height: 1.5; + color: var(--text-primary, #333); +} + +.responseReasoning { + margin-top: 0.5rem; + font-size: 0.8rem; + color: var(--text-secondary, #666); +} + +.responseMeta { + display: flex; + gap: 1rem; + margin-top: 0.5rem; + font-size: 0.75rem; + color: var(--text-tertiary, #999); +} + +/* Summary Card */ +.summaryCard { + background: var(--surface-color, #fff); + border: 1px solid var(--border-color, #e0e0e0); + border-radius: 8px; + overflow: hidden; +} + +.summaryText { + padding: 1rem; + font-size: 0.9rem; + line-height: 1.6; + color: var(--text-primary, #333); + white-space: pre-wrap; +} + +/* ============================================================================ + Settings View + ============================================================================ */ + +.settingsContainer { + padding: 1rem; + max-width: 720px; +} + +.settingsCard { + background: var(--surface-color, #fff); + border: 1px solid var(--border-color, #e0e0e0); + border-radius: 8px; + padding: 1.5rem; +} + +.settingsSection { + margin-bottom: 2rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid var(--border-color, #e0e0e0); +} + +.settingsSection:last-of-type { + border-bottom: none; +} + +.settingsActions { + display: flex; + justify-content: flex-end; + padding-top: 1rem; +} diff --git a/src/pages/views/teamsbot/TeamsbotDashboardView.tsx b/src/pages/views/teamsbot/TeamsbotDashboardView.tsx new file mode 100644 index 0000000..5e8b869 --- /dev/null +++ b/src/pages/views/teamsbot/TeamsbotDashboardView.tsx @@ -0,0 +1,229 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { useCurrentInstance } from '../../../hooks/useCurrentInstance'; +import * as teamsbotApi from '../../../api/teamsbotApi'; +import type { TeamsbotSession, StartSessionRequest } from '../../../api/teamsbotApi'; +import styles from './Teamsbot.module.css'; + +/** + * TeamsbotDashboardView - Overview of all Teams Bot sessions. + * Allows starting new sessions and viewing active/past sessions. + */ +export const TeamsbotDashboardView: React.FC = () => { + const { instance } = useCurrentInstance(); + const instanceId = instance?.id || ''; + + const [sessions, setSessions] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // New session form + const [meetingLink, setMeetingLink] = useState(''); + const [botName, setBotName] = useState(''); + const [isStarting, setIsStarting] = useState(false); + + const _loadSessions = useCallback(async () => { + if (!instanceId) return; + try { + setLoading(true); + const result = await teamsbotApi.listSessions(instanceId); + setSessions(result.sessions || []); + setError(null); + } catch (err: any) { + setError(err.message || 'Fehler beim Laden der Sitzungen'); + } finally { + setLoading(false); + } + }, [instanceId]); + + useEffect(() => { + _loadSessions(); + }, [_loadSessions]); + + const _handleStartSession = async () => { + if (!meetingLink.trim()) return; + + setIsStarting(true); + setError(null); + + try { + const request: StartSessionRequest = { + meetingLink: meetingLink.trim(), + botName: botName.trim() || undefined, + }; + + await teamsbotApi.startSession(instanceId, request); + setMeetingLink(''); + setBotName(''); + await _loadSessions(); + } catch (err: any) { + setError(err.message || 'Fehler beim Starten der Sitzung'); + } finally { + setIsStarting(false); + } + }; + + const _handleStopSession = async (sessionId: string) => { + try { + await teamsbotApi.stopSession(instanceId, sessionId); + await _loadSessions(); + } catch (err: any) { + setError(err.message || 'Fehler beim Stoppen der Sitzung'); + } + }; + + const _handleDeleteSession = async (sessionId: string) => { + if (!window.confirm('Sitzung und alle zugehoerigen Daten loeschen?')) return; + try { + await teamsbotApi.deleteSession(instanceId, sessionId); + await _loadSessions(); + } catch (err: any) { + setError(err.message || 'Fehler beim Loeschen der Sitzung'); + } + }; + + const _getStatusBadgeClass = (status: string) => { + switch (status) { + case 'active': return styles.statusActive; + case 'joining': return styles.statusJoining; + case 'pending': return styles.statusPending; + case 'ended': return styles.statusEnded; + case 'error': return styles.statusError; + case 'leaving': return styles.statusLeaving; + default: return ''; + } + }; + + const _getStatusLabel = (status: string) => { + const labels: Record = { + pending: 'Wartend', + joining: 'Beitritt...', + active: 'Aktiv', + leaving: 'Verlassen...', + ended: 'Beendet', + error: 'Fehler', + }; + return labels[status] || status; + }; + + const activeSessions = sessions.filter(s => ['pending', 'joining', 'active'].includes(s.status)); + const pastSessions = sessions.filter(s => ['ended', 'error', 'leaving'].includes(s.status)); + + return ( +
+ {/* Start New Session Card */} +
+

Neue Bot-Sitzung starten

+

+ Fuege den Teams Meeting-Link ein, um den AI-Bot in ein Meeting einzuschleusen. +

+ +
+ + setMeetingLink(e.target.value)} + disabled={isStarting} + /> +
+ +
+ + setBotName(e.target.value)} + disabled={isStarting} + /> +
+ + +
+ + {error &&
{error}
} + + {/* Active Sessions */} + {activeSessions.length > 0 && ( +
+

Aktive Sitzungen

+
+ {activeSessions.map((session) => ( +
+
+ {session.botName} + + {_getStatusLabel(session.status)} + +
+
+ {session.transcriptSegmentCount} Segmente + {session.botResponseCount} Antworten + {session.startedAt && Seit: {new Date(session.startedAt).toLocaleTimeString('de-CH')}} +
+
+ Live ansehen + {session.status === 'active' && ( + + )} +
+
+ ))} +
+
+ )} + + {/* Past Sessions */} +
+

+ {loading ? 'Lade Sitzungen...' : `Vergangene Sitzungen (${pastSessions.length})`} +

+ {pastSessions.length === 0 && !loading && ( +

Noch keine vergangenen Sitzungen.

+ )} +
+ {pastSessions.map((session) => ( +
+
+ {session.botName} + + {_getStatusLabel(session.status)} + +
+
+ {session.transcriptSegmentCount} Segmente + {session.botResponseCount} Antworten + {session.startedAt && {new Date(session.startedAt).toLocaleDateString('de-CH')}} +
+ {session.summary && ( +
{session.summary.substring(0, 200)}...
+ )} + {session.errorMessage && ( +
{session.errorMessage}
+ )} +
+ Details + +
+
+ ))} +
+
+
+ ); +}; + +export default TeamsbotDashboardView; diff --git a/src/pages/views/teamsbot/TeamsbotSessionView.tsx b/src/pages/views/teamsbot/TeamsbotSessionView.tsx new file mode 100644 index 0000000..59fe63c --- /dev/null +++ b/src/pages/views/teamsbot/TeamsbotSessionView.tsx @@ -0,0 +1,225 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { useCurrentInstance } from '../../../hooks/useCurrentInstance'; +import * as teamsbotApi from '../../../api/teamsbotApi'; +import type { TeamsbotSession, TeamsbotTranscript, TeamsbotBotResponse, TeamsbotSSEEvent } from '../../../api/teamsbotApi'; +import styles from './Teamsbot.module.css'; + +/** + * TeamsbotSessionView - Live session view with real-time transcript and bot responses. + */ +export const TeamsbotSessionView: React.FC = () => { + const { instance } = useCurrentInstance(); + const instanceId = instance?.id || ''; + const [searchParams] = useSearchParams(); + const sessionId = searchParams.get('sessionId') || ''; + + const [session, setSession] = useState(null); + const [transcripts, setTranscripts] = useState([]); + const [botResponses, setBotResponses] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [isLive, setIsLive] = useState(false); + + const transcriptEndRef = useRef(null); + const eventSourceRef = useRef(null); + + // Load session data + const _loadSession = useCallback(async () => { + if (!instanceId || !sessionId) return; + try { + setLoading(true); + const result = await teamsbotApi.getSession(instanceId, sessionId); + setSession(result.session); + setTranscripts(result.transcripts || []); + setBotResponses(result.botResponses || []); + setError(null); + } catch (err: any) { + setError(err.message || 'Fehler beim Laden der Sitzung'); + } finally { + setLoading(false); + } + }, [instanceId, sessionId]); + + useEffect(() => { + _loadSession(); + }, [_loadSession]); + + // SSE Live Stream + useEffect(() => { + if (!instanceId || !sessionId || !session) return; + if (!['active', 'joining', 'pending'].includes(session.status)) return; + + const eventSource = teamsbotApi.createSessionStream(instanceId, sessionId); + eventSourceRef.current = eventSource; + setIsLive(true); + + eventSource.onmessage = (event) => { + try { + const sseEvent: TeamsbotSSEEvent = JSON.parse(event.data); + + switch (sseEvent.type) { + case 'transcript': + setTranscripts(prev => [...prev, sseEvent.data as TeamsbotTranscript]); + break; + + case 'botResponse': + setBotResponses(prev => [...prev, sseEvent.data as TeamsbotBotResponse]); + break; + + case 'statusChange': + setSession(prev => prev ? { ...prev, status: sseEvent.data.status } : null); + if (['ended', 'error'].includes(sseEvent.data.status)) { + setIsLive(false); + eventSource.close(); + } + break; + + case 'analysis': + // Debug info - could show in UI + break; + + case 'suggestedResponse': + // Manual mode: show suggested response + break; + + case 'ping': + break; + } + } catch (err) { + console.error('SSE parse error:', err); + } + }; + + eventSource.onerror = () => { + setIsLive(false); + }; + + return () => { + eventSource.close(); + eventSourceRef.current = null; + setIsLive(false); + }; + }, [instanceId, sessionId, session?.status]); + + // Auto-scroll transcript + useEffect(() => { + transcriptEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [transcripts]); + + const _handleStop = async () => { + if (!instanceId || !sessionId) return; + try { + await teamsbotApi.stopSession(instanceId, sessionId); + } catch (err: any) { + setError(err.message); + } + }; + + const _formatTime = (timestamp: string) => { + try { + return new Date(timestamp).toLocaleTimeString('de-CH', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); + } catch { + return ''; + } + }; + + const _getSpeakerColor = (speaker: string) => { + const colors = ['#4A90D9', '#D94A4A', '#4AD99A', '#D9A84A', '#9A4AD9', '#4AD9D9']; + let hash = 0; + for (let i = 0; i < speaker.length; i++) { + hash = speaker.charCodeAt(i) + ((hash << 5) - hash); + } + return colors[Math.abs(hash) % colors.length]; + }; + + if (loading) return
Lade Sitzung...
; + if (!session) return
Sitzung nicht gefunden
; + + return ( +
+ {/* Session Header */} +
+
+

{session.botName}

+ + {session.status} + + {isLive && LIVE} +
+
+ {['active', 'joining'].includes(session.status) && ( + + )} +
+
+ + {error &&
{error}
} + + {/* Main Content: Transcript + Responses */} +
+ {/* Left: Transcript */} +
+

Transkript ({transcripts.length} Segmente)

+
+ {transcripts.map((t) => ( +
+ {_formatTime(t.timestamp)} + + {t.speaker || 'Unknown'}: + + {t.text} +
+ ))} +
+ {transcripts.length === 0 && ( +
Noch kein Transkript vorhanden.
+ )} +
+
+ + {/* Right: Bot Responses */} +
+

Bot-Antworten ({botResponses.length})

+
+ {botResponses.map((r) => ( +
+
+ {r.detectedIntent} + {_formatTime(r.timestamp || '')} +
+
{r.responseText}
+ {r.reasoning && ( +
+ Reasoning: {r.reasoning} +
+ )} +
+ {r.modelName} + {r.processingTime.toFixed(1)}s + {r.priceCHF.toFixed(4)} CHF +
+
+ ))} + {botResponses.length === 0 && ( +
Noch keine Bot-Antworten.
+ )} +
+
+
+ + {/* Summary (for ended sessions) */} + {session.summary && ( +
+

Meeting-Zusammenfassung

+
{session.summary}
+
+ )} +
+ ); +}; + +export default TeamsbotSessionView; diff --git a/src/pages/views/teamsbot/TeamsbotSettingsView.tsx b/src/pages/views/teamsbot/TeamsbotSettingsView.tsx new file mode 100644 index 0000000..3f2f8fe --- /dev/null +++ b/src/pages/views/teamsbot/TeamsbotSettingsView.tsx @@ -0,0 +1,240 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { useCurrentInstance } from '../../../hooks/useCurrentInstance'; +import * as teamsbotApi from '../../../api/teamsbotApi'; +import type { TeamsbotConfig, ConfigUpdateRequest } from '../../../api/teamsbotApi'; +import styles from './Teamsbot.module.css'; + +/** + * TeamsbotSettingsView - Bot configuration for a feature instance. + */ +export const TeamsbotSettingsView: React.FC = () => { + const { instance } = useCurrentInstance(); + const instanceId = instance?.id || ''; + + const [config, setConfig] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + const [successMsg, setSuccessMsg] = useState(null); + + // Form state + const [formData, setFormData] = useState({}); + + const _loadConfig = useCallback(async () => { + if (!instanceId) return; + try { + setLoading(true); + const result = await teamsbotApi.getConfig(instanceId); + setConfig(result.config); + setFormData(result.config); + setError(null); + } catch (err: any) { + setError(err.message || 'Fehler beim Laden der Konfiguration'); + } finally { + setLoading(false); + } + }, [instanceId]); + + useEffect(() => { + _loadConfig(); + }, [_loadConfig]); + + const _handleSave = async () => { + if (!instanceId) return; + setSaving(true); + setError(null); + setSuccessMsg(null); + + try { + const result = await teamsbotApi.updateConfig(instanceId, formData); + setConfig(result.config); + setFormData(result.config); + setSuccessMsg('Konfiguration gespeichert'); + setTimeout(() => setSuccessMsg(null), 3000); + } catch (err: any) { + setError(err.message || 'Fehler beim Speichern'); + } finally { + setSaving(false); + } + }; + + const _updateField = (field: keyof ConfigUpdateRequest, value: any) => { + setFormData(prev => ({ ...prev, [field]: value })); + }; + + if (loading) return
Lade Konfiguration...
; + + return ( +
+
+

Bot-Einstellungen

+ + {error &&
{error}
} + {successMsg &&
{successMsg}
} + + {/* Bot Identity */} +
+

Bot-Identitaet

+ +
+ + _updateField('botName', e.target.value)} + placeholder="AI Assistant" + /> + Wird als Teilnehmer-Name im Meeting angezeigt +
+ +
+ + _updateField('backgroundImageUrl', e.target.value)} + placeholder="https://example.com/background.jpg" + /> + Hintergrundbild fuer den Video-Feed des Bots +
+
+ + {/* AI Behavior */} +
+

AI-Verhalten

+ +
+ +