mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-05 00:45:29 +08:00
chore: update project files
This commit is contained in:
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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" />}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "🤖 自动 / 随机",
|
||||
|
||||
Reference in New Issue
Block a user