import React, { useMemo, useState, useEffect } from 'react'; import Select, { components, StylesConfig, type SingleValue } from 'react-select'; import { matchSorter } from 'match-sorter'; import { LANGUAGE_MAP, ACCURATE_LANGUAGES } from '../utils/languages'; import { getScriptName, getScriptDescription } from '../utils/scripts'; import { getSupportedLanguages } from '../services/transcriptionApi'; interface LanguageSelectorProps { selectedLanguage: string | null; selectedScript: string | null; onLanguageAndScriptSelect: (language: string | null, script: string | null) => void; disabled?: boolean; } interface OptionType { value: string; // The full code_script combination label: string; languageName: string; scriptName: string; languageCode: string; scriptCode: string; } const parseLanguage = (languageString: string): OptionType | null => { const parts = languageString.split('_'); // Always expect format: "eng_Latn" if (parts.length === 2) { const [languageCode, scriptCode] = parts; const languageName = (LANGUAGE_MAP as Record)[languageCode] || languageCode; const scriptName = getScriptName(scriptCode); return { value: languageString, label: `${languageName} ${scriptName} (${languageString})`, languageName, scriptName, languageCode, scriptCode, }; } return null; }; // Custom Option component to show language, script, and code with tooltip const Option = (props: any) => { const scriptDescription = getScriptDescription(props.data.scriptCode); return (
{props.data.languageName}
{props.data.scriptName} ({props.data.value})
); }; // Custom SingleValue component for selected value const SingleValue = (props: any) => (
{props.data.languageName}
{props.data.scriptName} ({props.data.value})
); // Custom styles to match the dark theme const customStyles: StylesConfig = { control: (styles, { isDisabled, isFocused }) => ({ ...styles, backgroundColor: isDisabled ? '#374151' : '#374151', // gray-700 borderColor: isFocused ? '#3b82f6' : '#4b5563', // blue-500 : gray-600 borderRadius: '0.375rem', minHeight: '40px', boxShadow: isFocused ? '0 0 0 1px #3b82f6' : 'none', '&:hover': { borderColor: isDisabled ? '#4b5563' : '#6b7280', // gray-600 : gray-500 backgroundColor: isDisabled ? '#374151' : '#4b5563', // gray-700 : gray-600 }, cursor: isDisabled ? 'not-allowed' : 'pointer', }), singleValue: (styles) => ({ ...styles, color: '#f9fafb', // gray-50 }), placeholder: (styles) => ({ ...styles, color: '#9ca3af', // gray-400 }), input: (styles) => ({ ...styles, color: '#f9fafb', // gray-50 }), menu: (styles) => ({ ...styles, backgroundColor: '#374151', // gray-700 border: '1px solid #4b5563', // gray-600 borderRadius: '0.5rem', boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', zIndex: 50, }), menuList: (styles) => ({ ...styles, maxHeight: '200px', padding: 0, }), option: (styles, { isFocused, isSelected }) => ({ ...styles, backgroundColor: isSelected ? '#2563eb' // blue-600 : isFocused ? '#4b5563' // gray-600 : 'transparent', color: '#f9fafb', // gray-50 cursor: 'pointer', padding: '8px 12px', '&:hover': { backgroundColor: isSelected ? '#2563eb' : '#4b5563', // blue-600 : gray-600 }, }), indicatorSeparator: (styles) => ({ ...styles, backgroundColor: '#4b5563', // gray-600 }), dropdownIndicator: (styles, { isDisabled }) => ({ ...styles, color: isDisabled ? '#6b7280' : '#9ca3af', // gray-500 : gray-400 '&:hover': { color: isDisabled ? '#6b7280' : '#d1d5db', // gray-500 : gray-300 }, }), clearIndicator: (styles) => ({ ...styles, color: '#9ca3af', // gray-400 '&:hover': { color: '#d1d5db', // gray-300 }, }), noOptionsMessage: (styles) => ({ ...styles, color: '#9ca3af', // gray-400 }), }; const LanguageSelector: React.FC = ({ selectedLanguage, selectedScript, onLanguageAndScriptSelect, disabled = false }) => { const [supportedLanguages, setSupportedLanguages] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); // Fetch supported languages from API useEffect(() => { const fetchSupportedLanguages = async () => { try { setIsLoading(true); const languages = await getSupportedLanguages(); setSupportedLanguages(languages); } catch (err) { console.error('Failed to fetch supported languages:', err); setError('Failed to load supported languages'); } finally { setIsLoading(false); } }; fetchSupportedLanguages(); }, []); // Convert supported languages to options const languageOptions = useMemo(() => { const allowAllLanguages = import.meta.env.VITE_ALLOW_ALL_LANGUAGES === 'true'; return supportedLanguages .map(parseLanguage) .filter((option): option is OptionType => option !== null) .filter((option) => { if (allowAllLanguages) { return true; } return ACCURATE_LANGUAGES.includes(option.languageCode); }) .sort((a, b) => a.languageName.localeCompare(b.languageName)); }, [supportedLanguages]); // Find the selected option const selectedOption = useMemo(() => { if (!selectedLanguage || !selectedScript) return null; const combinedValue = `${selectedLanguage}_${selectedScript}`; return languageOptions.find(option => option.value === combinedValue) || null; }, [selectedLanguage, selectedScript, languageOptions]); const handleChange = (newValue: SingleValue) => { if (newValue) { onLanguageAndScriptSelect(newValue.languageCode, newValue.scriptCode); } else { onLanguageAndScriptSelect(null, null); } }; // Custom filterOption function using match-sorter const filterOptions = useMemo(() => { return (option: { label: string; value: string; data: OptionType }, inputValue: string) => { if (!inputValue.trim()) return true; // Use match-sorter to check if this individual option matches const matches = matchSorter([option.data], inputValue, { keys: [ 'languageName', // Primary: language name 'scriptName', // Secondary: script name 'languageCode', // Tertiary: language code 'scriptCode', // Quaternary: script code 'label', // Fallback: full label ], threshold: matchSorter.rankings.CONTAINS, }); return matches.length > 0; }; }, []); if (error) { return (
{error}
); } return ( value={selectedOption} onChange={handleChange} options={languageOptions} placeholder={isLoading ? "Loading languages..." : "Select language..."} isClearable isDisabled={disabled || isLoading} isSearchable filterOption={(option, inputValue) => filterOptions(option, inputValue)} components={{ Option, SingleValue }} styles={customStyles} menuPortalTarget={document.body} menuPosition="fixed" noOptionsMessage={({ inputValue }) => `No languages found matching "${inputValue}"` } // Performance optimizations menuIsOpen={undefined} // Let react-select manage this blurInputOnSelect={true} closeMenuOnSelect={true} hideSelectedOptions={false} /> ); }; export default LanguageSelector;