feat: 管理界面报刊风统一改造 — 移除独立暗色主题,预测/回测/监控/配置页增强

This commit is contained in:
shangfangjian
2026-09-18 10:42:14 +08:00
parent 91e406f5ee
commit cf0751d1ff
15 changed files with 1275 additions and 828 deletions
+98 -125
View File
@@ -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>