ui-nyla/src/pages/views/teamsbot/TeamsbotSessionView.tsx
2026-02-13 00:00:26 +01:00

225 lines
7.9 KiB
TypeScript

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<TeamsbotSession | null>(null);
const [transcripts, setTranscripts] = useState<TeamsbotTranscript[]>([]);
const [botResponses, setBotResponses] = useState<TeamsbotBotResponse[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isLive, setIsLive] = useState(false);
const transcriptEndRef = useRef<HTMLDivElement>(null);
const eventSourceRef = useRef<EventSource | null>(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 <div className={styles.loading}>Lade Sitzung...</div>;
if (!session) return <div className={styles.errorBanner}>Sitzung nicht gefunden</div>;
return (
<div className={styles.sessionContainer}>
{/* Session Header */}
<div className={styles.sessionViewHeader}>
<div className={styles.sessionInfo}>
<h3 className={styles.sessionTitle}>{session.botName}</h3>
<span className={`${styles.statusBadge} ${styles[`status${session.status.charAt(0).toUpperCase() + session.status.slice(1)}`] || ''}`}>
{session.status}
</span>
{isLive && <span className={styles.liveBadge}>LIVE</span>}
</div>
<div className={styles.sessionControls}>
{['active', 'joining'].includes(session.status) && (
<button className={styles.stopButton} onClick={_handleStop}>Sitzung beenden</button>
)}
</div>
</div>
{error && <div className={styles.errorBanner}>{error}</div>}
{/* Main Content: Transcript + Responses */}
<div className={styles.sessionContent}>
{/* Left: Transcript */}
<div className={styles.transcriptPanel}>
<h4 className={styles.panelTitle}>Transkript ({transcripts.length} Segmente)</h4>
<div className={styles.transcriptList}>
{transcripts.map((t) => (
<div key={t.id} className={styles.transcriptItem}>
<span className={styles.transcriptTime}>{_formatTime(t.timestamp)}</span>
<span
className={styles.transcriptSpeaker}
style={{ color: _getSpeakerColor(t.speaker || 'Unknown') }}
>
{t.speaker || 'Unknown'}:
</span>
<span className={styles.transcriptText}>{t.text}</span>
</div>
))}
<div ref={transcriptEndRef} />
{transcripts.length === 0 && (
<div className={styles.emptyState}>Noch kein Transkript vorhanden.</div>
)}
</div>
</div>
{/* Right: Bot Responses */}
<div className={styles.responsesPanel}>
<h4 className={styles.panelTitle}>Bot-Antworten ({botResponses.length})</h4>
<div className={styles.responseList}>
{botResponses.map((r) => (
<div key={r.id} className={styles.responseItem}>
<div className={styles.responseHeader}>
<span className={styles.responseIntent}>{r.detectedIntent}</span>
<span className={styles.responseTime}>{_formatTime(r.timestamp || '')}</span>
</div>
<div className={styles.responseText}>{r.responseText}</div>
{r.reasoning && (
<div className={styles.responseReasoning}>
<em>Reasoning: {r.reasoning}</em>
</div>
)}
<div className={styles.responseMeta}>
<span>{r.modelName}</span>
<span>{r.processingTime.toFixed(1)}s</span>
<span>{r.priceCHF.toFixed(4)} CHF</span>
</div>
</div>
))}
{botResponses.length === 0 && (
<div className={styles.emptyState}>Noch keine Bot-Antworten.</div>
)}
</div>
</div>
</div>
{/* Summary (for ended sessions) */}
{session.summary && (
<div className={styles.summaryCard}>
<h4 className={styles.panelTitle}>Meeting-Zusammenfassung</h4>
<div className={styles.summaryText}>{session.summary}</div>
</div>
)}
</div>
);
};
export default TeamsbotSessionView;