Files
ds2api/webui/src/features/apiTester/ApiTesterContainer.jsx

234 lines
7.0 KiB
JavaScript

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>
)
}