Files
model-platform/frontend/app/features/operations/PromptManagementPage.tsx
T

66 lines
6.9 KiB
TypeScript
Raw 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 { Fragment, useState } from "react";
import { CheckCircle2, Edit3, RotateCcw, Save, Sparkles } from "lucide-react";
import { toast } from "sonner";
import { Button } from "~/components/ui/button";
import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table";
import { Textarea } from "~/components/ui/textarea";
import { usePersistentState } from "~/hooks/use-persistent-state";
import { FilterSelect, OperationsPageHeader } from "./OperationsUi";
import { PROMPTS, PROMPT_REGRESSION, PROMPT_VERSIONS } from "./governanceData";
type PromptKey = keyof typeof PROMPTS;
function PromptPreview({ text }: { text: string }) {
const parts = text.split(/(\{\{[^}]+\}\})/g);
return <pre className="whitespace-pre-wrap rounded-xl bg-bg-log p-5 font-mono text-xs leading-6 text-white/80">{parts.map((part, index) => part.startsWith("{{") ? <mark className="rounded bg-warning/20 px-1 text-warning" key={`${part}-${index}`}>{part}</mark> : <Fragment key={`${part}-${index}`}>{part}</Fragment>)}</pre>;
}
export default function PromptManagementPage() {
const [promptKey, setPromptKey] = useState<PromptKey>("BC");
const [texts, setTexts] = usePersistentState<Record<PromptKey, string>>("a-card-prompt-texts", { A: PROMPTS.A.text, BC: PROMPTS.BC.text });
const [editing, setEditing] = useState(false);
const [versions, setVersions] = usePersistentState("a-card-prompt-versions", PROMPT_VERSIONS);
const passed = PROMPT_REGRESSION.filter((item) => item.result === "通过").length;
const savePrompt = () => {
const version = `P${Number(versions[0]?.version.slice(1) ?? 5) + 1}`;
setVersions((current) => [{ version, author: "模型团队 李伟", createdAt: "2026-08-31 11:05", current: true, note: "手动保存并提交回归(Mock" }, ...current.map((item) => ({ ...item, current: false }))]);
setEditing(false);
toast.success(`已保存为 ${version} 并提交回归评审`);
};
return (
<section className="h-full overflow-auto bg-bg p-6">
<div className="mx-auto flex max-w-screen-2xl flex-col gap-6 pb-8">
<OperationsPageHeader title="报告 Prompt 管理" description="查看和调整提示词全文,保留版本记录,并通过历史样本回归后生效。" actions={<Button onClick={savePrompt}><Save />保存并提交回归</Button>} />
<div className="rounded-xl bg-brand-soft p-4 text-sm leading-6 text-primary">提示词查看与调整属于需求沟通补充项,建议在下一版需求文档中同步固化。平台注入变量由程序取数,不交由大模型计算。</div>
<div className="grid grid-cols-[minmax(0,1.5fr)_minmax(20rem,0.7fr)] gap-6">
<Card>
<CardHeader className="border-b border-border"><CardTitle className="flex items-center gap-2"><Sparkles className="size-5 text-primary" />提示词全文</CardTitle><CardDescription>变量占位符必须保持双花括号格式</CardDescription><CardAction className="flex items-end gap-2"><FilterSelect className="w-72" label="报告模板" value={promptKey} allLabel="请选择" options={[{ label: PROMPTS.A.name, value: "A" }, { label: PROMPTS.BC.name, value: "BC" }]} onChange={(value) => { setPromptKey(value as PromptKey); setEditing(false); }} /><Button variant="outline" size="sm" onClick={() => setEditing((value) => !value)}><Edit3 />{editing ? "退出编辑" : "编辑"}</Button></CardAction></CardHeader>
<CardContent className="space-y-4">
{editing ? <Textarea className="min-h-[36rem] font-mono text-xs leading-6" value={texts[promptKey]} onChange={(event) => setTexts((current) => ({ ...current, [promptKey]: event.target.value }))} /> : <PromptPreview text={texts[promptKey]} />}
<p className="rounded-xl bg-muted/50 p-4 text-xs leading-5 text-muted-foreground">高亮内容为程序注入变量。等级字段仅用于选择模板,不进入报告正文;Prompt 禁止自行计算指标或臆测业务原因。</p>
</CardContent>
</Card>
<div className="space-y-6">
<Card>
<CardHeader><CardTitle>版本留痕</CardTitle><CardDescription>当前生效与历史版本</CardDescription></CardHeader>
<CardContent className="space-y-3">{versions.map((version) => <div className="flex items-center gap-3 rounded-xl border border-border p-4" key={version.version}><b className="text-sm text-foreground">{version.version}</b><span className="min-w-0 flex-1"><small className="block truncate text-xs text-muted-foreground">{version.note}</small><small className="text-3xs text-muted-foreground">{version.author} · {version.createdAt}</small></span>{version.current ? <span className="rounded-full bg-success-soft px-2.5 py-1 text-xs text-success-strong">生效中</span> : <Button variant="outline" size="icon-xs" aria-label={`回滚至 ${version.version}`} onClick={() => { setVersions((current) => current.map((item) => ({ ...item, current: item.version === version.version }))); toast.success(`Prompt 已模拟回滚至 ${version.version}`); }}><RotateCcw /></Button>}</div>)}</CardContent>
</Card>
<Card>
<CardHeader><CardTitle>回归评审</CardTitle><CardDescription>上线前使用历史样本回归</CardDescription></CardHeader>
<CardContent className="grid grid-cols-3 gap-3"><div className="rounded-xl bg-muted/50 p-3"><span className="text-xs text-muted-foreground">回归样本</span><strong className="mt-1 block text-xl tabular-nums">{PROMPT_REGRESSION.length}</strong></div><div className="rounded-xl bg-success-soft p-3"><span className="text-xs text-success-strong">通过</span><strong className="mt-1 block text-xl tabular-nums text-success-strong">{passed}</strong></div><div className="rounded-xl bg-warning-soft p-3"><span className="text-xs text-warning">待复核</span><strong className="mt-1 block text-xl tabular-nums text-warning">{PROMPT_REGRESSION.length - passed}</strong></div></CardContent>
<CardContent className="px-0 pt-0"><Table><TableHeader><TableRow><TableHead>历史样本</TableHead><TableHead>结果</TableHead><TableHead>说明</TableHead></TableRow></TableHeader><TableBody>{PROMPT_REGRESSION.map((item) => <TableRow key={item.sample}><TableCell className="min-w-48 whitespace-normal">{item.sample}</TableCell><TableCell>{item.result === "通过" ? <span className="inline-flex items-center gap-1 rounded-full bg-success-soft px-2.5 py-1 text-xs text-success-strong"><CheckCircle2 className="size-3" />通过</span> : <span className="rounded-full bg-warning-soft px-2.5 py-1 text-xs text-warning">待复核</span>}</TableCell><TableCell className="whitespace-normal text-muted-foreground">{item.detail}</TableCell></TableRow>)}</TableBody></Table></CardContent>
</Card>
</div>
</div>
</div>
</section>
);
}