Files
browser/platform_web/components/AIConfigDiffReviewPanel.tsx
T
2026-07-22 15:27:26 +08:00

78 lines
4.2 KiB
TypeScript

import { FileCheck2, RotateCw } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { platformApiClient } from "../api/client";
import type { AIConfigDiffPreviewResponse } from "../api/types";
import { ConfirmDialog } from "./OperationControls";
import { ErrorState, LoadingState, ResultBadge } from "./StateViews";
export function AIConfigDiffReviewPanel() {
const [items, setItems] = useState<AIConfigDiffPreviewResponse[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [selected, setSelected] = useState<AIConfigDiffPreviewResponse | null>(null);
const [busyId, setBusyId] = useState("");
const [result, setResult] = useState<{ status: "succeeded" | "failed"; label: string } | null>(null);
const refresh = useCallback(async () => {
setLoading(true);
setError("");
try {
const response = await platformApiClient.listAIConfigDiffs();
setItems(response.items);
} catch (caught) {
setError(caught instanceof Error ? caught.message : "AI 配置审查队列加载失败");
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
void refresh();
}, [refresh]);
async function approve() {
if (!selected || busyId) return;
setBusyId(selected.id);
setResult(null);
try {
const response = await platformApiClient.approveAIConfigDiff(selected.id, `web:ai.config.approve:${selected.id}`);
setResult({ status: "succeeded", label: `已审批 ${response.preview.id} · 写入任务 ${response.dispatch.job.id}` });
setSelected(null);
await refresh();
} catch (caught) {
setResult({ status: "failed", label: caught instanceof Error ? caught.message : "AI 配置审批失败" });
setSelected(null);
} finally {
setBusyId("");
}
}
return (
<section className="console-panel console-module ai-diff-review-panel" aria-label="AI config diff review">
<div className="panel-header">
<h2><FileCheck2 size={16} /> AI 配置审查</h2>
<button type="button" className="icon-command" disabled={loading || Boolean(busyId)} onClick={() => void refresh()}><RotateCw size={14} /><span>刷新</span></button>
</div>
{result && <ResultBadge status={result.status} label={result.label} />}
{loading && <LoadingState label="正在同步 AI 配置差异…" compact />}
{!loading && error && <ErrorState title="AI 配置审查不可用" reason={error} diagnosticId="ai-config-diffs" onRetry={() => void refresh()} compact />}
{!loading && !error && (
<div className="console-record-list">
{items.length === 0 && <p className="console-empty-note">当前没有 AI 配置差异。</p>}
{items.slice(0, 12).map((item) => (
<div key={item.id} className="console-record">
<div className="console-record-head"><strong>{item.serverInstanceId} · {item.key}</strong><span className={`status-pill status-${item.state === "approved" ? "succeeded" : item.state === "pending" ? "warning" : "disabled"}`}>{item.state}</span></div>
<div className="console-record-meta"><span>请求 {item.requestId}</span><span>版本 {item.configVersion}</span><span>{item.model || "Platform model"}</span><span>到期 {new Date(item.expiresAt).toLocaleString()}</span>{item.jobId && <span>任务 {item.jobId}</span>}</div>
<p>{item.diffSummary}</p>
{item.proposedConfig && <pre className="log-view ai-config-proposal">{item.proposedConfig}</pre>}
{item.state === "pending" && <div className="row-actions console-row-actions"><button type="button" disabled={Boolean(busyId)} onClick={() => setSelected(item)}><FileCheck2 size={14} /><span>审查并批准</span></button></div>}
</div>
))}
</div>
)}
<ConfirmDialog open={selected !== null} title="批准 AI 配置差异" description={selected ? `服务器 ${selected.serverInstanceId},配置版本 ${selected.configVersion},差异 ${selected.id}。` : "确认 AI 配置差异。"} confirmLabel="批准并派发" busy={Boolean(busyId)} onCancel={() => { if (!busyId) setSelected(null); }} onConfirm={() => void approve()} />
</section>
);
}