import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useCurrentInstance } from '../../../hooks/useCurrentInstance'; import * as teamsbotApi from '../../../api/teamsbotApi'; import type { TeamsbotConfig, ConfigUpdateRequest, VoiceOption, AuthTestResults, AuthTestResult } from '../../../api/teamsbotApi'; import { FaPlay, FaSpinner, FaFlask, FaImage, FaChevronDown, FaChevronRight } from 'react-icons/fa'; import styles from './Teamsbot.module.css'; /** Format voice name for display: "de-DE-Wavenet-A" -> "Wavenet A" + gender */ function _formatVoiceName(voice: VoiceOption): string { const parts = voice.name.split('-'); const type = parts[2] || ''; // Wavenet, Neural2, Standard, etc. const variant = parts[3] || ''; const gender = voice.ssmlGender === 'FEMALE' ? 'Frau' : voice.ssmlGender === 'MALE' ? 'Mann' : ''; return `${gender} ${variant} (${type})`.trim(); } /** * 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 [testingVoice, setTestingVoice] = useState(false); const [error, setError] = useState(null); const [successMsg, setSuccessMsg] = useState(null); const audioRef = useRef(null); // Form state const [formData, setFormData] = useState({}); // Dynamic voice data from Google TTS API const [languages, setLanguages] = useState([]); const [voices, setVoices] = useState([]); const [loadingVoices, setLoadingVoices] = useState(false); // Auth detection test state const [testMeetingUrl, setTestMeetingUrl] = useState(''); const [testRunning, setTestRunning] = useState(false); const [testResults, setTestResults] = useState(null); const [testError, setTestError] = useState(null); const [screenshotPreview, setScreenshotPreview] = useState<{ src: string; caption: string } | null>(null); const [expandedLogs, setExpandedLogs] = useState>(new Set()); const _loadConfig = useCallback(async () => { if (!instanceId) return; try { setLoading(true); // Load per-user settings (merged with instance defaults) const [settingsResult, languagesResult] = await Promise.all([ teamsbotApi.getUserSettings(instanceId), teamsbotApi.fetchLanguages(), ]); const effectiveConfig = settingsResult.effectiveConfig; setConfig(effectiveConfig); setFormData(effectiveConfig); setLanguages(languagesResult); // Load voices for the current language const lang = effectiveConfig.language || 'de-DE'; const voicesResult = await teamsbotApi.fetchVoices(lang); setVoices(voicesResult); 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 { // Save as per-user settings (not instance config) const result = await teamsbotApi.updateUserSettings(instanceId, formData); setConfig(result.effectiveConfig); setFormData(result.effectiveConfig); setSuccessMsg('Einstellungen 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 })); }; const _handleLanguageChange = async (language: string) => { _updateField('language', language); // Load voices for the new language dynamically setLoadingVoices(true); try { const voicesResult = await teamsbotApi.fetchVoices(language); setVoices(voicesResult); // Auto-select first voice if current voice doesn't match if (voicesResult.length > 0 && !voicesResult.find(v => v.name === formData.voiceId)) { _updateField('voiceId', voicesResult[0].name); } } catch { setVoices([]); } finally { setLoadingVoices(false); } }; const _handleTestVoice = async () => { if (!instanceId) return; setTestingVoice(true); try { const language = formData.language || 'de-DE'; const botName = formData.botName || 'AI Assistant'; const result = await teamsbotApi.testVoice(instanceId, botName, language, formData.voiceId); if (result.success && result.audio) { // Play audio from base64 const audioBlob = new Blob( [Uint8Array.from(atob(result.audio), c => c.charCodeAt(0))], { type: 'audio/mp3' } ); const audioUrl = URL.createObjectURL(audioBlob); if (audioRef.current) { audioRef.current.pause(); } const audio = new Audio(audioUrl); audioRef.current = audio; audio.play(); audio.onended = () => URL.revokeObjectURL(audioUrl); } else { setError(result.error || 'Stimmtest fehlgeschlagen'); setTimeout(() => setError(null), 3000); } } catch (err: any) { setError(err.message || 'Stimmtest fehlgeschlagen'); setTimeout(() => setError(null), 3000); } finally { setTestingVoice(false); } }; const _handleRunAuthTest = async () => { if (!instanceId || !testMeetingUrl.trim()) return; setTestRunning(true); setTestResults(null); setTestError(null); try { const results = await teamsbotApi.testAuth(instanceId, testMeetingUrl.trim()); setTestResults(results); } catch (err: any) { setTestError(err.message || 'Auth-Test fehlgeschlagen'); } finally { setTestRunning(false); } }; const _getPageTypeBadge = (result: AuthTestResult) => { switch (result.pageType) { case 'v2': return /v2/; case 'lightMeetings': return light-meetings; case 'error': return Fehler; default: return Unbekannt; } }; const _getCheckmark = (value: boolean) => { return value ? : ; }; const _formatDuration = (ms: number) => { if (ms < 1000) return `${ms}ms`; return `${(ms / 1000).toFixed(1)}s`; }; const _toggleLogs = (variantId: string) => { setExpandedLogs(prev => { const next = new Set(prev); if (next.has(variantId)) { next.delete(variantId); } else { next.add(variantId); } return next; }); }; const _getLogLevelClass = (log: string): string => { if (log.startsWith('[ERROR]') || log.startsWith('[CONSOLE:error]') || log.startsWith('[PAGE_ERROR]')) { return styles.testLogError; } if (log.startsWith('[WARN]') || log.startsWith('[CONSOLE:warning]')) { return styles.testLogWarn; } return styles.testLogInfo; }; if (loading) return
Lade Konfiguration...
; return (

Bot-Einstellungen

{error &&
{error}
} {successMsg &&
{successMsg}
} {/* Auth Detection Test */}

Auth-Erkennung testen

Testet Direct-/v2/-Navigation gegen einen aktiven Teams-Call. Prueft ob Teams /v2/ (Auth moeglich) oder light-meetings (anonym erzwungen) liefert. Der Bot tritt dem Meeting NICHT bei.
setTestMeetingUrl(e.target.value)} placeholder="https://teams.microsoft.com/meet/..." disabled={testRunning} />
{testRunning && (
Test laeuft (~30-45 Sekunden)...
)} {testError &&
{testError}
} {testResults && ( <> {testResults.variants.map((result) => ( {expandedLogs.has(result.variantId) && result.logs && result.logs.length > 0 && ( )} ))}
Variante Seite Sign-in Name-Input Join Auth Dauer
{result.logs && result.logs.length > 0 && ( )} {result.variantName}
{result.error && (
{result.error.length > 120 ? result.error.substring(0, 120) + '...' : result.error}
)}
{_getPageTypeBadge(result)} {result.success ? _getCheckmark(result.hasSignInLink) : } {result.success ? _getCheckmark(result.hasNameInput) : } {result.success ? _getCheckmark(result.hasJoinButton) : } {result.authAttempted ? (result.authSuccess ? Erfolgreich : Fehlgeschlagen) : } {_formatDuration(result.durationMs)} {result.screenshot && ( )}
{result.logs.map((log, idx) => (
{log}
))}
{testResults.recommendation && (
Empfehlung: {testResults.recommendation}
)} )}
{/* Screenshot Overlay */} {screenshotPreview && (
setScreenshotPreview(null)} > {screenshotPreview.caption}
{screenshotPreview.caption} — Klicken zum Schliessen
)} {/* 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