feat(teamsbot): dynamic voice API, session auto-load, clean API types

- Dynamic language list from Google TTS API (string codes instead of objects)
- Voice test sends botName for AI-generated sample text per language
- Session view auto-loads most recent session when no sessionId given
- Shows "Keine Sitzungen vorhanden" when no sessions exist
- Updated testVoice API to pass botName instead of static text

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ValueOn AG 2026-02-15 10:08:51 +01:00
parent 76dd20c1ce
commit c58bc77154
3 changed files with 40 additions and 15 deletions

View file

@ -186,16 +186,16 @@ export async function updateConfig(instanceId: string, updates: ConfigUpdateRequ
} }
/** /**
* Test TTS voice with a sample text. Returns base64-encoded audio. * Test TTS voice with AI-generated sample text. Returns base64-encoded audio.
*/ */
export async function testVoice( export async function testVoice(
instanceId: string, instanceId: string,
text: string, botName: string,
language: string, language: string,
voiceId?: string, voiceId?: string,
): Promise<{ success: boolean; audio?: string; format?: string; error?: string }> { ): Promise<{ success: boolean; audio?: string; format?: string; text?: string; error?: string }> {
const response = await api.post(`/api/teamsbot/${instanceId}/voice/test`, { const response = await api.post(`/api/teamsbot/${instanceId}/voice/test`, {
text, botName,
language, language,
voiceId, voiceId,
}); });
@ -204,8 +204,9 @@ export async function testVoice(
/** /**
* Fetch available TTS languages from Google Cloud. * Fetch available TTS languages from Google Cloud.
* Returns array of language codes (e.g. ["de-DE", "en-US", ...])
*/ */
export async function fetchLanguages(): Promise<VoiceLanguage[]> { export async function fetchLanguages(): Promise<string[]> {
try { try {
const response = await api.get('/voice-google/languages'); const response = await api.get('/voice-google/languages');
return response.data?.languages || []; return response.data?.languages || [];

View file

@ -11,25 +11,44 @@ import styles from './Teamsbot.module.css';
export const TeamsbotSessionView: React.FC = () => { export const TeamsbotSessionView: React.FC = () => {
const { instance } = useCurrentInstance(); const { instance } = useCurrentInstance();
const instanceId = instance?.id || ''; const instanceId = instance?.id || '';
const [searchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const sessionId = searchParams.get('sessionId') || ''; const sessionId = searchParams.get('sessionId') || '';
const [session, setSession] = useState<TeamsbotSession | null>(null); const [session, setSession] = useState<TeamsbotSession | null>(null);
const [transcripts, setTranscripts] = useState<TeamsbotTranscript[]>([]); const [transcripts, setTranscripts] = useState<TeamsbotTranscript[]>([]);
const [botResponses, setBotResponses] = useState<TeamsbotBotResponse[]>([]); const [botResponses, setBotResponses] = useState<TeamsbotBotResponse[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [noSessions, setNoSessions] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [isLive, setIsLive] = useState(false); const [isLive, setIsLive] = useState(false);
const transcriptEndRef = useRef<HTMLDivElement>(null); const transcriptEndRef = useRef<HTMLDivElement>(null);
const eventSourceRef = useRef<EventSource | null>(null); const eventSourceRef = useRef<EventSource | null>(null);
// Load session data // Load session data - if no sessionId given, load the most recent session
const _loadSession = useCallback(async () => { const _loadSession = useCallback(async () => {
if (!instanceId || !sessionId) return; if (!instanceId) return;
try { try {
setLoading(true); setLoading(true);
const result = await teamsbotApi.getSession(instanceId, sessionId); setNoSessions(false);
let targetSessionId = sessionId;
// No sessionId in URL -> find the most recent session
if (!targetSessionId) {
const listResult = await teamsbotApi.listSessions(instanceId, true);
const sessions = listResult.sessions || [];
if (sessions.length === 0) {
setNoSessions(true);
setLoading(false);
return;
}
// Pick the most recent (first in list, sorted by creation date desc)
targetSessionId = sessions[0].id;
setSearchParams({ sessionId: targetSessionId }, { replace: true });
}
const result = await teamsbotApi.getSession(instanceId, targetSessionId);
setSession(result.session); setSession(result.session);
setTranscripts(result.transcripts || []); setTranscripts(result.transcripts || []);
setBotResponses(result.botResponses || []); setBotResponses(result.botResponses || []);
@ -39,7 +58,7 @@ export const TeamsbotSessionView: React.FC = () => {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [instanceId, sessionId]); }, [instanceId, sessionId, setSearchParams]);
useEffect(() => { useEffect(() => {
_loadSession(); _loadSession();
@ -134,6 +153,12 @@ export const TeamsbotSessionView: React.FC = () => {
}; };
if (loading) return <div className={styles.loading}>Lade Sitzung...</div>; if (loading) return <div className={styles.loading}>Lade Sitzung...</div>;
if (noSessions) return (
<div className={styles.emptyState || styles.loading}>
<p>Keine Sitzungen vorhanden.</p>
<p>Starte eine neue Sitzung im <strong>Dashboard</strong>.</p>
</div>
);
if (!session) return <div className={styles.errorBanner}>Sitzung nicht gefunden</div>; if (!session) return <div className={styles.errorBanner}>Sitzung nicht gefunden</div>;
return ( return (

View file

@ -33,7 +33,7 @@ export const TeamsbotSettingsView: React.FC = () => {
const [formData, setFormData] = useState<ConfigUpdateRequest>({}); const [formData, setFormData] = useState<ConfigUpdateRequest>({});
// Dynamic voice data from Google TTS API // Dynamic voice data from Google TTS API
const [languages, setLanguages] = useState<VoiceLanguage[]>([]); const [languages, setLanguages] = useState<string[]>([]);
const [voices, setVoices] = useState<VoiceOption[]>([]); const [voices, setVoices] = useState<VoiceOption[]>([]);
const [loadingVoices, setLoadingVoices] = useState(false); const [loadingVoices, setLoadingVoices] = useState(false);
@ -111,8 +111,7 @@ export const TeamsbotSettingsView: React.FC = () => {
try { try {
const language = formData.language || 'de-DE'; const language = formData.language || 'de-DE';
const botName = formData.botName || 'AI Assistant'; const botName = formData.botName || 'AI Assistant';
const testText = `Hallo, ich bin ${botName}. So klinge ich in diesem Meeting.`; const result = await teamsbotApi.testVoice(instanceId, botName, language, formData.voiceId);
const result = await teamsbotApi.testVoice(instanceId, testText, language, formData.voiceId);
if (result.success && result.audio) { if (result.success && result.audio) {
// Play audio from base64 // Play audio from base64
@ -221,8 +220,8 @@ export const TeamsbotSettingsView: React.FC = () => {
onChange={(e) => _handleLanguageChange(e.target.value)} onChange={(e) => _handleLanguageChange(e.target.value)}
> >
{languages.length > 0 ? ( {languages.length > 0 ? (
languages.map((lang, idx) => ( languages.map((langCode, idx) => (
<option key={`${lang.code}-${idx}`} value={lang.code}>{lang.name} ({lang.code})</option> <option key={`${langCode}-${idx}`} value={langCode}>{langCode}</option>
)) ))
) : ( ) : (
<> <>