| 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 ( |
| <div className={clsx('flex flex-col lg:grid lg:grid-cols-12 gap-6 h-[calc(100vh-140px)] min-h-0')}> |
| <ConfigPanel |
| t={t} |
| configExpanded={configExpanded} |
| setConfigExpanded={setConfigExpanded} |
| models={models} |
| model={model} |
| setModel={setModel} |
| modelsLoaded={modelsLoaded} |
| streamingMode={streamingMode} |
| setStreamingMode={setStreamingMode} |
| selectedAccount={selectedAccount} |
| setSelectedAccount={setSelectedAccount} |
| accounts={accounts} |
| resolveAccountIdentifier={resolveAccountIdentifier} |
| apiKey={apiKey} |
| setApiKey={setApiKey} |
| config={config} |
| customKeyActive={customKeyActive} |
| customKeyManaged={customKeyManaged} |
| /> |
| |
| <ChatPanel |
| t={t} |
| message={message} |
| setMessage={setMessage} |
| attachedFiles={attachedFiles} |
| setAttachedFiles={setAttachedFiles} |
| setSelectedAccount={setSelectedAccount} |
| effectiveKey={effectiveKey} |
| selectedAccount={selectedAccount} |
| model={model} |
| onMessage={onMessage} |
| response={response} |
| isStreaming={isStreaming} |
| loading={loading} |
| streamingThinking={streamingThinking} |
| streamingContent={streamingContent} |
| onRunTest={runTest} |
| onStopGeneration={stopGeneration} |
| hasAvailableModel={models.length > 0} |
| /> |
| </div> |
| ) |
| } |
|
|