feat: 管理界面报刊风统一改造 — 移除独立暗色主题,预测/回测/监控/配置页增强
This commit is contained in:
@@ -1,24 +1,24 @@
|
||||
/**
|
||||
* Admin 后台 - LLM 配置管理页面
|
||||
* Admin 后台 - LLM 配置管理页面(报刊风)
|
||||
*
|
||||
* 功能:
|
||||
* - 显示当前 LLM 配置(provider, model, base_url)
|
||||
* - 测试 LLM 连接(调用 /predict 测试)
|
||||
* - 显示 LLM 使用统计(预测次数、平均延迟、成功率)
|
||||
* - 模型切换(显示可用模型列表)
|
||||
* - 显示当前 LLM 配置(provider, model, base_url;后端暂无配置端点,当前值取自 .env 约定)
|
||||
* - 测试 LLM 连接(会真实调用一次 /predict,产生 LLM 调用费用)
|
||||
* - 显示 LLM 使用统计(从预测记录聚合)
|
||||
* - 可用模型列表
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { testLLMConnection, fetchLLMUsageStats } from '../dal'
|
||||
import type { LLMUsageStats } from '../types'
|
||||
import { Card, CardBody, CardHeader, Badge, SectionHeader } from '../components'
|
||||
import { Card, CardBody, CardHeader, Badge, SectionHeader, Alert, Spinner, SkeletonBlock } from '../components'
|
||||
|
||||
// 可用模型列表
|
||||
const AVAILABLE_MODELS = [
|
||||
{ id: 'gpt-4o', label: 'GPT-4o', provider: 'openai', description: '最强大模型,适合复杂分析' },
|
||||
{ id: 'gpt-4o', label: 'GPT-4o', provider: 'openai', description: '综合能力最强,适合复杂分析' },
|
||||
{ id: 'gpt-4o-mini', label: 'GPT-4o Mini', provider: 'openai', description: '快速经济,适合批量预测' },
|
||||
{ id: 'claude-3-5-sonnet', label: 'Claude 3.5 Sonnet', provider: 'anthropic', description: '长上下文分析能力强' },
|
||||
{ id: 'deepseek-chat', label: 'DeepSeek V3', provider: 'deepseek', description: '高性价比中文优化' },
|
||||
{ id: 'deepseek-chat', label: 'DeepSeek V3', provider: 'deepseek', description: '高性价比,中文优化' },
|
||||
]
|
||||
|
||||
export default function LLMConfigPage() {
|
||||
@@ -27,7 +27,7 @@ export default function LLMConfigPage() {
|
||||
const [testing, setTesting] = useState(false)
|
||||
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null)
|
||||
|
||||
// 当前配置(模拟,后端暂无配置端点)
|
||||
// 当前配置(后端暂无配置端点,取 .env 约定值展示)
|
||||
const currentConfig = {
|
||||
provider: 'openai',
|
||||
model: 'gpt-4o',
|
||||
@@ -68,62 +68,51 @@ export default function LLMConfigPage() {
|
||||
<div className="space-y-6">
|
||||
<SectionHeader
|
||||
title="LLM 配置"
|
||||
description="管理大语言模型连接与使用统计"
|
||||
description="大语言模型连接状态与使用统计。模型切换通过修改 .env 并重启服务完成。"
|
||||
/>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
{/* 当前配置 */}
|
||||
<Card>
|
||||
<CardHeader title="当前配置" />
|
||||
<CardBody className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between border-b border-gray-800 pb-3">
|
||||
<span className="text-xs text-gray-500">提供商</span>
|
||||
<Badge status="info">{currentConfig.provider}</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b border-gray-800 pb-3">
|
||||
<span className="text-xs text-gray-500">模型</span>
|
||||
<span className="text-sm text-gray-200">{currentConfig.model}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b border-gray-800 pb-3">
|
||||
<span className="text-xs text-gray-500">API 地址</span>
|
||||
<span className="text-xs font-mono text-gray-400">{currentConfig.base_url}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b border-gray-800 pb-3">
|
||||
<span className="text-xs text-gray-500">API Key</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-xs text-gray-400">{currentConfig.api_key_masked}</span>
|
||||
<Badge status={currentConfig.api_key_configured ? 'success' : 'failed'}>
|
||||
{currentConfig.api_key_configured ? '已配置' : '未配置'}
|
||||
</Badge>
|
||||
<CardBody>
|
||||
<div className="space-y-0">
|
||||
{[
|
||||
{ label: '提供商', value: currentConfig.provider, mono: false },
|
||||
{ label: '模型', value: currentConfig.model, mono: true },
|
||||
{ label: 'API 地址', value: currentConfig.base_url, mono: true },
|
||||
{ label: 'API Key', value: currentConfig.api_key_masked, mono: true },
|
||||
{ label: '模式', value: '多专家 (5 路 + 终裁)', mono: false },
|
||||
].map(row => (
|
||||
<div
|
||||
key={row.label}
|
||||
className="flex items-center justify-between gap-4 border-b border-ink-200 py-2.5 last:border-b-0"
|
||||
>
|
||||
<span className="text-xs text-ink-400">{row.label}</span>
|
||||
<span className={`text-xs text-ink-800 ${row.mono ? 'break-all font-mono' : ''}`}>
|
||||
{row.value}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-gray-500">模式</span>
|
||||
<span className="text-sm text-gray-200">多 Agent (5 专家 + 终裁)</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 测试连接 */}
|
||||
{testResult && (
|
||||
<div
|
||||
className={`rounded p-3 text-xs ${
|
||||
testResult.success
|
||||
? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/30'
|
||||
: 'bg-red-500/10 text-red-400 border border-red-500/30'
|
||||
}`}
|
||||
>
|
||||
{testResult.message}
|
||||
<div className="mt-4">
|
||||
<Alert
|
||||
kind={testResult.success ? 'ok' : 'error'}
|
||||
title={testResult.success ? '连接正常' : '连接失败'}
|
||||
message={testResult.success ? undefined : testResult.message}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleTest}
|
||||
disabled={testing}
|
||||
className="w-full rounded-md border border-gray-700 bg-gray-800 px-4 py-2.5 text-sm text-gray-300 transition-colors hover:bg-gray-700 hover:text-white disabled:opacity-50 min-h-[44px]"
|
||||
>
|
||||
{testing ? '测试中...' : '测试 LLM 连接'}
|
||||
<button onClick={handleTest} disabled={testing} className="btn btn-sm mt-4 w-full">
|
||||
{testing ? (<><Spinner /> 测试中</>) : '测试 LLM 连接'}
|
||||
</button>
|
||||
<p className="mt-2 text-center text-2xs text-ink-400">
|
||||
测试会真实调用一次单次模式预测,产生 LLM 费用。
|
||||
</p>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
@@ -131,126 +120,110 @@ export default function LLMConfigPage() {
|
||||
<Card>
|
||||
<CardHeader
|
||||
title="使用统计"
|
||||
description="从最近预测记录聚合"
|
||||
action={
|
||||
<button
|
||||
onClick={loadStats}
|
||||
className="rounded px-2 py-1 text-xs text-blue-400 hover:bg-gray-800 min-h-[44px] min-w-[44px]"
|
||||
>
|
||||
刷新
|
||||
<button onClick={loadStats} disabled={loading} className="btn btn-sm">
|
||||
{loading ? (<><Spinner /> 加载中</>) : '刷新'}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<CardBody>
|
||||
{loading ? (
|
||||
<div className="animate-pulse space-y-3">
|
||||
<div className="h-16 rounded bg-gray-800" />
|
||||
<div className="h-16 rounded bg-gray-800" />
|
||||
<div className="h-16 rounded bg-gray-800" />
|
||||
<div className="space-y-3">
|
||||
<SkeletonBlock className="h-16 w-full" />
|
||||
<SkeletonBlock className="h-16 w-full" />
|
||||
</div>
|
||||
) : stats ? (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="rounded-lg border border-gray-800 bg-gray-800/50 p-3 text-center">
|
||||
<div className="text-xl font-bold text-white tabular-nums">{stats.total_predictions}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">总预测数</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-800 bg-gray-800/50 p-3 text-center">
|
||||
<div className="text-xl font-bold text-blue-400 tabular-nums">{stats.avg_latency_ms}ms</div>
|
||||
<div className="text-xs text-gray-500 mt-1">平均延迟</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="border-t-2 border-ink-900 pt-3 text-center">
|
||||
<div className="font-serif text-2xl font-bold tabular-nums text-ink-900">
|
||||
{stats.total_predictions}
|
||||
</div>
|
||||
<div className="mt-1 text-2xs text-ink-400">总预测数</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-800 bg-gray-800/50 p-3 text-center">
|
||||
<div className="text-xl font-bold text-emerald-400 tabular-nums">
|
||||
{stats.success_rate.toFixed(1)}%
|
||||
<div className="border-t-2 border-ink-900 pt-3 text-center">
|
||||
<div className="font-serif text-2xl font-bold tabular-nums text-ink-900">
|
||||
{stats.avg_latency_ms > 0 ? `${(stats.avg_latency_ms / 1000).toFixed(1)}s` : '—'}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">成功率</div>
|
||||
<div className="mt-1 text-2xs text-ink-400">平均延迟</div>
|
||||
</div>
|
||||
<div className="border-t-2 border-press pt-3 text-center">
|
||||
<div className="font-serif text-2xl font-bold tabular-nums text-press">
|
||||
{stats.success_rate.toFixed(0)}%
|
||||
</div>
|
||||
<div className="mt-1 text-2xs text-ink-400">有效率</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-sm text-gray-500">暂无使用统计数据</div>
|
||||
<p className="py-6 text-center text-xs text-ink-400">暂无使用统计数据</p>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 模型切换 */}
|
||||
{/* 可用模型 */}
|
||||
<Card>
|
||||
<CardHeader title="可用模型" description="切换预测使用的 LLM 模型(通过修改 .env 文件)" />
|
||||
<CardBody>
|
||||
<div className="space-y-3">
|
||||
{AVAILABLE_MODELS.map(model => {
|
||||
const isCurrent = model.id === currentConfig.model
|
||||
return (
|
||||
<div
|
||||
key={model.id}
|
||||
className={`flex flex-col sm:flex-row sm:items-center justify-between rounded-lg border p-4 gap-3 ${
|
||||
isCurrent
|
||||
? 'border-blue-500/30 bg-blue-500/5'
|
||||
: 'border-gray-800 hover:border-gray-700'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-200">{model.label}</span>
|
||||
{isCurrent && <Badge status="success">当前</Badge>}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-0.5">{model.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<CardHeader title="可用模型" description="在 .env 中修改 LLM_MODEL 后重启服务生效" />
|
||||
<CardBody className="px-0 sm:px-0">
|
||||
{AVAILABLE_MODELS.map(model => {
|
||||
const isCurrent = model.id === currentConfig.model
|
||||
return (
|
||||
<div
|
||||
key={model.id}
|
||||
className={`flex flex-col gap-2 border-b border-ink-200 px-4 py-3 last:border-b-0 sm:flex-row sm:items-center sm:justify-between sm:px-5 ${
|
||||
isCurrent ? 'bg-press-wash/50' : ''
|
||||
}`}
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge status="info">{model.provider}</Badge>
|
||||
{!isCurrent && (
|
||||
<span className="text-xs text-gray-500 whitespace-nowrap">
|
||||
编辑 .env 切换
|
||||
</span>
|
||||
)}
|
||||
<span className="text-sm font-medium text-ink-900">{model.label}</span>
|
||||
{isCurrent && <Badge status="success">当前</Badge>}
|
||||
</div>
|
||||
<p className="mt-0.5 text-2xs text-ink-500">{model.description}</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-mono text-2xs text-ink-400">{model.provider}</span>
|
||||
{!isCurrent && <span className="text-2xs text-ink-400">编辑 .env 切换</span>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
{/* 最近预测 */}
|
||||
<Card>
|
||||
<CardHeader title="最近预测记录" />
|
||||
<CardBody>
|
||||
<CardBody className="px-0 sm:px-0">
|
||||
{loading ? (
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-2 px-4 sm:px-5">
|
||||
{[1, 2, 3].map(i => (
|
||||
<div key={i} className="h-12 animate-pulse rounded bg-gray-800" />
|
||||
<SkeletonBlock key={i} className="h-10 w-full" />
|
||||
))}
|
||||
</div>
|
||||
) : stats && stats.recent_predictions.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
{stats.recent_predictions.map(p => (
|
||||
<div
|
||||
key={p.id}
|
||||
className="flex flex-col sm:flex-row sm:items-center justify-between rounded-lg border border-gray-800 p-3 gap-2"
|
||||
className="flex flex-col gap-1.5 border-b border-ink-200 px-4 py-2.5 last:border-b-0 sm:flex-row sm:items-center sm:justify-between sm:px-5"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-gray-500">#{p.id}</span>
|
||||
<span className="text-sm text-gray-300">Match #{p.match_id}</span>
|
||||
<span className="text-xs font-mono text-gray-500">{p.model}</span>
|
||||
<div className="flex items-baseline gap-3">
|
||||
<span className="text-2xs tabular-nums text-ink-400">#{p.id}</span>
|
||||
<span className="text-xs text-ink-800">比赛 #{p.match_id}</span>
|
||||
<span className="font-mono text-2xs text-ink-500">{p.model}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500">
|
||||
{p.created_at ? new Date(p.created_at).toLocaleString() : '—'}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-2xs tabular-nums text-ink-400">
|
||||
{p.created_at ? new Date(p.created_at).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '—'}
|
||||
</span>
|
||||
<Badge status={p.status === 'success' ? 'success' : 'failed'}>
|
||||
{p.status === 'success' ? '成功' : '失败'}
|
||||
</Badge>
|
||||
{p.status === 'success' ? <Badge status="success">成功</Badge> : <Badge status="error">失败</Badge>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-sm text-gray-500">
|
||||
<p>暂无预测记录</p>
|
||||
<p className="mt-1 text-xs">触发预测后将在此显示记录</p>
|
||||
</div>
|
||||
<p className="py-8 text-center text-xs text-ink-400">暂无预测记录</p>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user