240 lines
8.8 KiB
TypeScript
240 lines
8.8 KiB
TypeScript
import React, { useState, useEffect, useCallback } from 'react';
|
|
import { useCurrentInstance } from '../../../hooks/useCurrentInstance';
|
|
import * as teamsbotApi from '../../../api/teamsbotApi';
|
|
import type { TeamsbotConfig, ConfigUpdateRequest } from '../../../api/teamsbotApi';
|
|
import styles from './Teamsbot.module.css';
|
|
|
|
/**
|
|
* TeamsbotSettingsView - Bot configuration for a feature instance.
|
|
*/
|
|
export const TeamsbotSettingsView: React.FC = () => {
|
|
const { instance } = useCurrentInstance();
|
|
const instanceId = instance?.id || '';
|
|
|
|
const [, setConfig] = useState<TeamsbotConfig | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [successMsg, setSuccessMsg] = useState<string | null>(null);
|
|
|
|
// Form state
|
|
const [formData, setFormData] = useState<ConfigUpdateRequest>({});
|
|
|
|
const _loadConfig = useCallback(async () => {
|
|
if (!instanceId) return;
|
|
try {
|
|
setLoading(true);
|
|
const result = await teamsbotApi.getConfig(instanceId);
|
|
setConfig(result.config);
|
|
setFormData(result.config);
|
|
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 {
|
|
const result = await teamsbotApi.updateConfig(instanceId, formData);
|
|
setConfig(result.config);
|
|
setFormData(result.config);
|
|
setSuccessMsg('Konfiguration 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 }));
|
|
};
|
|
|
|
if (loading) return <div className={styles.loading}>Lade Konfiguration...</div>;
|
|
|
|
return (
|
|
<div className={styles.settingsContainer}>
|
|
<div className={styles.settingsCard}>
|
|
<h3 className={styles.cardTitle}>Bot-Einstellungen</h3>
|
|
|
|
{error && <div className={styles.errorBanner}>{error}</div>}
|
|
{successMsg && <div className={styles.successBanner}>{successMsg}</div>}
|
|
|
|
{/* Bot Identity */}
|
|
<div className={styles.settingsSection}>
|
|
<h4 className={styles.sectionTitle}>Bot-Identitaet</h4>
|
|
|
|
<div className={styles.formGroup}>
|
|
<label className={styles.label}>Bot-Name</label>
|
|
<input
|
|
type="text"
|
|
className={styles.input}
|
|
value={formData.botName || ''}
|
|
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>
|
|
</div>
|
|
</div>
|
|
|
|
{/* AI Behavior */}
|
|
<div className={styles.settingsSection}>
|
|
<h4 className={styles.sectionTitle}>AI-Verhalten</h4>
|
|
|
|
<div className={styles.formGroup}>
|
|
<label className={styles.label}>System-Prompt</label>
|
|
<textarea
|
|
className={styles.textarea}
|
|
value={formData.aiSystemPrompt || ''}
|
|
onChange={(e) => _updateField('aiSystemPrompt', e.target.value)}
|
|
rows={6}
|
|
placeholder="Beschreibe, wie sich der Bot im Meeting verhalten soll..."
|
|
/>
|
|
<span className={styles.hint}>Instruktionen fuer den AI-Bot: Wann und wie soll er antworten?</span>
|
|
</div>
|
|
|
|
<div className={styles.formGroup}>
|
|
<label className={styles.label}>Antwort-Modus</label>
|
|
<select
|
|
className={styles.select}
|
|
value={formData.responseMode || 'auto'}
|
|
onChange={(e) => _updateField('responseMode', e.target.value)}
|
|
>
|
|
<option value="auto">Automatisch - AI entscheidet selbst</option>
|
|
<option value="manual">Manuell - Antworten muessen bestaetigt werden</option>
|
|
<option value="transcribeOnly">Nur Transkription - Keine AI-Antworten</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Voice Settings */}
|
|
<div className={styles.settingsSection}>
|
|
<h4 className={styles.sectionTitle}>Sprach-Einstellungen</h4>
|
|
|
|
<div className={styles.formGroup}>
|
|
<label className={styles.label}>Sprache</label>
|
|
<select
|
|
className={styles.select}
|
|
value={formData.language || 'de-DE'}
|
|
onChange={(e) => _updateField('language', e.target.value)}
|
|
>
|
|
<option value="de-DE">Deutsch (Deutschland)</option>
|
|
<option value="de-CH">Deutsch (Schweiz)</option>
|
|
<option value="en-US">English (US)</option>
|
|
<option value="en-GB">English (UK)</option>
|
|
<option value="fr-FR">Francais</option>
|
|
<option value="it-IT">Italiano</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className={styles.formGroup}>
|
|
<label className={styles.label}>Stimme (Voice ID)</label>
|
|
<input
|
|
type="text"
|
|
className={styles.input}
|
|
value={formData.voiceId || ''}
|
|
onChange={(e) => _updateField('voiceId', e.target.value)}
|
|
placeholder="de-DE-Standard-A"
|
|
/>
|
|
<span className={styles.hint}>Google TTS Voice ID fuer die Sprachausgabe</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Advanced Settings */}
|
|
<div className={styles.settingsSection}>
|
|
<h4 className={styles.sectionTitle}>Erweiterte Einstellungen</h4>
|
|
|
|
<div className={styles.formRow}>
|
|
<div className={styles.formGroup}>
|
|
<label className={styles.label}>Analyse-Intervall (Sek.)</label>
|
|
<input
|
|
type="number"
|
|
className={styles.input}
|
|
value={formData.triggerIntervalSeconds || 10}
|
|
onChange={(e) => _updateField('triggerIntervalSeconds', parseInt(e.target.value))}
|
|
min={3}
|
|
max={60}
|
|
/>
|
|
<span className={styles.hint}>Periodisches Analyse-Intervall</span>
|
|
</div>
|
|
|
|
<div className={styles.formGroup}>
|
|
<label className={styles.label}>Cooldown (Sek.)</label>
|
|
<input
|
|
type="number"
|
|
className={styles.input}
|
|
value={formData.triggerCooldownSeconds || 3}
|
|
onChange={(e) => _updateField('triggerCooldownSeconds', parseInt(e.target.value))}
|
|
min={1}
|
|
max={30}
|
|
/>
|
|
<span className={styles.hint}>Min. Abstand zwischen AI-Calls</span>
|
|
</div>
|
|
|
|
<div className={styles.formGroup}>
|
|
<label className={styles.label}>Kontext-Fenster (Segmente)</label>
|
|
<input
|
|
type="number"
|
|
className={styles.input}
|
|
value={formData.contextWindowSegments || 20}
|
|
onChange={(e) => _updateField('contextWindowSegments', parseInt(e.target.value))}
|
|
min={5}
|
|
max={100}
|
|
/>
|
|
<span className={styles.hint}>Anzahl Transkript-Segmente fuer AI-Kontext</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className={styles.formGroup}>
|
|
<label className={styles.label}>Media Bridge URL</label>
|
|
<input
|
|
type="url"
|
|
className={styles.input}
|
|
value={formData.bridgeUrl || ''}
|
|
onChange={(e) => _updateField('bridgeUrl', e.target.value)}
|
|
placeholder="https://bridge.example.com"
|
|
/>
|
|
<span className={styles.hint}>URL des .NET Media Bridge Service</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Save Button */}
|
|
<div className={styles.settingsActions}>
|
|
<button
|
|
className={styles.saveButton}
|
|
onClick={_handleSave}
|
|
disabled={saving}
|
|
>
|
|
{saving ? 'Speichern...' : 'Konfiguration speichern'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default TeamsbotSettingsView;
|