refactor: UI cleanup - remove test block, backgroundImage, botAccount; add transferMode

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ValueOn AG 2026-02-17 18:34:35 +01:00
parent 2e1f3a8733
commit 41a4e8ffe1
2 changed files with 25 additions and 414 deletions

View file

@ -10,7 +10,6 @@ export interface TeamsbotSession {
mandateId: string; mandateId: string;
meetingLink: string; meetingLink: string;
botName: string; botName: string;
backgroundImageUrl?: string;
status: 'pending' | 'joining' | 'active' | 'leaving' | 'ended' | 'error'; status: 'pending' | 'joining' | 'active' | 'leaving' | 'ended' | 'error';
startedAt?: string; startedAt?: string;
endedAt?: string; endedAt?: string;
@ -53,17 +52,17 @@ export interface TeamsbotBotResponse {
export type TeamsbotResponseChannel = 'voice' | 'chat' | 'both'; export type TeamsbotResponseChannel = 'voice' | 'chat' | 'both';
export type TeamsbotJoinMode = 'systemBot' | 'anonymous' | 'userAccount'; export type TeamsbotJoinMode = 'systemBot' | 'anonymous' | 'userAccount';
export type TeamsbotTransferMode = 'caption' | 'audio' | 'auto';
export interface TeamsbotConfig { export interface TeamsbotConfig {
botName: string; botName: string;
backgroundImageUrl?: string;
aiSystemPrompt: string; aiSystemPrompt: string;
responseMode: 'auto' | 'manual' | 'transcribeOnly'; responseMode: 'auto' | 'manual' | 'transcribeOnly';
responseChannel: TeamsbotResponseChannel; responseChannel: TeamsbotResponseChannel;
transferMode: TeamsbotTransferMode;
language: string; language: string;
voiceId?: string; voiceId?: string;
browserBotUrl?: string; browserBotUrl?: string;
botAccountEmail?: string;
botAccountPassword?: string;
triggerIntervalSeconds: number; triggerIntervalSeconds: number;
triggerCooldownSeconds: number; triggerCooldownSeconds: number;
contextWindowSegments: number; contextWindowSegments: number;
@ -80,7 +79,6 @@ export interface TeamsbotSessionStats {
export interface StartSessionRequest { export interface StartSessionRequest {
meetingLink: string; meetingLink: string;
botName?: string; botName?: string;
backgroundImageUrl?: string;
connectionId?: string; connectionId?: string;
joinMode?: TeamsbotJoinMode; joinMode?: TeamsbotJoinMode;
sessionContext?: string; sessionContext?: string;
@ -88,15 +86,13 @@ export interface StartSessionRequest {
export interface ConfigUpdateRequest { export interface ConfigUpdateRequest {
botName?: string; botName?: string;
backgroundImageUrl?: string;
aiSystemPrompt?: string; aiSystemPrompt?: string;
responseMode?: 'auto' | 'manual' | 'transcribeOnly'; responseMode?: 'auto' | 'manual' | 'transcribeOnly';
responseChannel?: TeamsbotResponseChannel; responseChannel?: TeamsbotResponseChannel;
transferMode?: TeamsbotTransferMode;
language?: string; language?: string;
voiceId?: string; voiceId?: string;
browserBotUrl?: string; browserBotUrl?: string;
botAccountEmail?: string;
botAccountPassword?: string;
triggerIntervalSeconds?: number; triggerIntervalSeconds?: number;
triggerCooldownSeconds?: number; triggerCooldownSeconds?: number;
contextWindowSegments?: number; contextWindowSegments?: number;

View file

@ -1,8 +1,8 @@
import React, { useState, useEffect, useCallback, useRef } from 'react'; import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useCurrentInstance } from '../../../hooks/useCurrentInstance'; import { useCurrentInstance } from '../../../hooks/useCurrentInstance';
import * as teamsbotApi from '../../../api/teamsbotApi'; import * as teamsbotApi from '../../../api/teamsbotApi';
import type { TeamsbotConfig, ConfigUpdateRequest, VoiceOption, AuthTestResults, AuthTestResult } from '../../../api/teamsbotApi'; import type { TeamsbotConfig, ConfigUpdateRequest, VoiceOption } from '../../../api/teamsbotApi';
import { FaPlay, FaSpinner, FaFlask, FaImage, FaChevronDown, FaChevronRight } from 'react-icons/fa'; import { FaPlay, FaSpinner } from 'react-icons/fa';
import styles from './Teamsbot.module.css'; import styles from './Teamsbot.module.css';
/** Format voice name for display: "de-DE-Wavenet-A" -> "Wavenet A" + gender */ /** Format voice name for display: "de-DE-Wavenet-A" -> "Wavenet A" + gender */
@ -37,16 +37,6 @@ export const TeamsbotSettingsView: React.FC = () => {
const [voices, setVoices] = useState<VoiceOption[]>([]); const [voices, setVoices] = useState<VoiceOption[]>([]);
const [loadingVoices, setLoadingVoices] = useState(false); const [loadingVoices, setLoadingVoices] = useState(false);
// Auth detection test state
const [testMeetingUrl, setTestMeetingUrl] = useState('');
const [testBotEmail, setTestBotEmail] = useState('');
const [testBotPassword, setTestBotPassword] = useState('');
const [testRunning, setTestRunning] = useState(false);
const [testRunningVariant, setTestRunningVariant] = useState<string | null>(null);
const [testResults, setTestResults] = useState<AuthTestResults | null>(null);
const [testError, setTestError] = useState<string | null>(null);
const [screenshotPreview, setScreenshotPreview] = useState<{ src: string; caption: string } | null>(null);
const [expandedLogs, setExpandedLogs] = useState<Set<string>>(new Set());
const _loadConfig = useCallback(async () => { const _loadConfig = useCallback(async () => {
if (!instanceId) return; if (!instanceId) return;
@ -153,157 +143,6 @@ export const TeamsbotSettingsView: React.FC = () => {
} }
}; };
const _handleRunAuthTest = async () => {
if (!instanceId || !testMeetingUrl.trim()) return;
setTestRunning(true);
setTestResults(null);
setTestError(null);
setTestRunningVariant(null);
try {
// Step 1: Get available variants
const variants = await teamsbotApi.getTestAuthVariants(instanceId);
// Initialize results shell so UI shows progress
const initialResults: AuthTestResults = {
meetingUrl: testMeetingUrl.trim(),
timestamp: new Date().toISOString(),
variants: variants.map(v => ({
variantId: v.id,
variantName: v.name,
success: false,
pageType: 'unknown' as const,
finalUrl: '',
hasSignInLink: false,
hasNameInput: false,
hasJoinButton: false,
authAttempted: false,
authSuccess: null,
durationMs: 0,
detectedSignals: [],
logs: ['Warte...'],
})),
recommendation: 'Test laeuft...',
};
setTestResults(initialResults);
// Step 2: Run each variant sequentially
const completedVariants: AuthTestResult[] = [];
for (const variantInfo of variants) {
setTestRunningVariant(variantInfo.name);
try {
const result = await teamsbotApi.testAuthSingleVariant(
instanceId,
variantInfo.id,
testMeetingUrl.trim(),
testBotEmail.trim() || undefined,
testBotPassword || undefined,
);
completedVariants.push(result);
} catch (err: any) {
completedVariants.push({
variantId: variantInfo.id,
variantName: variantInfo.name,
success: false,
pageType: 'error',
finalUrl: '',
hasSignInLink: false,
hasNameInput: false,
hasJoinButton: false,
authAttempted: false,
authSuccess: null,
durationMs: 0,
error: err.message || 'Fehler',
detectedSignals: [],
logs: [`[ERROR] ${err.message || 'Unbekannter Fehler'}`],
});
}
// Update UI with results so far
setTestResults(prev => prev ? {
...prev,
variants: [
...completedVariants,
...variants.slice(completedVariants.length).map(v => ({
variantId: v.id,
variantName: v.name,
success: false,
pageType: 'unknown' as const,
finalUrl: '',
hasSignInLink: false,
hasNameInput: false,
hasJoinButton: false,
authAttempted: false,
authSuccess: null,
durationMs: 0,
detectedSignals: [],
logs: ['Warte...'],
})),
],
} : prev);
}
// Final update with recommendation
setTestResults({
meetingUrl: testMeetingUrl.trim(),
timestamp: new Date().toISOString(),
variants: completedVariants,
recommendation: `Test abgeschlossen. ${completedVariants.filter(v => v.pageType === 'v2').length} von ${completedVariants.length} Varianten auf /v2/.`,
});
} catch (err: any) {
setTestError(err.message || 'Auth-Test fehlgeschlagen');
} finally {
setTestRunning(false);
setTestRunningVariant(null);
}
};
const _getPageTypeBadge = (result: AuthTestResult) => {
switch (result.pageType) {
case 'v2':
return <span className={styles.testBadgeV2}>/v2/</span>;
case 'lightMeetings':
return <span className={styles.testBadgeLightMeetings}>light-meetings</span>;
case 'error':
return <span className={styles.testBadgeError}>Fehler</span>;
default:
return <span className={styles.testBadgeUnknown}>Unbekannt</span>;
}
};
const _getCheckmark = (value: boolean) => {
return value
? <span className={styles.testCheckmark}>&#10003;</span>
: <span className={styles.testCross}>&#10007;</span>;
};
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 <div className={styles.loading}>Lade Konfiguration...</div>; if (loading) return <div className={styles.loading}>Lade Konfiguration...</div>;
return ( return (
@ -314,227 +153,6 @@ export const TeamsbotSettingsView: React.FC = () => {
{error && <div className={styles.errorBanner}>{error}</div>} {error && <div className={styles.errorBanner}>{error}</div>}
{successMsg && <div className={styles.successBanner}>{successMsg}</div>} {successMsg && <div className={styles.successBanner}>{successMsg}</div>}
{/* Auth Detection Test */}
<div className={styles.testSection}>
<div className={styles.testSectionHeader}>
<FaFlask />
<h4 className={styles.testSectionTitle}>Auth-Erkennung testen</h4>
</div>
<span className={styles.hint}>
Testet Chromium Minimal: MS-Login, Join a meeting, dann Join im Chat-Header.
Screenshots pro Schritt. Nur funktionierende Variante aktiv.
</span>
<div className={styles.testInputRow} style={{ marginTop: '0.75rem' }}>
<input
type="url"
className={styles.input}
value={testMeetingUrl}
onChange={(e) => setTestMeetingUrl(e.target.value)}
placeholder="https://teams.microsoft.com/meet/..."
disabled={testRunning}
/>
<button
className={styles.testButton}
onClick={_handleRunAuthTest}
disabled={testRunning || !testMeetingUrl.trim()}
>
{testRunning ? <FaSpinner className={styles.spinner} /> : <FaFlask />}
{testRunning ? 'Teste...' : 'Testen'}
</button>
</div>
<div className={styles.testInputRow} style={{ marginTop: '0.5rem', gap: '0.5rem' }}>
<input
type="email"
className={styles.input}
value={testBotEmail}
onChange={(e) => setTestBotEmail(e.target.value)}
placeholder="Bot-Email (z.B. nyla.larsson@poweron.swiss)"
disabled={testRunning}
style={{ flex: 1 }}
/>
<input
type="password"
className={styles.input}
value={testBotPassword}
onChange={(e) => setTestBotPassword(e.target.value)}
placeholder="Bot-Passwort"
disabled={testRunning}
style={{ flex: 1 }}
/>
</div>
<span className={styles.hint}>
Optional: Bot-Credentials fuer Auth-Test. Werden beim ersten Test automatisch in der DB gespeichert.
</span>
{testRunning && (
<div className={styles.testProgress}>
<FaSpinner className={styles.spinner} />
{testRunningVariant
? `Teste: ${testRunningVariant}...`
: 'Varianten werden geladen...'}
</div>
)}
{testError && <div className={styles.errorBanner}>{testError}</div>}
{testResults && (
<>
<table className={styles.testResultsTable}>
<thead>
<tr>
<th>Variante</th>
<th>Seite</th>
<th>Sign-in</th>
<th>Name-Input</th>
<th>Join</th>
<th>Auth</th>
<th>Dauer</th>
<th></th>
</tr>
</thead>
<tbody>
{testResults.variants.map((result) => (
<React.Fragment key={result.variantId}>
<tr>
<td>
<div className={styles.testVariantCell}>
{result.logs && result.logs.length > 0 && (
<button
className={styles.testLogToggle}
onClick={() => _toggleLogs(result.variantId)}
title="Logs anzeigen/ausblenden"
>
{expandedLogs.has(result.variantId) ? <FaChevronDown /> : <FaChevronRight />}
</button>
)}
<span className={styles.testVariantName}>{result.variantName}</span>
</div>
{result.error && (
<div className={styles.testErrorText} title={result.error}>
{result.error.length > 120 ? result.error.substring(0, 120) + '...' : result.error}
</div>
)}
</td>
<td>{_getPageTypeBadge(result)}</td>
<td>{result.success ? _getCheckmark(result.hasSignInLink) : <span className={styles.testDash}></span>}</td>
<td>{result.success ? _getCheckmark(result.hasNameInput) : <span className={styles.testDash}></span>}</td>
<td>{result.success ? _getCheckmark(result.hasJoinButton) : <span className={styles.testDash}></span>}</td>
<td>
{result.authAttempted
? (result.authSuccess ? <span className={styles.testCheckmark}>Erfolgreich</span> : <span className={styles.testCross}>Fehlgeschlagen</span>)
: <span className={styles.testDash}></span>
}
</td>
<td><span className={styles.testDuration}>{_formatDuration(result.durationMs)}</span></td>
<td>
<div className={styles.testScreenshotButtons}>
{result.screenshots && result.screenshots.length > 0
? result.screenshots.map((ss, idx) => (
<button
key={idx}
className={styles.testScreenshotButton}
onClick={() => setScreenshotPreview({
src: `data:image/jpeg;base64,${ss.data}`,
caption: `${result.variantName}${ss.label}`,
})}
title={ss.label}
>
<FaImage /> {ss.label}
</button>
))
: result.screenshot && (
<button
className={styles.testScreenshotButton}
onClick={() => setScreenshotPreview({
src: `data:image/jpeg;base64,${result.screenshot}`,
caption: result.variantName,
})}
>
<FaImage /> Bild
</button>
)
}
</div>
</td>
</tr>
{expandedLogs.has(result.variantId) && result.logs && result.logs.length > 0 && (
<tr className={styles.testLogRow}>
<td colSpan={8}>
<div className={styles.testLogContainer}>
{result.logs.map((log, idx) => (
<div key={idx} className={`${styles.testLogLine} ${_getLogLevelClass(log)}`}>
{log}
</div>
))}
</div>
</td>
</tr>
)}
</React.Fragment>
))}
</tbody>
</table>
{(testResults.credentialsReceived || testResults.credentialDebug) && (
<div className={styles.testLogContainer} style={{ margin: '0.5rem 0' }}>
<div className={`${styles.testLogLine} ${styles.testLogInfo}`}>
<strong>Credential-Debug:</strong>
</div>
{testResults.credentialsReceived && (
<div className={`${styles.testLogLine} ${testResults.credentialsReceived.hasEmail ? styles.testLogInfo : styles.testLogError}`}>
Bot empfangen: Email={testResults.credentialsReceived.hasEmail ? 'ja' : 'NEIN'} | Passwort={testResults.credentialsReceived.hasPassword ? 'ja' : 'NEIN'}
</div>
)}
{testResults.credentialDebug && (
<>
<div className={`${styles.testLogLine} ${styles.testLogInfo}`}>
Suche: mandateId={testResults.credentialDebug.mandateId} | Strategie={testResults.credentialDebug.searchStrategy}
</div>
<div className={`${styles.testLogLine} ${testResults.credentialDebug.botFound ? styles.testLogInfo : styles.testLogError}`}>
Bot gefunden: {testResults.credentialDebug.botFound ? 'ja' : 'NEIN'}
{testResults.credentialDebug.botEmail && ` | ${testResults.credentialDebug.botEmail}`}
{testResults.credentialDebug.botMandateId && ` | bot-mandate=${testResults.credentialDebug.botMandateId}`}
{testResults.credentialDebug.allBotsCount !== undefined && ` | total=${testResults.credentialDebug.allBotsCount}`}
</div>
{testResults.credentialDebug.passwordDecrypted !== undefined && (
<div className={`${styles.testLogLine} ${testResults.credentialDebug.passwordDecrypted ? styles.testLogInfo : styles.testLogError}`}>
Passwort entschluesselt: {testResults.credentialDebug.passwordDecrypted ? 'ja' : 'NEIN'}
{testResults.credentialDebug.passwordError && ` | Fehler: ${testResults.credentialDebug.passwordError}`}
</div>
)}
</>
)}
</div>
)}
{testResults.recommendation && (
<div className={styles.testRecommendation}>
<strong>Empfehlung:</strong> {testResults.recommendation}
</div>
)}
</>
)}
</div>
{/* Screenshot Overlay */}
{screenshotPreview && (
<div
className={styles.testScreenshotOverlay}
onClick={() => setScreenshotPreview(null)}
>
<img
src={screenshotPreview.src}
alt={screenshotPreview.caption}
className={styles.testScreenshotImage}
/>
<div className={styles.testScreenshotCaption}>
{screenshotPreview.caption} Klicken zum Schliessen
</div>
</div>
)}
{/* Bot Identity */} {/* Bot Identity */}
<div className={styles.settingsSection}> <div className={styles.settingsSection}>
<h4 className={styles.sectionTitle}>Bot-Identitaet</h4> <h4 className={styles.sectionTitle}>Bot-Identitaet</h4>
@ -548,19 +166,9 @@ export const TeamsbotSettingsView: React.FC = () => {
onChange={(e) => _updateField('botName', e.target.value)} onChange={(e) => _updateField('botName', e.target.value)}
placeholder="AI Assistant" placeholder="AI Assistant"
/> />
<span className={styles.hint}>Wird als Teilnehmer-Name im Meeting angezeigt</span> <span className={styles.hint}>
</div> Default-Name fuer den Bot im Meeting. Falls keiner angegeben, wird der Name des System-Bots verwendet (z.B. "Nyla Larsson").
</span>
<div className={styles.formGroup}>
<label className={styles.label}>Hintergrundbild-URL</label>
<input
type="url"
className={styles.input}
value={formData.backgroundImageUrl || ''}
onChange={(e) => _updateField('backgroundImageUrl', e.target.value)}
placeholder="https://example.com/background.jpg"
/>
<span className={styles.hint}>Hintergrundbild fuer den Video-Feed des Bots</span>
</div> </div>
</div> </div>
@ -606,6 +214,22 @@ export const TeamsbotSettingsView: React.FC = () => {
</select> </select>
<span className={styles.hint}>Wie soll der Bot im Meeting antworten?</span> <span className={styles.hint}>Wie soll der Bot im Meeting antworten?</span>
</div> </div>
<div className={styles.formGroup}>
<label className={styles.label}>Transfer-Modus</label>
<select
className={styles.select}
value={formData.transferMode || 'auto'}
onChange={(e) => _updateField('transferMode', e.target.value)}
>
<option value="auto">Automatisch - Anonym: Audio, Authentifiziert: Captions</option>
<option value="caption">Captions - Teams-Transkript (Text aus Live-Captions)</option>
<option value="audio">Audio - Audio-Stream an Gateway (STT in eingestellter Sprache)</option>
</select>
<span className={styles.hint}>
Bei anonymem Join liefert Teams nur englische Captions. Audio-Modus ermoeglicht STT in jeder Sprache ueber den Gateway.
</span>
</div>
</div> </div>
{/* Voice Settings */} {/* Voice Settings */}
@ -668,15 +292,6 @@ export const TeamsbotSettingsView: React.FC = () => {
</div> </div>
</div> </div>
{/* Bot Account - managed by admin, read-only display */}
<div className={styles.settingsSection}>
<h4 className={styles.sectionTitle}>Bot-Account (Microsoft)</h4>
<span className={styles.hint}>
System-Bot-Accounts werden vom Administrator verwaltet und sind nicht direkt editierbar.
Waehle beim Session-Start den gewuenschten Join-Modus.
</span>
</div>
{/* Advanced Settings */} {/* Advanced Settings */}
<div className={styles.settingsSection}> <div className={styles.settingsSection}>
<h4 className={styles.sectionTitle}>Erweiterte Einstellungen</h4> <h4 className={styles.sectionTitle}>Erweiterte Einstellungen</h4>