centralized language catalog
This commit is contained in:
parent
d814a76660
commit
13f4574098
9 changed files with 277 additions and 157 deletions
|
|
@ -31,6 +31,7 @@ import { LanguageProvider } from './providers/language/LanguageContext';
|
|||
import { ToastProvider } from './contexts/ToastContext';
|
||||
import { WorkflowSelectionProvider } from './contexts/WorkflowSelectionContext';
|
||||
import { FileProvider } from './contexts/FileContext';
|
||||
import { VoiceCatalogProvider } from './contexts/VoiceCatalogContext';
|
||||
import { MainLayout } from './layouts/MainLayout';
|
||||
import { FeatureLayout } from './layouts/FeatureLayout';
|
||||
import { DashboardPage } from './pages/Dashboard';
|
||||
|
|
@ -72,6 +73,7 @@ function App() {
|
|||
<LanguageProvider>
|
||||
<AuthProvider>
|
||||
<ToastProvider>
|
||||
<VoiceCatalogProvider>
|
||||
<WorkflowSelectionProvider>
|
||||
<Router>
|
||||
<Routes>
|
||||
|
|
@ -231,6 +233,7 @@ function App() {
|
|||
</Routes>
|
||||
</Router>
|
||||
</WorkflowSelectionProvider>
|
||||
</VoiceCatalogProvider>
|
||||
</ToastProvider>
|
||||
</AuthProvider>
|
||||
</LanguageProvider>
|
||||
|
|
|
|||
|
|
@ -102,18 +102,10 @@ export interface ConfigUpdateRequest {
|
|||
debugMode?: boolean;
|
||||
}
|
||||
|
||||
// Voice/Language Types (from Google TTS API)
|
||||
export interface VoiceLanguage {
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface VoiceOption {
|
||||
name: string;
|
||||
languageCodes: string[];
|
||||
ssmlGender: string;
|
||||
naturalSampleRateHertz: number;
|
||||
}
|
||||
// Voice option type re-exported from the central voice catalog API.
|
||||
// The legacy teamsbot-specific {code,name} language type is gone — consumers
|
||||
// should use VoiceLanguage from voiceCatalogApi (catalog SSOT).
|
||||
export type { VoiceOption } from './voiceCatalogApi';
|
||||
|
||||
// Auth Detection Test Types
|
||||
export interface StepScreenshot {
|
||||
|
|
@ -313,25 +305,19 @@ export async function testVoice(
|
|||
}
|
||||
|
||||
/**
|
||||
* Fetch available TTS languages from Google Cloud.
|
||||
* Returns array of language codes (e.g. ["de-DE", "en-US", ...])
|
||||
* Fetch the curated voice/language catalog (single source of truth).
|
||||
* Re-exports the central voiceCatalogApi.fetchVoiceCatalog so legacy
|
||||
* teamsbot consumers stay on one import surface.
|
||||
*/
|
||||
export async function fetchLanguages(): Promise<string[]> {
|
||||
try {
|
||||
const response = await api.get('/voice-google/languages');
|
||||
return response.data?.languages || [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
export { fetchVoiceCatalog as fetchLanguages } from './voiceCatalogApi';
|
||||
|
||||
/**
|
||||
* Fetch available TTS voices for a language from Google Cloud.
|
||||
*/
|
||||
export async function fetchVoices(languageCode: string): Promise<VoiceOption[]> {
|
||||
try {
|
||||
const response = await api.get('/voice-google/voices', {
|
||||
params: { languageCode },
|
||||
const response = await api.get('/api/voice/voices', {
|
||||
params: { language: languageCode },
|
||||
});
|
||||
return response.data?.voices || [];
|
||||
} catch {
|
||||
|
|
|
|||
47
src/api/voiceCatalogApi.ts
Normal file
47
src/api/voiceCatalogApi.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* Voice / Language Catalog API.
|
||||
*
|
||||
* Single source of truth for every voice-language picker, default-voice
|
||||
* lookup, and ISO ⇄ BCP-47 mapping in the frontend. Mirrors
|
||||
* gateway/modules/shared/voiceCatalog.py 1:1.
|
||||
*
|
||||
* Hard-coded language lists or ad-hoc maps in components are forbidden —
|
||||
* consume `useVoiceCatalog()` instead.
|
||||
*/
|
||||
|
||||
import api from '../api';
|
||||
|
||||
export interface VoiceLanguage {
|
||||
bcp47: string;
|
||||
iso: string;
|
||||
label: string;
|
||||
flag: string;
|
||||
defaultVoice: string | null;
|
||||
}
|
||||
|
||||
export interface VoiceOption {
|
||||
name: string;
|
||||
languageCodes: string[];
|
||||
ssmlGender: string;
|
||||
naturalSampleRateHertz: number;
|
||||
}
|
||||
|
||||
interface CatalogResponse {
|
||||
languages: VoiceLanguage[];
|
||||
}
|
||||
|
||||
interface VoicesResponse {
|
||||
voices: VoiceOption[];
|
||||
}
|
||||
|
||||
export async function fetchVoiceCatalog(): Promise<VoiceLanguage[]> {
|
||||
const response = await api.get<CatalogResponse>('/api/voice/languages');
|
||||
return response.data?.languages ?? [];
|
||||
}
|
||||
|
||||
export async function fetchVoicesForLanguage(bcp47: string): Promise<VoiceOption[]> {
|
||||
const response = await api.get<VoicesResponse>('/api/voice/voices', {
|
||||
params: { language: bcp47 },
|
||||
});
|
||||
return response.data?.voices ?? [];
|
||||
}
|
||||
|
|
@ -1,67 +1,29 @@
|
|||
/**
|
||||
* VoiceLanguageSelect
|
||||
*
|
||||
* Reusable component for selecting voice/speech recognition language.
|
||||
* Defaults to user's profile language.
|
||||
* Can be used for speech-to-text, text-to-speech, and translation features.
|
||||
* Reusable picker for voice/speech-recognition language. Reads the language
|
||||
* list from the central VoiceCatalog (single source of truth) — never
|
||||
* hard-coded here.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
import { useVoiceCatalog, useDefaultVoiceLocale } from '../../../contexts/VoiceCatalogContext';
|
||||
import type { VoiceLanguage } from '../../../api/voiceCatalogApi';
|
||||
import styles from './VoiceLanguageSelect.module.css';
|
||||
|
||||
// Voice language options with full locale codes for Google Cloud Speech
|
||||
export interface VoiceLanguageOption {
|
||||
code: string; // Full locale code (e.g., 'de-DE')
|
||||
label: string; // Display label
|
||||
shortCode: string; // Short code for mapping (e.g., 'de')
|
||||
flag?: string; // Optional flag emoji
|
||||
}
|
||||
|
||||
// Supported languages for speech recognition
|
||||
export const voiceLanguages: VoiceLanguageOption[] = [
|
||||
{ code: 'de-DE', label: 'Deutsch', shortCode: 'de', flag: '🇩🇪' },
|
||||
{ code: 'de-CH', label: 'Deutsch (Schweiz)', shortCode: 'de', flag: '🇨🇭' },
|
||||
{ code: 'en-US', label: 'English (US)', shortCode: 'en', flag: '🇺🇸' },
|
||||
{ code: 'en-GB', label: 'English (UK)', shortCode: 'en', flag: '🇬🇧' },
|
||||
{ code: 'fr-FR', label: 'Français', shortCode: 'fr', flag: '🇫🇷' },
|
||||
{ code: 'fr-CH', label: 'Français (Suisse)', shortCode: 'fr', flag: '🇨🇭' },
|
||||
{ code: 'it-IT', label: 'Italiano', shortCode: 'it', flag: '🇮🇹' },
|
||||
{ code: 'it-CH', label: 'Italiano (Svizzera)', shortCode: 'it', flag: '🇨🇭' },
|
||||
{ code: 'es-ES', label: 'Español', shortCode: 'es', flag: '🇪🇸' },
|
||||
{ code: 'pt-BR', label: 'Português', shortCode: 'pt', flag: '🇧🇷' },
|
||||
];
|
||||
|
||||
// Map user profile language (short code) to default voice language (full code)
|
||||
const profileToVoiceLanguage: Record<string, string> = {
|
||||
'de': 'de-DE',
|
||||
'en': 'en-US',
|
||||
'fr': 'fr-FR',
|
||||
'it': 'it-IT',
|
||||
'es': 'es-ES',
|
||||
'pt': 'pt-BR',
|
||||
};
|
||||
export type VoiceLanguageOption = VoiceLanguage;
|
||||
|
||||
export interface VoiceLanguageSelectProps {
|
||||
value: string;
|
||||
onChange: (languageCode: string) => void;
|
||||
disabled?: boolean;
|
||||
compact?: boolean; // Compact mode shows only flag/short code
|
||||
showFlags?: boolean; // Show flag emojis
|
||||
compact?: boolean;
|
||||
showFlags?: boolean;
|
||||
className?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default voice language based on user's profile language
|
||||
*/
|
||||
export const getDefaultVoiceLanguage = (profileLanguage?: string): string => {
|
||||
if (profileLanguage && profileToVoiceLanguage[profileLanguage]) {
|
||||
return profileToVoiceLanguage[profileLanguage];
|
||||
}
|
||||
return 'de-DE'; // Default fallback
|
||||
};
|
||||
|
||||
export const VoiceLanguageSelect: React.FC<VoiceLanguageSelectProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
|
|
@ -71,6 +33,8 @@ export const VoiceLanguageSelect: React.FC<VoiceLanguageSelectProps> = ({
|
|||
className = '',
|
||||
title = 'Sprache für Spracherkennung',
|
||||
}) => {
|
||||
const { languages, isLoading } = useVoiceCatalog();
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
onChange(e.target.value);
|
||||
};
|
||||
|
|
@ -81,13 +45,13 @@ export const VoiceLanguageSelect: React.FC<VoiceLanguageSelectProps> = ({
|
|||
className={styles.select}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
disabled={disabled || isLoading}
|
||||
title={title}
|
||||
>
|
||||
{voiceLanguages.map((lang) => (
|
||||
<option key={lang.code} value={lang.code}>
|
||||
{languages.map((lang) => (
|
||||
<option key={lang.bcp47} value={lang.bcp47}>
|
||||
{showFlags && lang.flag ? `${lang.flag} ` : ''}
|
||||
{compact ? lang.code.split('-')[0].toUpperCase() : lang.label}
|
||||
{compact ? lang.iso.toUpperCase() : lang.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
|
@ -96,28 +60,25 @@ export const VoiceLanguageSelect: React.FC<VoiceLanguageSelectProps> = ({
|
|||
};
|
||||
|
||||
/**
|
||||
* Hook to manage voice language state with user profile default
|
||||
* Hook to manage voice language state with user profile default.
|
||||
* Initial value falls back to the catalog-derived default for the profile language.
|
||||
*/
|
||||
export const useVoiceLanguage = (initialValue?: string) => {
|
||||
const { currentLanguage } = useLanguage();
|
||||
const { languages } = useVoiceCatalog();
|
||||
const defaultLocale = useDefaultVoiceLocale(currentLanguage);
|
||||
|
||||
// Track if user has manually changed the language
|
||||
const hasManuallyChanged = React.useRef(false);
|
||||
|
||||
// Initialize with user's profile language (or provided initial value)
|
||||
const [voiceLanguage, setVoiceLanguage] = React.useState<string>(
|
||||
initialValue || getDefaultVoiceLanguage(currentLanguage)
|
||||
initialValue || defaultLocale,
|
||||
);
|
||||
|
||||
// Update voice language when user profile language changes (only if not manually set)
|
||||
React.useEffect(() => {
|
||||
if (!initialValue && !hasManuallyChanged.current) {
|
||||
const newDefault = getDefaultVoiceLanguage(currentLanguage);
|
||||
setVoiceLanguage(newDefault);
|
||||
setVoiceLanguage(defaultLocale);
|
||||
}
|
||||
}, [currentLanguage, initialValue]);
|
||||
}, [defaultLocale, initialValue]);
|
||||
|
||||
// Wrapper to track manual changes
|
||||
const handleSetVoiceLanguage = React.useCallback((newLanguage: string) => {
|
||||
hasManuallyChanged.current = true;
|
||||
setVoiceLanguage(newLanguage);
|
||||
|
|
@ -126,7 +87,7 @@ export const useVoiceLanguage = (initialValue?: string) => {
|
|||
return {
|
||||
voiceLanguage,
|
||||
setVoiceLanguage: handleSetVoiceLanguage,
|
||||
voiceLanguages,
|
||||
voiceLanguages: languages,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
export {
|
||||
VoiceLanguageSelect,
|
||||
useVoiceLanguage,
|
||||
getDefaultVoiceLanguage,
|
||||
voiceLanguages,
|
||||
type VoiceLanguageOption,
|
||||
type VoiceLanguageSelectProps
|
||||
type VoiceLanguageSelectProps,
|
||||
} from './VoiceLanguageSelect';
|
||||
|
|
|
|||
151
src/contexts/VoiceCatalogContext.tsx
Normal file
151
src/contexts/VoiceCatalogContext.tsx
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
/**
|
||||
* VoiceCatalogContext
|
||||
*
|
||||
* Loads the central voice/language catalog from the backend exactly once and
|
||||
* makes it available to every component via `useVoiceCatalog()`.
|
||||
*
|
||||
* Provides convenience helpers for ISO ⇄ BCP-47 lookups and curated default
|
||||
* voices, mirroring the backend `voiceCatalog` API. Components MUST NOT keep
|
||||
* their own static language lists.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { fetchVoiceCatalog, VoiceLanguage } from '../api/voiceCatalogApi';
|
||||
|
||||
interface VoiceCatalogContextType {
|
||||
languages: VoiceLanguage[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
getByBcp47: (code: string | null | undefined) => VoiceLanguage | undefined;
|
||||
getByIso: (iso: string | null | undefined) => VoiceLanguage | undefined;
|
||||
isoToBcp47: (iso: string | null | undefined) => string | undefined;
|
||||
getDefaultVoice: (bcp47: string | null | undefined) => string | null;
|
||||
}
|
||||
|
||||
const VoiceCatalogContext = createContext<VoiceCatalogContextType | undefined>(undefined);
|
||||
|
||||
interface VoiceCatalogProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const VoiceCatalogProvider: React.FC<VoiceCatalogProviderProps> = ({ children }) => {
|
||||
const [languages, setLanguages] = useState<VoiceLanguage[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
const data = await fetchVoiceCatalog();
|
||||
if (!cancelled) {
|
||||
setLanguages(data);
|
||||
setError(null);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (!cancelled) {
|
||||
setError(err?.message || 'Failed to load voice catalog');
|
||||
setLanguages([]);
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setIsLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const byBcp47 = useMemo(() => {
|
||||
const map = new Map<string, VoiceLanguage>();
|
||||
for (const v of languages) map.set(v.bcp47.toLowerCase(), v);
|
||||
return map;
|
||||
}, [languages]);
|
||||
|
||||
const byIso = useMemo(() => {
|
||||
const map = new Map<string, VoiceLanguage>();
|
||||
for (const v of languages) {
|
||||
if (!map.has(v.iso.toLowerCase())) map.set(v.iso.toLowerCase(), v);
|
||||
}
|
||||
return map;
|
||||
}, [languages]);
|
||||
|
||||
const getByBcp47 = useCallback(
|
||||
(code: string | null | undefined) =>
|
||||
code ? byBcp47.get(code.trim().toLowerCase()) : undefined,
|
||||
[byBcp47],
|
||||
);
|
||||
|
||||
const getByIso = useCallback(
|
||||
(iso: string | null | undefined) =>
|
||||
iso ? byIso.get(iso.trim().toLowerCase()) : undefined,
|
||||
[byIso],
|
||||
);
|
||||
|
||||
const isoToBcp47 = useCallback(
|
||||
(iso: string | null | undefined): string | undefined => {
|
||||
if (!iso) return undefined;
|
||||
const trimmed = iso.trim();
|
||||
if (!trimmed) return undefined;
|
||||
if (trimmed.includes('-')) {
|
||||
const canonical = byBcp47.get(trimmed.toLowerCase());
|
||||
return canonical ? canonical.bcp47 : trimmed;
|
||||
}
|
||||
const entry = byIso.get(trimmed.toLowerCase());
|
||||
if (entry) return entry.bcp47;
|
||||
return `${trimmed.toLowerCase()}-${trimmed.toUpperCase()}`;
|
||||
},
|
||||
[byBcp47, byIso],
|
||||
);
|
||||
|
||||
const getDefaultVoice = useCallback(
|
||||
(bcp47: string | null | undefined): string | null => {
|
||||
const entry = getByBcp47(bcp47);
|
||||
return entry?.defaultVoice ?? null;
|
||||
},
|
||||
[getByBcp47],
|
||||
);
|
||||
|
||||
const value = useMemo<VoiceCatalogContextType>(
|
||||
() => ({
|
||||
languages,
|
||||
isLoading,
|
||||
error,
|
||||
getByBcp47,
|
||||
getByIso,
|
||||
isoToBcp47,
|
||||
getDefaultVoice,
|
||||
}),
|
||||
[languages, isLoading, error, getByBcp47, getByIso, isoToBcp47, getDefaultVoice],
|
||||
);
|
||||
|
||||
return (
|
||||
<VoiceCatalogContext.Provider value={value}>{children}</VoiceCatalogContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useVoiceCatalog = (): VoiceCatalogContextType => {
|
||||
const ctx = useContext(VoiceCatalogContext);
|
||||
if (!ctx) {
|
||||
throw new Error('useVoiceCatalog must be used within VoiceCatalogProvider');
|
||||
}
|
||||
return ctx;
|
||||
};
|
||||
|
||||
/**
|
||||
* Map a profile language (ISO short code) to a default voice locale.
|
||||
* Returns the catalog's BCP-47 for the ISO if available, else falls back to
|
||||
* `de-DE` so the UI always has a deterministic starting value.
|
||||
*/
|
||||
export const useDefaultVoiceLocale = (profileLanguage?: string | null): string => {
|
||||
const { isoToBcp47 } = useVoiceCatalog();
|
||||
return isoToBcp47(profileLanguage) || 'de-DE';
|
||||
};
|
||||
|
|
@ -11,6 +11,7 @@ import { setUserDataCache, getUserDataCache } from '../utils/userCache';
|
|||
import { FormGeneratorForm } from '../components/FormGenerator/FormGeneratorForm/FormGeneratorForm';
|
||||
import type { AttributeDefinition } from '../components/FormGenerator/FormGeneratorForm/FormGeneratorForm';
|
||||
import { useApiRequest } from '../hooks/useApi';
|
||||
import { useVoiceCatalog } from '../contexts/VoiceCatalogContext';
|
||||
import styles from './Settings.module.css';
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -92,6 +93,7 @@ interface VoiceMapEntry { language: string; voiceName: string; }
|
|||
const VoiceSettingsTab: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
const { request } = useApiRequest();
|
||||
const { languages: voiceCatalog } = useVoiceCatalog();
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
|
@ -100,7 +102,6 @@ const VoiceSettingsTab: React.FC = () => {
|
|||
const [success, setSuccess] = useState<string | null>(null);
|
||||
|
||||
const [sttLanguage, setSttLanguage] = useState('de-DE');
|
||||
const [languages, setLanguages] = useState<any[]>([]);
|
||||
const [voiceMap, setVoiceMap] = useState<VoiceMapEntry[]>([]);
|
||||
|
||||
const [addLanguage, setAddLanguage] = useState('de-DE');
|
||||
|
|
@ -111,13 +112,7 @@ const VoiceSettingsTab: React.FC = () => {
|
|||
const _loadSettings = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [prefsData, languagesData] = await Promise.all([
|
||||
request({ url: '/api/voice/preferences', method: 'get' }),
|
||||
request({ url: '/api/voice/languages', method: 'get' }),
|
||||
]);
|
||||
|
||||
const langList = (languagesData as any)?.languages || [];
|
||||
setLanguages(langList);
|
||||
const prefsData = await request({ url: '/api/voice/preferences', method: 'get' });
|
||||
|
||||
const prefs = prefsData as any;
|
||||
setSttLanguage(prefs?.sttLanguage || 'de-DE');
|
||||
|
|
@ -203,16 +198,9 @@ const VoiceSettingsTab: React.FC = () => {
|
|||
}, [request]);
|
||||
|
||||
const _getLanguageName = useCallback((code: string) => {
|
||||
const found = languages.find((l: any) => (l.code || l) === code);
|
||||
return found?.name || found?.code || code;
|
||||
}, [languages]);
|
||||
|
||||
const _defaultLangs = [
|
||||
{ code: 'de-DE', name: 'Deutsch' }, { code: 'en-US', name: 'English (US)' },
|
||||
{ code: 'fr-FR', name: 'Francais' }, { code: 'it-IT', name: 'Italiano' },
|
||||
{ code: 'es-ES', name: 'Espanol' },
|
||||
];
|
||||
const _displayLanguages = languages.length > 0 ? languages : _defaultLangs;
|
||||
const entry = voiceCatalog.find(l => l.bcp47.toLowerCase() === code.toLowerCase());
|
||||
return entry ? `${entry.flag ? entry.flag + ' ' : ''}${entry.label}` : code;
|
||||
}, [voiceCatalog]);
|
||||
|
||||
if (loading) return <div style={{ padding: '1rem', color: '#888' }}>{t('Einstellungen werden geladen')}</div>;
|
||||
|
||||
|
|
@ -230,8 +218,10 @@ const VoiceSettingsTab: React.FC = () => {
|
|||
</div>
|
||||
<div className={styles.settingControl}>
|
||||
<select className={styles.select} value={sttLanguage} onChange={e => setSttLanguage(e.target.value)}>
|
||||
{_displayLanguages.map((lang: any) => (
|
||||
<option key={lang.code || lang} value={lang.code || lang}>{lang.name || lang.code || lang}</option>
|
||||
{voiceCatalog.map(lang => (
|
||||
<option key={lang.bcp47} value={lang.bcp47}>
|
||||
{lang.flag ? `${lang.flag} ` : ''}{lang.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
|
@ -274,8 +264,10 @@ const VoiceSettingsTab: React.FC = () => {
|
|||
<div>
|
||||
<label className={styles.settingLabel} style={{ fontSize: '0.8rem' }}>{t('Sprache')}</label>
|
||||
<select className={styles.select} value={addLanguage} onChange={e => setAddLanguage(e.target.value)}>
|
||||
{_displayLanguages.map((lang: any) => (
|
||||
<option key={lang.code || lang} value={lang.code || lang}>{lang.name || lang.code || lang}</option>
|
||||
{voiceCatalog.map(lang => (
|
||||
<option key={lang.bcp47} value={lang.bcp47}>
|
||||
{lang.flag ? `${lang.flag} ` : ''}{lang.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|||
import { useCurrentInstance } from '../../../hooks/useCurrentInstance';
|
||||
import * as teamsbotApi from '../../../api/teamsbotApi';
|
||||
import type { TeamsbotConfig, ConfigUpdateRequest, VoiceOption } from '../../../api/teamsbotApi';
|
||||
import type { VoiceLanguage } from '../../../api/voiceCatalogApi';
|
||||
import { FaPlay, FaSpinner } from 'react-icons/fa';
|
||||
import styles from './Teamsbot.module.css';
|
||||
|
||||
|
|
@ -36,8 +37,8 @@ export const TeamsbotSettingsView: React.FC = () => {
|
|||
// Form state
|
||||
const [formData, setFormData] = useState<ConfigUpdateRequest>({});
|
||||
|
||||
// Dynamic voice data from Google TTS API
|
||||
const [languages, setLanguages] = useState<string[]>([]);
|
||||
// Voice catalog (single source of truth) + dynamic voices for the selected language
|
||||
const [languages, setLanguages] = useState<VoiceLanguage[]>([]);
|
||||
const [voices, setVoices] = useState<VoiceOption[]>([]);
|
||||
const [loadingVoices, setLoadingVoices] = useState(false);
|
||||
|
||||
|
|
@ -247,19 +248,13 @@ export const TeamsbotSettingsView: React.FC = () => {
|
|||
value={formData.language || 'de-DE'}
|
||||
onChange={(e) => _handleLanguageChange(e.target.value)}
|
||||
>
|
||||
{languages.length > 0 ? (
|
||||
languages.map((langCode, idx) => (
|
||||
<option key={`${langCode}-${idx}`} value={langCode}>{langCode}</option>
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
<option value="de-DE">{t('Deutsch (Deutschland)')}</option>
|
||||
<option value="en-US">{t('Englisch (US)')}</option>
|
||||
<option value="fr-FR">Francais</option>
|
||||
</>
|
||||
)}
|
||||
{languages.map(lang => (
|
||||
<option key={lang.bcp47} value={lang.bcp47}>
|
||||
{lang.flag ? `${lang.flag} ` : ''}{lang.label} ({lang.bcp47})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span className={styles.hint}>Sprache fuer Captions und Sprachausgabe ({languages.length} Sprachen verfuegbar)</span>
|
||||
<span className={styles.hint}>{t('Sprache für Captions und Sprachausgabe')} ({languages.length} {t('Sprachen verfügbar')})</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
|
|
|
|||
|
|
@ -11,21 +11,7 @@ import { useVoiceStream } from '../../../hooks/useSpeechAudioCapture';
|
|||
import type { WorkspaceFile, DataSource, FeatureDataSource } from './useWorkspace';
|
||||
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
const _STT_LANGUAGES = [
|
||||
{ code: 'de-DE', label: 'Deutsch' },
|
||||
{ code: 'en-US', label: 'English (US)' },
|
||||
{ code: 'en-GB', label: 'English (UK)' },
|
||||
{ code: 'fr-FR', label: 'Francais' },
|
||||
{ code: 'it-IT', label: 'Italiano' },
|
||||
{ code: 'es-ES', label: 'Espanol' },
|
||||
{ code: 'pt-BR', label: 'Portugues' },
|
||||
{ code: 'nl-NL', label: 'Nederlands' },
|
||||
{ code: 'pl-PL', label: 'Polski' },
|
||||
{ code: 'ru-RU', label: 'Russkij' },
|
||||
{ code: 'ja-JP', label: 'Japanese' },
|
||||
{ code: 'zh-CN', label: 'Chinese' },
|
||||
];
|
||||
import { useVoiceCatalog } from '../../../contexts/VoiceCatalogContext';
|
||||
|
||||
interface PendingFile {
|
||||
fileId: string;
|
||||
|
|
@ -88,6 +74,7 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({ instanceId: _ins
|
|||
onDraftAppendConsumed,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const { languages: voiceCatalogLanguages } = useVoiceCatalog();
|
||||
const [prompt, setPrompt] = useState('');
|
||||
const [showAutocomplete, setShowAutocomplete] = useState(false);
|
||||
const [autocompleteFilter, setAutocompleteFilter] = useState('');
|
||||
|
|
@ -617,23 +604,23 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({ instanceId: _ins
|
|||
borderRadius: 8, boxShadow: '0 -2px 8px rgba(0,0,0,0.1)', zIndex: 20,
|
||||
maxHeight: 240, overflowY: 'auto', minWidth: 160,
|
||||
}}>
|
||||
{_STT_LANGUAGES.map(lang => (
|
||||
{voiceCatalogLanguages.map(lang => (
|
||||
<div
|
||||
key={lang.code}
|
||||
key={lang.bcp47}
|
||||
onClick={() => {
|
||||
setVoiceLanguage(lang.code);
|
||||
setVoiceLanguage(lang.bcp47);
|
||||
setShowLangPicker(false);
|
||||
fetch('/api/voice/preferences', { method: 'PUT', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sttLanguage: lang.code }) }).catch(() => {});
|
||||
fetch('/api/voice/preferences', { method: 'PUT', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sttLanguage: lang.bcp47 }) }).catch(() => {});
|
||||
}}
|
||||
style={{
|
||||
padding: '8px 12px', cursor: 'pointer', fontSize: 13,
|
||||
background: lang.code === voiceLanguage ? 'var(--primary-color, #F25843)' : 'transparent',
|
||||
color: lang.code === voiceLanguage ? '#fff' : 'var(--text-primary, #333)',
|
||||
background: lang.bcp47 === voiceLanguage ? 'var(--primary-color, #F25843)' : 'transparent',
|
||||
color: lang.bcp47 === voiceLanguage ? '#fff' : 'var(--text-primary, #333)',
|
||||
}}
|
||||
onMouseEnter={e => { if (lang.code !== voiceLanguage) e.currentTarget.style.background = 'var(--bg-secondary, #f5f5f5)'; }}
|
||||
onMouseLeave={e => { if (lang.code !== voiceLanguage) e.currentTarget.style.background = ''; }}
|
||||
onMouseEnter={e => { if (lang.bcp47 !== voiceLanguage) e.currentTarget.style.background = 'var(--bg-secondary, #f5f5f5)'; }}
|
||||
onMouseLeave={e => { if (lang.bcp47 !== voiceLanguage) e.currentTarget.style.background = ''; }}
|
||||
>
|
||||
{lang.label} ({lang.code})
|
||||
{lang.flag ? `${lang.flag} ` : ''}{lang.label} ({lang.bcp47})
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue