94 lines
3.9 KiB
TypeScript
94 lines
3.9 KiB
TypeScript
"use client";
|
||
|
||
import Link from "next/link";
|
||
import { useEffect, useState } from "react";
|
||
import { AlertTriangle, CheckCircle2, Database, FileQuestion, Library, Play, Rows3, Timer } from "lucide-react";
|
||
import { api, formatDateTime } from "@/lib/api";
|
||
import type { DashboardSummary, TaskRun } from "@/lib/types";
|
||
import StatCard from "@/components/StatCard";
|
||
import StatusBadge from "@/components/StatusBadge";
|
||
|
||
export default function DashboardPage() {
|
||
const [summary, setSummary] = useState<DashboardSummary | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [running, setRunning] = useState(false);
|
||
|
||
async function load() {
|
||
setLoading(true);
|
||
const data = await api.get<DashboardSummary>("/dashboard/summary");
|
||
setSummary(data);
|
||
setLoading(false);
|
||
}
|
||
|
||
async function runTask() {
|
||
setRunning(true);
|
||
await api.post<TaskRun>("/tasks/run-now", {});
|
||
await load();
|
||
setRunning(false);
|
||
}
|
||
|
||
useEffect(() => {
|
||
load().catch(() => setLoading(false));
|
||
}, []);
|
||
|
||
if (loading || !summary) {
|
||
return <div className="text-sm text-slate-500">加载中...</div>;
|
||
}
|
||
|
||
const actions = [
|
||
{ href: "/review", label: "去审核" },
|
||
{ href: "/high-risk", label: "查看高风险" },
|
||
{ href: "/standard-qa", label: "查看标准问答库" },
|
||
{ href: "/logs", label: "查看运行日志" }
|
||
];
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||
<div>
|
||
<h1 className="text-2xl font-semibold text-ink">仪表盘</h1>
|
||
<p className="mt-1 text-sm text-slate-500">最近任务:{summary.last_task_time ? formatDateTime(summary.last_task_time) : "-"}</p>
|
||
</div>
|
||
<button
|
||
onClick={runTask}
|
||
disabled={running}
|
||
className="inline-flex h-10 items-center gap-2 rounded-md bg-jade px-4 text-sm font-medium text-white hover:bg-teal-800 disabled:opacity-60"
|
||
>
|
||
<Play className="h-4 w-4" aria-hidden />
|
||
{running ? "处理中" : "手动触发处理任务"}
|
||
</button>
|
||
</div>
|
||
|
||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||
<StatCard title="昨晚处理答疑场次数" value={summary.processed_sessions} icon={<Timer className="h-5 w-5" />} />
|
||
<StatCard title="已拆解问答总数" value={summary.pending_review_count + summary.approved_qa_count} icon={<Rows3 className="h-5 w-5" />} tone="indigo" />
|
||
<StatCard title="待审核问答数" value={summary.pending_review_count} icon={<FileQuestion className="h-5 w-5" />} tone="amber" />
|
||
<StatCard title="高风险问答数" value={summary.high_risk_count} icon={<AlertTriangle className="h-5 w-5" />} tone="rose" />
|
||
<StatCard title="已审核问答数" value={summary.approved_qa_count} icon={<CheckCircle2 className="h-5 w-5" />} />
|
||
<StatCard title="标准问答数" value={summary.standard_qa_count} icon={<Library className="h-5 w-5" />} tone="indigo" />
|
||
<StatCard title="可被智能体调用问答数" value={summary.callable_qa_count} icon={<Database className="h-5 w-5" />} />
|
||
<div className="rounded-md border border-line bg-white p-4">
|
||
<div className="text-sm text-slate-500">最近一次任务状态</div>
|
||
<div className="mt-3">
|
||
{summary.last_task_status ? <StatusBadge status={summary.last_task_status} /> : <span className="text-sm text-slate-400">暂无任务</span>}
|
||
</div>
|
||
{summary.last_task_error ? <p className="mt-3 text-sm text-rose-700">{summary.last_task_error}</p> : null}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex flex-wrap gap-2">
|
||
{actions.map((action) => (
|
||
<Link
|
||
key={action.href}
|
||
href={action.href}
|
||
className="inline-flex h-10 items-center rounded-md border border-line bg-white px-4 text-sm font-medium text-ink hover:bg-slate-50"
|
||
>
|
||
{action.label}
|
||
</Link>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|