chore: update project files

This commit is contained in:
CJACK
2026-04-27 02:09:11 +08:00
parent 40d5e3ebb5
commit 90ce595325
27 changed files with 511 additions and 150 deletions

View File

@@ -1,3 +1,4 @@
import { useEffect, useMemo, useState } from 'react'
import clsx from 'clsx'
import { useI18n } from '../../i18n'
@@ -6,8 +7,75 @@ 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-search')) {
description = t('apiTester.models.visionSearch')
} else 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,
@@ -49,14 +117,58 @@ export default function ApiTesterContainer({ config, onMessage, authFetch }) {
const customKeyActive = trimmedApiKey !== ''
const customKeyManaged = customKeyActive && configuredKeys.includes(trimmedApiKey)
const models = [
{ id: 'deepseek-v4-flash', name: 'deepseek-v4-flash', icon: 'MessageSquare', desc: t('apiTester.models.flash'), color: 'text-amber-500' },
{ id: 'deepseek-v4-pro', name: 'deepseek-v4-pro', icon: 'Cpu', desc: t('apiTester.models.pro'), color: 'text-amber-600' },
{ id: 'deepseek-v4-flash-search', name: 'deepseek-v4-flash-search', icon: 'SearchIcon', desc: t('apiTester.models.flashSearch'), color: 'text-cyan-500' },
{ id: 'deepseek-v4-pro-search', name: 'deepseek-v4-pro-search', icon: 'SearchIcon', desc: t('apiTester.models.proSearch'), color: 'text-cyan-600' },
{ id: 'deepseek-v4-vision', name: 'deepseek-v4-vision', icon: 'ImageIcon', desc: t('apiTester.models.vision'), color: 'text-violet-500' },
{ id: 'deepseek-v4-vision-search', name: 'deepseek-v4-vision-search', icon: 'SearchIcon', desc: t('apiTester.models.visionSearch'), color: 'text-fuchsia-600' },
]
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,
@@ -84,6 +196,7 @@ export default function ApiTesterContainer({ config, onMessage, authFetch }) {
models={models}
model={model}
setModel={setModel}
modelsLoaded={modelsLoaded}
streamingMode={streamingMode}
setStreamingMode={setStreamingMode}
selectedAccount={selectedAccount}
@@ -114,6 +227,7 @@ export default function ApiTesterContainer({ config, onMessage, authFetch }) {
streamingContent={streamingContent}
onRunTest={runTest}
onStopGeneration={stopGeneration}
hasAvailableModel={models.length > 0}
/>
</div>
)

View File

@@ -21,6 +21,7 @@ export default function ChatPanel({
streamingContent,
onRunTest,
onStopGeneration,
hasAvailableModel,
}) {
const fileInputRef = useRef(null)
const [uploadingFiles, setUploadingFiles] = useState(false)
@@ -181,7 +182,7 @@ export default function ChatPanel({
<div className="absolute left-2 bottom-2 z-10">
<button
onClick={() => fileInputRef.current?.click()}
disabled={uploadingFiles || isStreaming}
disabled={uploadingFiles || isStreaming || !hasAvailableModel}
className="p-2 text-muted-foreground hover:text-primary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="Attach files"
>
@@ -189,11 +190,12 @@ export default function ChatPanel({
</button>
</div>
<textarea
className="w-full bg-[#09090b] border border-border rounded-xl pl-12 pr-12 py-3 text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all resize-none custom-scrollbar placeholder:text-muted-foreground/50 text-foreground shadow-inner"
placeholder={t('apiTester.enterMessage')}
className="w-full bg-[#09090b] border border-border rounded-xl pl-12 pr-12 py-3 text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all resize-none custom-scrollbar placeholder:text-muted-foreground/50 text-foreground shadow-inner disabled:opacity-60 disabled:cursor-not-allowed"
placeholder={hasAvailableModel ? t('apiTester.enterMessage') : t('apiTester.noModelsMessagePlaceholder')}
rows={1}
style={{ minHeight: '52px' }}
value={message}
disabled={!hasAvailableModel}
onChange={e => setMessage(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter' && !e.shiftKey) {
@@ -212,7 +214,7 @@ export default function ChatPanel({
) : (
<button
onClick={onRunTest}
disabled={loading || uploadingFiles || (!message.trim() && attachedFiles.length === 0)}
disabled={loading || uploadingFiles || !hasAvailableModel || (!message.trim() && attachedFiles.length === 0)}
className="p-2 text-primary hover:text-primary/80 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}

View File

@@ -19,6 +19,7 @@ export default function ConfigPanel({
models,
model,
setModel,
modelsLoaded,
streamingMode,
setStreamingMode,
selectedAccount,
@@ -43,6 +44,7 @@ export default function ConfigPanel({
const selectedModel = models.find(m => m.id === model) || models[0]
const SelectedModelIcon = selectedModel ? (iconMap[selectedModel.icon] || MessageSquare) : MessageSquare
const defaultKeyPreview = maskSecret(config.keys?.[0])
const hasModels = models.length > 0
return (
<div className={clsx(
@@ -73,19 +75,24 @@ export default function ConfigPanel({
<label className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider ml-0.5">{t('apiTester.modelLabel')}</label>
<div className="relative">
<select
className="w-full h-11 pl-3 pr-9 bg-secondary border border-border rounded-lg text-sm appearance-none focus:outline-none focus:ring-1 focus:ring-ring focus:border-ring transition-all cursor-pointer hover:bg-muted/70 text-foreground"
className="w-full h-11 pl-3 pr-9 bg-secondary border border-border rounded-lg text-sm appearance-none focus:outline-none focus:ring-1 focus:ring-ring focus:border-ring transition-all cursor-pointer hover:bg-muted/70 text-foreground disabled:opacity-60 disabled:cursor-not-allowed"
value={model}
onChange={e => setModel(e.target.value)}
disabled={!hasModels}
>
{models.map(m => (
{hasModels ? models.map(m => (
<option key={m.id} value={m.id} className="bg-popover text-popover-foreground">
{m.name}
</option>
))}
)) : (
<option value="" className="bg-popover text-popover-foreground">
{modelsLoaded ? t('apiTester.noModels') : t('apiTester.loadingModels')}
</option>
)}
</select>
<ChevronDown className="absolute right-2.5 top-3.5 w-4 h-4 text-muted-foreground pointer-events-none" />
</div>
{selectedModel && (
{selectedModel ? (
<div className="mt-3 rounded-lg border border-border bg-muted/20 p-3">
<div className="flex items-start gap-3">
<div className={clsx(
@@ -107,6 +114,10 @@ export default function ConfigPanel({
{t('apiTester.modelPickerHint')}
</p>
</div>
) : (
<div className="mt-3 rounded-lg border border-dashed border-border bg-muted/10 p-3 text-[11px] text-muted-foreground leading-relaxed">
{modelsLoaded ? t('apiTester.noModelsHint') : t('apiTester.loadingModelsHint')}
</div>
)}
</div>

View File

@@ -224,7 +224,9 @@
"flashSearch": "v4 Flash (with search)",
"proSearch": "v4 Pro (with search)",
"vision": "v4 Vision (thinking on by default)",
"visionSearch": "v4 Vision (with search)"
"visionSearch": "v4 Vision (with search)",
"generic": "Compatible model",
"noThinking": "thinking forced off"
},
"missingApiKey": "Please provide an API key.",
"requestFailed": "Request failed.",
@@ -234,6 +236,11 @@
"config": "Configuration",
"modelLabel": "Model",
"modelPickerHint": "Use the dropdown to pick a model. The list scrolls automatically.",
"loadingModels": "Loading models...",
"loadingModelsHint": "Fetching the available model list from /v1/models.",
"noModels": "No models available",
"noModelsHint": "The /v1/models endpoint did not return any usable models. Check the backend configuration or API status.",
"noModelsMessagePlaceholder": "No models are available right now, so the tester cannot send a request.",
"streamMode": "Streaming",
"accountSelector": "Account",
"autoRandom": "🤖 Auto / Random",

View File

@@ -224,7 +224,9 @@
"flashSearch": "v4 Flash带搜索",
"proSearch": "v4 Pro带搜索",
"vision": "v4 Vision默认开启思考",
"visionSearch": "v4 Vision带搜索"
"visionSearch": "v4 Vision带搜索",
"generic": "兼容模型",
"noThinking": "强制关闭思考"
},
"missingApiKey": "请提供 API 密钥",
"requestFailed": "请求失败",
@@ -234,6 +236,11 @@
"config": "配置",
"modelLabel": "模型",
"modelPickerHint": "使用下拉列表选择模型,长列表会自动滚动。",
"loadingModels": "正在加载模型...",
"loadingModelsHint": "正在从 /v1/models 拉取可用模型列表。",
"noModels": "没有可用模型",
"noModelsHint": "/v1/models 当前没有返回任何可用模型,请先检查后端配置或接口状态。",
"noModelsMessagePlaceholder": "当前没有可用模型,暂时无法发起测试。",
"streamMode": "流式模式",
"accountSelector": "选择账号",
"autoRandom": "🤖 自动 / 随机",