- {/* Director Prompt Panel (private operator instructions) */}
- {['active', 'joining', 'pending'].includes(session.status) && (
+ {/* Director Prompt Panel (private operator instructions).
+ Blacklist statt Whitelist: zeigen ausser bei terminal-Status,
+ damit das Panel nicht waehrend kurzer SSE-Race-Conditions
+ (Status briefly leer/unbekannt direkt nach Mount) verschwindet
+ und erst nach Reload wieder auftaucht. */}
+ {!['ended', 'error', 'leaving'].includes(session.status) && (
{
const [voices, setVoices] = useState([]);
const [loadingVoices, setLoadingVoices] = useState(false);
+ // Media files for avatar picker
+ const [mediaFiles, setMediaFiles] = useState([]);
+ const fileCtx = useFileContext();
+ const avatarInputRef = useRef(null);
+ const [avatarUploading, setAvatarUploading] = useState(false);
+
const _loadConfig = useCallback(async () => {
if (!instanceId) return;
try {
setLoading(true);
// Load per-user settings (merged with instance defaults)
- const [settingsResult, languagesResult] = await Promise.all([
+ const [settingsResult, languagesResult, mediaResult] = await Promise.all([
teamsbotApi.getUserSettings(instanceId),
teamsbotApi.fetchLanguages(),
+ teamsbotApi.listMediaFiles().catch(() => [] as MediaFileInfo[]),
]);
+ setMediaFiles(mediaResult);
const effectiveConfig = settingsResult.effectiveConfig;
setConfig(effectiveConfig);
setFormData(effectiveConfig);
@@ -117,6 +126,35 @@ export const TeamsbotSettingsView: React.FC = () => {
}
};
+ const _refreshMediaFiles = useCallback(async () => {
+ const result = await teamsbotApi.listMediaFiles().catch(() => [] as MediaFileInfo[]);
+ setMediaFiles(result);
+ }, []);
+
+ const _handleAvatarUpload = async (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file || !fileCtx?.handleFileUpload) return;
+ setAvatarUploading(true);
+ try {
+ const result = await fileCtx.handleFileUpload(file);
+ if (result?.success) {
+ const data: any = (result.fileData as any)?.file || result.fileData;
+ const id = data?.id || (result.fileData as any)?.id;
+ if (id) {
+ _updateField('avatarFileId', id);
+ const refreshed = await teamsbotApi.listMediaFiles().catch(() => [] as MediaFileInfo[]);
+ setMediaFiles(refreshed);
+ }
+ }
+ } catch (err: any) {
+ setError(err.message || t('Upload fehlgeschlagen'));
+ setTimeout(() => setError(null), 3000);
+ } finally {
+ setAvatarUploading(false);
+ if (avatarInputRef.current) avatarInputRef.current.value = '';
+ }
+ };
+
const _handleTestVoice = async () => {
if (!instanceId) return;
setTestingVoice(true);
@@ -210,6 +248,48 @@ export const TeamsbotSettingsView: React.FC = () => {
Default-Name fuer den Bot im Meeting. Falls keiner angegeben, wird der Name des System-Bots verwendet (z.B. "Nyla Larsson").
+
+
+
{t('Avatar-Bild / Video')}
+
+ _updateField('avatarFileId', e.target.value || undefined)}
+ onFocus={_refreshMediaFiles}
+ >
+ {t('Standard (statische Farbfläche)')}
+ {mediaFiles.map(f => (
+
+ {f.fileName} ({f.mimeType})
+
+ ))}
+
+
+ avatarInputRef.current?.click()}
+ disabled={avatarUploading}
+ style={{ minWidth: '44px', padding: '8px 12px', whiteSpace: 'nowrap' }}
+ >
+ {avatarUploading ? : null}
+ {avatarUploading ? t('Laden...') : t('Hochladen')}
+
+
+
+ {mediaFiles.length === 0
+ ? t('Noch keine Bild-/Video-Dateien vorhanden. Lade ein Bild oder Video hoch.')
+ : t('Bild oder Video, das als Bot-Video im Meeting angezeigt wird.')}
+
+
{/* AI Behavior */}