import React, { useState, useEffect, useRef, useCallback, useMemo } 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, ScreenshotInfo } from '../../../api/teamsbotApi'; import { getUserDataCache } from '../../../utils/userCache'; 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, setSearchParams] = useSearchParams(); const sessionId = searchParams.get('sessionId') || ''; const cachedUser = getUserDataCache(); const _isSysAdmin = cachedUser?.isSysAdmin === true; const [session, setSession] = useState(null); const [allSessions, setAllSessions] = useState([]); const [transcripts, setTranscripts] = useState([]); const [botResponses, setBotResponses] = useState([]); const [loading, setLoading] = useState(true); const [noSessions, setNoSessions] = useState(false); const [error, setError] = useState(null); const [isLive, setIsLive] = useState(false); const [screenshots, setScreenshots] = useState([]); const [screenshotsLoading, setScreenshotsLoading] = useState(false); const [screenshotsLoaded, setScreenshotsLoaded] = useState(false); const transcriptEndRef = useRef(null); const eventSourceRef = useRef(null); // Load session data - if no sessionId given, load the most recent session const _loadSession = useCallback(async () => { if (!instanceId) return; try { setLoading(true); setNoSessions(false); // Always load the full session list for the switcher const listResult = await teamsbotApi.listSessions(instanceId, true); const sessions = listResult.sessions || []; setAllSessions(sessions); let targetSessionId = sessionId; // No sessionId in URL -> find the most recent active or latest session if (!targetSessionId) { if (sessions.length === 0) { setNoSessions(true); setLoading(false); return; } // Prefer active sessions, then most recent const activeSession = sessions.find(s => ['active', 'joining', 'pending'].includes(s.status)); targetSessionId = activeSession ? activeSession.id : sessions[0].id; setSearchParams({ sessionId: targetSessionId }, { replace: true }); } const result = await teamsbotApi.getSession(instanceId, targetSessionId); 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, setSearchParams]); 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]); // Polling fallback: refresh session data every 5s when session is active (in case SSE fails) const pollRef = useRef | null>(null); const isActive = useMemo(() => session && ['pending', 'joining', 'active'].includes(session.status), [session]); useEffect(() => { if (isActive && instanceId && sessionId && !isLive) { pollRef.current = setInterval(async () => { try { const result = await teamsbotApi.getSession(instanceId, sessionId); setSession(result.session); if (result.transcripts) setTranscripts(result.transcripts); if (result.botResponses) setBotResponses(result.botResponses); } catch {} }, 5000); } return () => { if (pollRef.current) clearInterval(pollRef.current); }; }, [isActive, instanceId, sessionId, isLive]); // 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 (noSessions) return (

Keine Sitzungen vorhanden.

Starte eine neue Sitzung im Dashboard.

); if (!session) return
Sitzung nicht gefunden
; const _switchSession = (newSessionId: string) => { setSearchParams({ sessionId: newSessionId }); }; return (
{/* Session Switcher (if multiple sessions exist) */} {allSessions.length > 1 && (
{allSessions.map((s) => ( ))}
)} {/* 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}
)} {/* Debug Screenshots (SysAdmin only) */} {_isSysAdmin && (

Debug Screenshots

{screenshotsLoaded && screenshots.length === 0 && (
Keine Screenshots fuer diese Session.
)} {screenshots.length > 0 && (
{screenshots.map((s) => { const imgUrl = teamsbotApi.getScreenshotUrl(instanceId, s.name); return ( {s.step}
{s.step}
{new Date(s.timestamp).toLocaleTimeString('de-CH')} — {(s.sizeBytes / 1024).toFixed(0)} KB
); })}
)}
)}
); }; export default TeamsbotSessionView;