assistant/console/components/AutoReview.tsx

148 lines
5.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from 'react';
import { Check, Play, Wand2, X } from 'lucide-react';
import { api, errorText } from '../api';
import { CATEGORY_LABEL, SCOPE_LABEL, type Scope } from '../types';
interface Plan {
id: number;
key: string;
value: string;
category: string;
school_count: number;
observations: number;
action: 'verify' | 'reject' | 'hold';
scope: Scope | null;
reason: string;
}
interface Result {
dry_run: boolean;
examined: number;
verified: number;
rejected: number;
held: number;
policy: { min_schools: number; min_observations: number; require_llm: boolean; auto_reject: boolean; max_actions: number };
plans: { verify: Plan[]; reject: Plan[]; hold: Plan[] };
}
function PlanList({ title, plans, tone }: { title: string; plans: Plan[]; tone: string }) {
if (plans.length === 0) return null;
return (
<div className="plan-group">
<div className={`plan-title ${tone}`}>{title} · {plans.length}</div>
<ul className="plan-list">
{plans.slice(0, 40).map(plan => (
<li key={plan.id}>
<span className="plan-key">{plan.key}</span>
<span className={`chip cat-${plan.category}`}>{CATEGORY_LABEL[plan.category] ?? plan.category}</span>
{plan.scope && <span className="chip">{SCOPE_LABEL[plan.scope]}</span>}
<span className="muted small">{plan.school_count} 校 · 观察 {plan.observations}</span>
<div className="plan-reason muted small">{plan.reason}</div>
</li>
))}
{plans.length > 40 && <li className="muted small">还有 {plans.length - 40} 条未列出</li>}
</ul>
</div>
);
}
export function AutoReview({ llmReady, onApplied, onMessage }: {
llmReady: boolean;
onApplied: () => void;
onMessage: (text: string) => void;
}) {
const [minSchools, setMinSchools] = useState(2);
const [minObservations, setMinObservations] = useState(3);
const [requireLlm, setRequireLlm] = useState(true);
const [autoReject, setAutoReject] = useState(true);
const [result, setResult] = useState<Result | null>(null);
const [busy, setBusy] = useState(false);
async function run(dryRun: boolean): Promise<void> {
setBusy(true);
try {
const body = {
dry_run: dryRun,
min_schools: minSchools,
min_observations: minObservations,
require_llm: requireLlm,
auto_reject: autoReject,
};
const data = await api.post<Result>('/api/entries/auto-review', body);
setResult(data);
if (!dryRun) {
onMessage(`已自动采纳 ${data.verified} 条、驳回 ${data.rejected} 条,留给人工 ${data.held} 条`);
onApplied();
}
} catch (error) {
onMessage(errorText(error));
} finally {
setBusy(false);
}
}
return (
<section className="panel auto-panel">
<h2>智能采纳</h2>
<p className="muted">
只对证据够硬的条目动手。<strong>系统规则和其他类永远留给人</strong>,因为一条错的规则会让 Agent 以后一直绕开正确操作。
自动驳回只是暂缓,那条知识再被观察到会退回队列;人工驳回才是终局。
{llmReady ? '模型已配置,会参与判断。' : '未配模型 Key,只用规则层,能自动处理的会少很多。'}
</p>
<div className="policy-row">
<label>
跨校门槛
<input type="number" min={1} max={20} value={minSchools}
onChange={event => setMinSchools(Number(event.target.value))} />
<span className="muted small">几所学校各自观察到就算成立</span>
</label>
<label>
单校观察
<input type="number" min={1} max={50} value={minObservations}
onChange={event => setMinObservations(Number(event.target.value))} />
<span className="muted small">只有一所学校时的观察次数门槛</span>
</label>
<label className="check">
<input type="checkbox" checked={requireLlm} disabled={!llmReady}
onChange={event => setRequireLlm(event.target.checked)} />
证据够也要模型点头
</label>
<label className="check">
<input type="checkbox" checked={autoReject} onChange={event => setAutoReject(event.target.checked)} />
自动清掉明显的垃圾
</label>
</div>
<div className="policy-actions">
<button type="button" className="secondary-button small" disabled={busy} onClick={() => { void run(true); }}>
<Wand2 size={14} />先看会动哪些
</button>
<button type="button" className="primary-button small" disabled={busy || !result || result.dry_run === false}
onClick={() => { void run(false); }}>
<Play size={14} />按这个计划执行
</button>
{busy && <span className="muted small">模型逐条判断中,几十条要半分钟左右…</span>}
</div>
{result && (
<div className="plan-result">
<p className="muted small">
{result.dry_run ? '干跑结果(还没改任何状态)' : '已执行'}:检查 {result.examined} 条 →
采纳 {result.verified}、驳回 {result.rejected}、留给人工 {result.held}
</p>
<PlanList title="自动采纳" plans={result.plans.verify} tone="ok" />
<PlanList title="自动驳回(暂缓)" plans={result.plans.reject} tone="danger" />
<PlanList title="留给人工" plans={result.plans.hold} tone="warn" />
</div>
)}
<p className="muted small">
<Check size={12} /> 自动处理过的条目会标记来源为 auto,在队列里能筛出来复查,审错了点「退回待审」即可,
退回会分配新版本号推回各校。
<X size={12} /> 单次最多处理 {result?.policy.max_actions ?? 50} 条。
</p>
</section>
);
}