refactor: UI cleanup - remove test block, backgroundImage, botAccount; add transferMode
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2e1f3a8733
commit
41a4e8ffe1
2 changed files with 25 additions and 414 deletions
|
|
@ -10,7 +10,6 @@ export interface TeamsbotSession {
|
|||
mandateId: string;
|
||||
meetingLink: string;
|
||||
botName: string;
|
||||
backgroundImageUrl?: string;
|
||||
status: 'pending' | 'joining' | 'active' | 'leaving' | 'ended' | 'error';
|
||||
startedAt?: string;
|
||||
endedAt?: string;
|
||||
|
|
@ -53,17 +52,17 @@ export interface TeamsbotBotResponse {
|
|||
export type TeamsbotResponseChannel = 'voice' | 'chat' | 'both';
|
||||
export type TeamsbotJoinMode = 'systemBot' | 'anonymous' | 'userAccount';
|
||||
|
||||
export type TeamsbotTransferMode = 'caption' | 'audio' | 'auto';
|
||||
|
||||
export interface TeamsbotConfig {
|
||||
botName: string;
|
||||
backgroundImageUrl?: string;
|
||||
aiSystemPrompt: string;
|
||||
responseMode: 'auto' | 'manual' | 'transcribeOnly';
|
||||
responseChannel: TeamsbotResponseChannel;
|
||||
transferMode: TeamsbotTransferMode;
|
||||
language: string;
|
||||
voiceId?: string;
|
||||
browserBotUrl?: string;
|
||||
botAccountEmail?: string;
|
||||
botAccountPassword?: string;
|
||||
triggerIntervalSeconds: number;
|
||||
triggerCooldownSeconds: number;
|
||||
contextWindowSegments: number;
|
||||
|
|
@ -80,7 +79,6 @@ export interface TeamsbotSessionStats {
|
|||
export interface StartSessionRequest {
|
||||
meetingLink: string;
|
||||
botName?: string;
|
||||
backgroundImageUrl?: string;
|
||||
connectionId?: string;
|
||||
joinMode?: TeamsbotJoinMode;
|
||||
sessionContext?: string;
|
||||
|
|
@ -88,15 +86,13 @@ export interface StartSessionRequest {
|
|||
|
||||
export interface ConfigUpdateRequest {
|
||||
botName?: string;
|
||||
backgroundImageUrl?: string;
|
||||
aiSystemPrompt?: string;
|
||||
responseMode?: 'auto' | 'manual' | 'transcribeOnly';
|
||||
responseChannel?: TeamsbotResponseChannel;
|
||||
transferMode?: TeamsbotTransferMode;
|
||||
language?: string;
|
||||
voiceId?: string;
|
||||
browserBotUrl?: string;
|
||||
botAccountEmail?: string;
|
||||
botAccountPassword?: string;
|
||||
triggerIntervalSeconds?: number;
|
||||
triggerCooldownSeconds?: number;
|
||||
contextWindowSegments?: number;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
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 type { TeamsbotConfig, ConfigUpdateRequest, VoiceOption } from '../../../api/teamsbotApi';
|
||||
import { FaPlay, FaSpinner } from 'react-icons/fa';
|
||||
import styles from './Teamsbot.module.css';
|
||||
|
||||
/** 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 [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 () => {
|
||||
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}>✓</span>
|
||||
: <span className={styles.testCross}>✗</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>;
|
||||
|
||||
return (
|
||||
|
|
@ -314,227 +153,6 @@ export const TeamsbotSettingsView: React.FC = () => {
|
|||
{error && <div className={styles.errorBanner}>{error}</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 */}
|
||||
<div className={styles.settingsSection}>
|
||||
<h4 className={styles.sectionTitle}>Bot-Identitaet</h4>
|
||||
|
|
@ -548,19 +166,9 @@ export const TeamsbotSettingsView: React.FC = () => {
|
|||
onChange={(e) => _updateField('botName', e.target.value)}
|
||||
placeholder="AI Assistant"
|
||||
/>
|
||||
<span className={styles.hint}>Wird als Teilnehmer-Name im Meeting angezeigt</span>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<span className={styles.hint}>
|
||||
Default-Name fuer den Bot im Meeting. Falls keiner angegeben, wird der Name des System-Bots verwendet (z.B. "Nyla Larsson").
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -606,6 +214,22 @@ export const TeamsbotSettingsView: React.FC = () => {
|
|||
</select>
|
||||
<span className={styles.hint}>Wie soll der Bot im Meeting antworten?</span>
|
||||
</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>
|
||||
|
||||
{/* Voice Settings */}
|
||||
|
|
@ -668,15 +292,6 @@ export const TeamsbotSettingsView: React.FC = () => {
|
|||
</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 */}
|
||||
<div className={styles.settingsSection}>
|
||||
<h4 className={styles.sectionTitle}>Erweiterte Einstellungen</h4>
|
||||
|
|
|
|||
Loading…
Reference in a new issue