import { useEffect, useMemo, useState } from 'react' import clsx from 'clsx' import { useI18n } from '../../i18n' import { useApiTesterState } from './useApiTesterState' import { useChatStreamClient } from './useChatStreamClient' import ConfigPanel from './ConfigPanel' import ChatPanel from './ChatPanel' function describeModel(t, modelID) { const noThinking = modelID.endsWith('-nothinking') let description = t('apiTester.models.generic') if (modelID.includes('vision')) { description = t('apiTester.models.vision') } else if (modelID.includes('pro-search')) { description = t('apiTester.models.proSearch') } else if (modelID.includes('pro')) { description = t('apiTester.models.pro') } else if (modelID.includes('flash-search')) { description = t('apiTester.models.flashSearch') } else if (modelID.includes('flash')) { description = t('apiTester.models.flash') } if (noThinking) { return `${description} ยท ${t('apiTester.models.noThinking')}` } return description } function decorateModel(t, modelID) { const isVision = modelID.includes('vision') const isSearch = modelID.includes('search') const isPro = modelID.includes('pro') if (isVision && isSearch) { return { id: modelID, name: modelID, icon: 'ImageIcon', desc: describeModel(t, modelID), color: 'text-fuchsia-600', } } if (isVision) { return { id: modelID, name: modelID, icon: 'ImageIcon', desc: describeModel(t, modelID), color: 'text-violet-500', } } if (isSearch) { return { id: modelID, name: modelID, icon: 'SearchIcon', desc: describeModel(t, modelID), color: isPro ? 'text-cyan-600' : 'text-cyan-500', } } return { id: modelID, name: modelID, icon: isPro ? 'Cpu' : 'MessageSquare', desc: describeModel(t, modelID), color: isPro ? 'text-amber-600' : 'text-amber-500', } } export default function ApiTesterContainer({ config, onMessage, authFetch }) { const { t } = useI18n() const [availableModelIDs, setAvailableModelIDs] = useState([]) const [modelsLoaded, setModelsLoaded] = useState(false) const { model, setModel, message, setMessage, attachedFiles, setAttachedFiles, apiKey, setApiKey, selectedAccount, setSelectedAccount, response, setResponse, loading, setLoading, streamingContent, setStreamingContent, streamingThinking, setStreamingThinking, isStreaming, setIsStreaming, streamingMode, setStreamingMode, configExpanded, setConfigExpanded, abortControllerRef, } = useApiTesterState({ t }) const accounts = config.accounts || [] const resolveAccountIdentifier = (acc) => { if (!acc || typeof acc !== 'object') return '' return String(acc.identifier || acc.email || acc.mobile || '').trim() } const configuredKeys = config.keys || [] const trimmedApiKey = apiKey.trim() const defaultKey = configuredKeys[0] || '' const effectiveKey = trimmedApiKey || defaultKey const customKeyActive = trimmedApiKey !== '' const customKeyManaged = customKeyActive && configuredKeys.includes(trimmedApiKey) useEffect(() => { let disposed = false async function loadModels() { try { const res = await authFetch('/v1/models') if (!res.ok) { throw new Error(`failed to fetch models: ${res.status}`) } const data = await res.json() const modelIDs = Array.isArray(data?.data) ? data.data .map((item) => String(item?.id || '').trim()) .filter(Boolean) : [] if (!disposed) { setAvailableModelIDs(modelIDs) } } catch (_err) { if (!disposed) { setAvailableModelIDs([]) } } finally { if (!disposed) { setModelsLoaded(true) } } } setModelsLoaded(false) loadModels() return () => { disposed = true } }, [authFetch]) const models = useMemo( () => availableModelIDs.map((modelID) => decorateModel(t, modelID)), [availableModelIDs, t] ) useEffect(() => { if (!models.length) { if (model) { setModel('') } return } if (!model || !models.some((item) => item.id === model)) { setModel(models[0].id) } }, [model, models, setModel]) const { runTest, stopGeneration } = useChatStreamClient({ t, onMessage, model, message, effectiveKey, selectedAccount, streamingMode, attachedFiles, abortControllerRef, setLoading, setIsStreaming, setResponse, setStreamingContent, setStreamingThinking, }) return (
0} />
) }