teamsbot anonymous bot working
This commit is contained in:
parent
ccb2798170
commit
2ee08c314b
4 changed files with 216 additions and 7 deletions
|
|
@ -71,6 +71,7 @@ export interface TeamsbotConfig {
|
|||
triggerCooldownSeconds: number;
|
||||
contextWindowSegments: number;
|
||||
debugMode?: boolean;
|
||||
avatarFileId?: string;
|
||||
}
|
||||
|
||||
export interface TeamsbotSessionStats {
|
||||
|
|
@ -103,6 +104,7 @@ export interface ConfigUpdateRequest {
|
|||
triggerCooldownSeconds?: number;
|
||||
contextWindowSegments?: number;
|
||||
debugMode?: boolean;
|
||||
avatarFileId?: string;
|
||||
}
|
||||
|
||||
// Voice option type re-exported from the central voice catalog API
|
||||
|
|
@ -602,6 +604,7 @@ export interface MeetingModule {
|
|||
kpiTargets?: string;
|
||||
defaultMeetingLink?: string;
|
||||
defaultBotName?: string;
|
||||
defaultAvatarFileId?: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
|
|
@ -612,7 +615,7 @@ export async function listModules(instanceId: string): Promise<MeetingModule[]>
|
|||
|
||||
export async function createModule(instanceId: string, body: {
|
||||
title: string; seriesType?: string; defaultBotId?: string; goals?: string; kpiTargets?: string;
|
||||
defaultMeetingLink?: string; defaultBotName?: string;
|
||||
defaultMeetingLink?: string; defaultBotName?: string; defaultAvatarFileId?: string;
|
||||
}): Promise<MeetingModule> {
|
||||
const response = await api.post(`/api/teamsbot/${instanceId}/modules`, body);
|
||||
return response.data?.module;
|
||||
|
|
@ -631,3 +634,31 @@ export async function updateModule(instanceId: string, moduleId: string, body: P
|
|||
export async function deleteModule(instanceId: string, moduleId: string): Promise<void> {
|
||||
await api.delete(`/api/teamsbot/${instanceId}/modules/${moduleId}`);
|
||||
}
|
||||
|
||||
export interface MediaFileInfo {
|
||||
id: string;
|
||||
fileName: string;
|
||||
mimeType: string;
|
||||
}
|
||||
|
||||
export async function listMediaFiles(): Promise<MediaFileInfo[]> {
|
||||
const response = await api.get('/api/files/list', {
|
||||
params: { pagination: JSON.stringify({ pageSize: 500 }) },
|
||||
});
|
||||
const data = response.data;
|
||||
let items: any[];
|
||||
if (Array.isArray(data)) {
|
||||
items = data;
|
||||
} else if (Array.isArray(data?.items)) {
|
||||
items = data.items;
|
||||
} else {
|
||||
console.warn('[listMediaFiles] unexpected response shape:', Object.keys(data || {}));
|
||||
items = [];
|
||||
}
|
||||
const filtered = items.filter((f: any) => {
|
||||
const mime = (f.mimeType || '').toLowerCase();
|
||||
return mime.startsWith('image/') || mime.startsWith('video/');
|
||||
});
|
||||
console.log(`[listMediaFiles] ${items.length} total files, ${filtered.length} media files`);
|
||||
return filtered.map((f: any) => ({ id: f.id, fileName: f.fileName, mimeType: f.mimeType }));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@ import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useCurrentInstance } from '../../../hooks/useCurrentInstance';
|
||||
import * as teamsbotApi from '../../../api/teamsbotApi';
|
||||
import type { MeetingModule, TeamsbotSession } from '../../../api/teamsbotApi';
|
||||
import type { MeetingModule, TeamsbotSession, MediaFileInfo } from '../../../api/teamsbotApi';
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
import { useFileContext } from '../../../contexts/FileContext';
|
||||
import { FaSpinner } from 'react-icons/fa';
|
||||
import styles from './Teamsbot.module.css';
|
||||
|
||||
const SERIES_TYPE_LABELS: Record<string, string> = {
|
||||
|
|
@ -46,8 +48,47 @@ export const TeamsbotModulesView: React.FC = () => {
|
|||
const [createDefaultLink, setCreateDefaultLink] = useState('');
|
||||
const [createDefaultBotName, setCreateDefaultBotName] = useState('');
|
||||
const [createGoals, setCreateGoals] = useState('');
|
||||
const [createDefaultAvatarFileId, setCreateDefaultAvatarFileId] = useState('');
|
||||
const [createSaving, setCreateSaving] = useState(false);
|
||||
|
||||
const [mediaFiles, setMediaFiles] = useState<MediaFileInfo[]>([]);
|
||||
const fileCtx = useFileContext();
|
||||
const avatarInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [avatarUploading, setAvatarUploading] = useState(false);
|
||||
const [avatarTarget, setAvatarTarget] = useState<'create' | 'edit'>('create');
|
||||
|
||||
const _refreshMediaFiles = useCallback(async () => {
|
||||
const result = await teamsbotApi.listMediaFiles().catch(() => [] as MediaFileInfo[]);
|
||||
setMediaFiles(result);
|
||||
}, []);
|
||||
|
||||
const _handleAvatarUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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) {
|
||||
if (avatarTarget === 'create') {
|
||||
setCreateDefaultAvatarFileId(id);
|
||||
} else if (editingModule) {
|
||||
setEditingModule({ ...editingModule, defaultAvatarFileId: id });
|
||||
}
|
||||
const refreshed = await teamsbotApi.listMediaFiles().catch(() => [] as MediaFileInfo[]);
|
||||
setMediaFiles(refreshed);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// upload error handled by FileContext
|
||||
} finally {
|
||||
setAvatarUploading(false);
|
||||
if (avatarInputRef.current) avatarInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const _loadModules = useCallback(async () => {
|
||||
if (!instanceId) return;
|
||||
setLoading(true);
|
||||
|
|
@ -63,6 +104,10 @@ export const TeamsbotModulesView: React.FC = () => {
|
|||
|
||||
useEffect(() => { _loadModules(); }, [_loadModules]);
|
||||
|
||||
useEffect(() => {
|
||||
teamsbotApi.listMediaFiles().then(setMediaFiles).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const _loadModuleSessions = useCallback(async (moduleId: string) => {
|
||||
if (!instanceId) return;
|
||||
try {
|
||||
|
|
@ -125,6 +170,7 @@ export const TeamsbotModulesView: React.FC = () => {
|
|||
seriesType: createSeriesType,
|
||||
defaultMeetingLink: createDefaultLink.trim() || undefined,
|
||||
defaultBotName: createDefaultBotName.trim() || undefined,
|
||||
defaultAvatarFileId: createDefaultAvatarFileId || undefined,
|
||||
goals: createGoals.trim() || undefined,
|
||||
});
|
||||
setCreateOpen(false);
|
||||
|
|
@ -132,6 +178,7 @@ export const TeamsbotModulesView: React.FC = () => {
|
|||
setCreateSeriesType('adhoc');
|
||||
setCreateDefaultLink('');
|
||||
setCreateDefaultBotName('');
|
||||
setCreateDefaultAvatarFileId('');
|
||||
setCreateGoals('');
|
||||
_loadModules();
|
||||
} catch (err) {
|
||||
|
|
@ -185,7 +232,7 @@ export const TeamsbotModulesView: React.FC = () => {
|
|||
<div className={styles.moduleRow} onClick={() => _toggleExpand(mod.id)}>
|
||||
<span className={styles.moduleType}>{t(SERIES_TYPE_LABELS[mod.seriesType] || mod.seriesType)}</span>
|
||||
<span className={styles.moduleTitle}>{mod.title}</span>
|
||||
<span className={styles.moduleStatus}>{t(STATUS_LABELS[mod.status] || mod.status)}</span>
|
||||
<span className={styles.moduleStatus}>{t(STATUS_LABELS[mod.status] || mod.status || 'Aktiv')}</span>
|
||||
<div className={styles.moduleActions}>
|
||||
<button className={styles.btnPrimary} style={{ padding: '0.3rem 0.7rem', fontSize: '0.8rem' }} onClick={e => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -257,6 +304,25 @@ export const TeamsbotModulesView: React.FC = () => {
|
|||
value={createDefaultBotName}
|
||||
onChange={e => setCreateDefaultBotName(e.target.value)}
|
||||
/>
|
||||
<label className={styles.label} style={{ display: 'block', marginTop: 8 }}>{t('Avatar-Bild / Video')}</label>
|
||||
<div style={{ display: 'flex', gap: '6px', alignItems: 'center' }}>
|
||||
<select
|
||||
className={styles.wizardSelect}
|
||||
style={{ flex: 1 }}
|
||||
value={createDefaultAvatarFileId}
|
||||
onChange={e => setCreateDefaultAvatarFileId(e.target.value)}
|
||||
onFocus={_refreshMediaFiles}
|
||||
>
|
||||
<option value="">{t('Standard (aus Bot-Einstellungen)')}</option>
|
||||
{mediaFiles.map(f => (
|
||||
<option key={f.id} value={f.id}>{f.fileName} ({f.mimeType})</option>
|
||||
))}
|
||||
</select>
|
||||
<button type="button" className={styles.btnSmall} disabled={avatarUploading} onClick={() => { setAvatarTarget('create'); avatarInputRef.current?.click(); }} style={{ whiteSpace: 'nowrap' }}>
|
||||
{avatarUploading && avatarTarget === 'create' ? <FaSpinner className={styles.spinner} /> : null}
|
||||
{t('Hochladen')}
|
||||
</button>
|
||||
</div>
|
||||
<label className={styles.label} style={{ display: 'block', marginTop: 8 }}>{t('Ziele')}</label>
|
||||
<textarea
|
||||
className={styles.wizardTextarea}
|
||||
|
|
@ -317,6 +383,25 @@ export const TeamsbotModulesView: React.FC = () => {
|
|||
placeholder={t('z.B. AI Assistant')}
|
||||
onBlur={e => setEditingModule({ ...editingModule, defaultBotName: e.target.value })}
|
||||
/>
|
||||
<label className={styles.label} style={{ display: 'block', marginTop: 8 }}>{t('Avatar-Bild / Video')}</label>
|
||||
<div style={{ display: 'flex', gap: '6px', alignItems: 'center' }}>
|
||||
<select
|
||||
className={styles.wizardSelect}
|
||||
style={{ flex: 1 }}
|
||||
value={editingModule.defaultAvatarFileId || ''}
|
||||
onChange={e => setEditingModule({ ...editingModule, defaultAvatarFileId: e.target.value || undefined })}
|
||||
onFocus={_refreshMediaFiles}
|
||||
>
|
||||
<option value="">{t('Standard (aus Bot-Einstellungen)')}</option>
|
||||
{mediaFiles.map(f => (
|
||||
<option key={f.id} value={f.id}>{f.fileName} ({f.mimeType})</option>
|
||||
))}
|
||||
</select>
|
||||
<button type="button" className={styles.btnSmall} disabled={avatarUploading} onClick={() => { setAvatarTarget('edit'); avatarInputRef.current?.click(); }} style={{ whiteSpace: 'nowrap' }}>
|
||||
{avatarUploading && avatarTarget === 'edit' ? <FaSpinner className={styles.spinner} /> : null}
|
||||
{t('Hochladen')}
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
defaultValue={editingModule.goals || ''}
|
||||
className={styles.wizardTextarea}
|
||||
|
|
@ -331,11 +416,20 @@ export const TeamsbotModulesView: React.FC = () => {
|
|||
goals: editingModule.goals,
|
||||
defaultMeetingLink: (editingModule.defaultMeetingLink || '').trim(),
|
||||
defaultBotName: (editingModule.defaultBotName || '').trim(),
|
||||
defaultAvatarFileId: (editingModule.defaultAvatarFileId || '').trim() || undefined,
|
||||
})}>{t('Speichern')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input
|
||||
ref={avatarInputRef}
|
||||
type="file"
|
||||
accept="image/*,video/*"
|
||||
style={{ display: 'none' }}
|
||||
onChange={_handleAvatarUpload}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -899,8 +899,12 @@ export const TeamsbotSessionView: React.FC = () => {
|
|||
{/* Main column */}
|
||||
<div className={styles.sessionMain}>
|
||||
|
||||
{/* 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) && (
|
||||
<div
|
||||
className={`${styles.directorPanel} ${directorDragOver ? styles.directorPanelDragOver : ''}`}
|
||||
onDragEnter={_onDirectorDragEnter}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useCurrentInstance } from '../../../hooks/useCurrentInstance';
|
||||
import * as teamsbotApi from '../../../api/teamsbotApi';
|
||||
import type { TeamsbotConfig, ConfigUpdateRequest, VoiceOption, SystemBot } from '../../../api/teamsbotApi';
|
||||
import type { TeamsbotConfig, ConfigUpdateRequest, VoiceOption, SystemBot, MediaFileInfo } from '../../../api/teamsbotApi';
|
||||
import type { VoiceLanguage } from '../../../api/voiceCatalogApi';
|
||||
import { FaPlay, FaSpinner, FaTrash } from 'react-icons/fa';
|
||||
import styles from './Teamsbot.module.css';
|
||||
import { getUserDataCache } from '../../../utils/userCache';
|
||||
import { useFileContext } from '../../../contexts/FileContext';
|
||||
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
|
|
@ -45,16 +46,24 @@ export const TeamsbotSettingsView: React.FC = () => {
|
|||
const [voices, setVoices] = useState<VoiceOption[]>([]);
|
||||
const [loadingVoices, setLoadingVoices] = useState(false);
|
||||
|
||||
// Media files for avatar picker
|
||||
const [mediaFiles, setMediaFiles] = useState<MediaFileInfo[]>([]);
|
||||
const fileCtx = useFileContext();
|
||||
const avatarInputRef = useRef<HTMLInputElement | null>(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<HTMLInputElement>) => {
|
||||
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").
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>{t('Avatar-Bild / Video')}</label>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<select
|
||||
className={styles.select}
|
||||
style={{ flex: 1 }}
|
||||
value={formData.avatarFileId || ''}
|
||||
onChange={(e) => _updateField('avatarFileId', e.target.value || undefined)}
|
||||
onFocus={_refreshMediaFiles}
|
||||
>
|
||||
<option value="">{t('Standard (statische Farbfläche)')}</option>
|
||||
{mediaFiles.map(f => (
|
||||
<option key={f.id} value={f.id}>
|
||||
{f.fileName} ({f.mimeType})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
ref={avatarInputRef}
|
||||
type="file"
|
||||
accept="image/*,video/*"
|
||||
style={{ display: 'none' }}
|
||||
onChange={_handleAvatarUpload}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.testButton || styles.saveButton}
|
||||
onClick={() => avatarInputRef.current?.click()}
|
||||
disabled={avatarUploading}
|
||||
style={{ minWidth: '44px', padding: '8px 12px', whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{avatarUploading ? <FaSpinner className={styles.spinner} /> : null}
|
||||
{avatarUploading ? t('Laden...') : t('Hochladen')}
|
||||
</button>
|
||||
</div>
|
||||
<span className={styles.hint}>
|
||||
{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.')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI Behavior */}
|
||||
|
|
|
|||
Loading…
Reference in a new issue