import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { useCurrentInstance } from '../../../hooks/useCurrentInstance'; import * as teamsbotApi from '../../../api/teamsbotApi'; import type { TeamsbotSession, StartSessionRequest, TeamsbotJoinMode, UserAccountStatus, MfaChallengeEvent } from '../../../api/teamsbotApi'; import { getUserDataCache } from '../../../utils/userCache'; import styles from './Teamsbot.module.css'; import { useLanguage } from '../../../providers/language/LanguageContext'; /** * TeamsbotDashboardView - Overview of all Teams Bot sessions. * Allows starting new sessions and viewing active/past sessions. * Supports "Mein Account" login with saved credentials and MFA relay. */ export const TeamsbotDashboardView: React.FC = () => { const { t } = useLanguage(); const { instance, mandateId, featureCode } = useCurrentInstance(); const instanceId = instance?.id || ''; const navigate = useNavigate(); const cachedUser = getUserDataCache(); const _isSysAdmin = cachedUser?.isSysAdmin === true; 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 [joinMode, setJoinMode] = useState('anonymous'); const [sessionContext, setSessionContext] = useState(''); const [isStarting, setIsStarting] = useState(false); // User Account (Mein Account) state const [userAccount, setUserAccount] = useState(null); const [showCredentialForm, setShowCredentialForm] = useState(false); const [credEmail, setCredEmail] = useState(''); const [credPassword, setCredPassword] = useState(''); const [savingCredentials, setSavingCredentials] = useState(false); // MFA state const [mfaChallenge, setMfaChallenge] = useState(null); const [mfaCode, setMfaCode] = useState(''); const [mfaSessionId, setMfaSessionId] = useState(null); const [mfaWaitingPush, setMfaWaitingPush] = useState(false); const sseRef = useRef(null); 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 || t('Fehler beim Laden der Sitzungen')); } finally { setLoading(false); } }, [instanceId, t]); useEffect(() => { _loadSessions(); }, [_loadSessions]); // Load user account status when joinMode changes to userAccount useEffect(() => { if (joinMode === 'userAccount' && instanceId) { teamsbotApi.getUserAccount(instanceId).then(setUserAccount).catch(() => setUserAccount(null)); } }, [joinMode, instanceId]); // Adaptive polling: 3s with active sessions, 30s otherwise const pollRef = useRef | null>(null); useEffect(() => { const hasActiveSessions = sessions.some(s => ['pending', 'joining', 'active'].includes(s.status)); const interval = hasActiveSessions ? 3000 : 30000; if (instanceId) { pollRef.current = setInterval(() => { teamsbotApi.listSessions(instanceId).then(r => setSessions(r.sessions || [])).catch(() => {}); }, interval); } return () => { if (pollRef.current) clearInterval(pollRef.current); }; }, [sessions, instanceId]); // Cleanup SSE on unmount useEffect(() => { return () => { sseRef.current?.close(); }; }, []); const _startMfaListener = useCallback((sessionId: string) => { sseRef.current?.close(); const es = teamsbotApi.createSessionStream(instanceId, sessionId); sseRef.current = es; setMfaSessionId(sessionId); es.onmessage = (event) => { try { const parsed = JSON.parse(event.data); if (parsed.type === 'mfaChallenge') { const data = parsed.data as MfaChallengeEvent; if (data.mfaType === 'timeout') { setMfaChallenge(null); setMfaWaitingPush(false); setError(t('MFA-Zeitlimit überschritten, bitte erneut versuchen')); } else { setMfaChallenge(data); setMfaCode(''); setMfaWaitingPush(data.mfaType === 'pushApproval' || data.mfaType === 'numberMatch'); } } else if (parsed.type === 'mfaResolved') { setMfaChallenge(null); setMfaWaitingPush(false); setMfaSessionId(null); es.close(); sseRef.current = null; _loadSessions(); } } catch { /* ignore parse errors */ } }; }, [instanceId, _loadSessions, t]); const _handleStartSession = async () => { if (!meetingLink.trim()) return; // For userAccount: need credentials (saved or entered now) if (joinMode === 'userAccount' && !userAccount?.hasSavedCredentials && !credEmail) { setShowCredentialForm(true); return; } // userAccount with new/unsaved credentials: save to DB before starting const needsSave = joinMode === 'userAccount' && !userAccount?.hasSavedCredentials && credEmail && credPassword; const needsUpdate = joinMode === 'userAccount' && showCredentialForm && credEmail && credPassword; if (needsSave || needsUpdate) { try { setSavingCredentials(true); await teamsbotApi.saveUserAccount(instanceId, credEmail, credPassword); setUserAccount({ hasSavedCredentials: true, email: credEmail }); } catch (err: any) { setError(err.message || t('Fehler beim Speichern der Zugangsdaten')); setSavingCredentials(false); return; } finally { setSavingCredentials(false); } setShowCredentialForm(false); } setIsStarting(true); setError(null); try { const request: StartSessionRequest = { meetingLink: meetingLink.trim(), botName: botName.trim() || undefined, joinMode: joinMode, sessionContext: sessionContext.trim() || undefined, }; const result = await teamsbotApi.startSession(instanceId, request); const newSessionId = result.session?.id; setMeetingLink(''); setBotName(''); // Start SSE listener for MFA events if userAccount mode if (joinMode === 'userAccount' && newSessionId) { _startMfaListener(newSessionId); } await _loadSessions(); } catch (err: any) { setError(err.message || t('Fehler beim Starten der Sitzung')); } finally { setIsStarting(false); } }; const _handleSubmitMfaCode = async () => { if (!mfaSessionId || !instanceId) return; const needsCode = mfaChallenge?.mfaType === 'totpCode' || mfaChallenge?.mfaType === 'smsCode'; try { await teamsbotApi.submitMfaCode( instanceId, mfaSessionId, needsCode ? mfaCode : '', needsCode ? 'code' : 'confirmed', ); if (!needsCode) { setMfaWaitingPush(true); } } catch (err: any) { setError(err.message || t('Fehler beim Senden des MFA-Codes')); } }; const _handleDeleteUserAccount = async () => { try { await teamsbotApi.deleteUserAccount(instanceId); setUserAccount({ hasSavedCredentials: false }); setCredEmail(''); setCredPassword(''); } catch (err: any) { setError(err.message || t('Fehler beim Löschen der Zugangsdaten')); } }; const _handleStopSession = async (sessionId: string) => { try { await teamsbotApi.stopSession(instanceId, sessionId); await _loadSessions(); } catch (err: any) { setError(err.message || t('Fehler beim Stoppen der Sitzung')); } }; const _handleDeleteSession = async (sessionId: string) => { try { await teamsbotApi.deleteSession(instanceId, sessionId); await _loadSessions(); } catch (err: any) { setError(err.message || t('Fehler beim Löschen 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)); const _needsCodeInput = mfaChallenge?.mfaType === 'totpCode' || mfaChallenge?.mfaType === 'smsCode'; return (
{/* MFA Challenge Dialog */} {mfaChallenge && (
Multi-Faktor-Authentifizierung
{mfaChallenge.displayNumber && (
{mfaChallenge.displayNumber}
)}
{mfaChallenge.prompt}
{_needsCodeInput ? ( <> setMfaCode(e.target.value)} autoFocus onKeyDown={(e) => e.key === 'Enter' && _handleSubmitMfaCode()} /> ) : mfaWaitingPush ? ( <>

Warte auf Bestaetigung in der Authenticator App...

) : ( )}
)} {/* Start New Session Card */}

{t('Neue Botsitzung starten')}

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

setMeetingLink(e.target.value)} disabled={isStarting} />
{/* User Account: saved credentials info or credential form */} {joinMode === 'userAccount' && (
{userAccount?.hasSavedCredentials && !showCredentialForm ? (
Gespeichert: {userAccount.email}
) : ( <>
setCredEmail(e.target.value)} disabled={savingCredentials} />
setCredPassword(e.target.value)} disabled={savingCredentials} />
Zugangsdaten werden verschluesselt gespeichert. {userAccount?.hasSavedCredentials && ( )} )}
)}
setBotName(e.target.value)} disabled={isStarting} />