Files
writer-work-flow/apps/web/components/review/CharacterizationPanel.tsx
Yaojia Wang 72b3c81146 feat(review): 人物塑造 advisory 审(第五审,仅建议不阻断验收)
写→审链新增第五审「人物塑造」(灵感③/D2):advisory 单维——仅建议、不阻断
验收(不进 conflicts、不碰 assert_conflicts_resolved、不入 REVIEW_RESERVED_NAMES);
只做人物塑造、不做氛围。

- SPEC characterization_spec(analyst,reads=("characters",),writes=())+
  prompts/characterization.md(只读、显式忽略文本长度与辞藻华丽度、引用逐字片段+置信度)
- schema CharacterizationReview{issues[...]}(全字段默认值守解析韧性)+ 注册 catalog
- 计数三处 22→23 + regen prompt_hashes.json(仅加一条)
- DB chapter_reviews 加 characterization JSONB nullable 列(迁移 f6a7b8c9d0e1)
- 接线:graph REVIEW_SPECS 追加 · chain ReviewRecordRepo · collect(extract/record) ·
  review_repo(Protocol/Sql/_to_view/ReviewView) · sse(event/factory/section) · normalize 自动纳入
- 契约 ReviewHistoryItem 加 characterization(gen:api 已重生成)
- 前端 sse/history/useReviewStream + CharacterizationPanel(只展示无裁决 UI、置信度降序+低置信折叠)+ ReviewReport 挂面板
- 测试 test_characterization_does_not_block_accept + 计数/golden + collect/normalize + 前端 reducer/normalize/parse

守不变量 #2/#3/#9;依赖 ⑧(motive/appearance 作客观锚点)。
2026-07-06 17:08:38 +02:00

103 lines
3.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.

"use client";
import { CheckCircle2, CircleDashed, UserRound } from "lucide-react";
import { Badge } from "@/components/ui/Badge";
import type { CharacterizationIssue, CharacterizationReport } from "@/lib/review/sse";
interface CharacterizationPanelProps {
characterization: CharacterizationReport | null;
incomplete: boolean;
}
// 高置信度阈值≥0.7 为主区常显,<0.7 折进「更多低置信度建议」抽屉(控作者注意力预算,灵感 §10
const HIGH_CONFIDENCE = 0.7;
// 人物塑造报告区(灵感③ advisory 单维):只读建议、**无裁决 UI**(不进 conflicts/不阻断验收)。
// 以人物卡 motive/appearance 为客观锚点;每条带逐字引用 + 置信度。按置信度降序,
// 低置信度折叠,避免维度噪声淹没作者。
export function CharacterizationPanel({
characterization,
incomplete,
}: CharacterizationPanelProps) {
if (incomplete) {
return (
<section>
<Badge variant="warning">
<CircleDashed className="h-3 w-3" aria-hidden="true" />
</Badge>
</section>
);
}
const issues = characterization?.issues ?? [];
if (characterization === null || issues.length === 0) {
return (
<section>
<Badge variant="success">
<CheckCircle2 className="h-3 w-3" aria-hidden="true" />
</Badge>
</section>
);
}
// 置信度降序(优先级合并);高/低分区。
const sorted = [...issues].sort((a, b) => b.confidence - a.confidence);
const high = sorted.filter((i) => i.confidence >= HIGH_CONFIDENCE);
const low = sorted.filter((i) => i.confidence < HIGH_CONFIDENCE);
return (
<section className="space-y-2 text-xs">
<p className="rounded border border-dashed border-line/70 bg-bg/40 px-3 py-1.5 text-[11px] text-ink-soft">
</p>
<ul className="space-y-2">
{(high.length > 0 ? high : sorted).map((issue, i) => (
<CharacterizationIssueCard key={i} issue={issue} />
))}
</ul>
{high.length > 0 && low.length > 0 ? (
<details className="group rounded border border-line/70 bg-bg/35 p-2">
<summary className="cursor-pointer list-none text-2xs font-semibold text-ink-soft">
{low.length}
</summary>
<ul className="mt-2 space-y-2">
{low.map((issue, i) => (
<CharacterizationIssueCard key={i} issue={issue} />
))}
</ul>
</details>
) : null}
</section>
);
}
function CharacterizationIssueCard({ issue }: { issue: CharacterizationIssue }) {
const pct = Math.round(Math.max(0, Math.min(1, issue.confidence)) * 100);
return (
<li className="rounded border border-line/70 bg-bg/35 p-2">
<div className="flex items-center justify-between gap-2">
<span className="flex items-center gap-1.5 font-semibold text-ink">
<UserRound className="h-3.5 w-3.5 text-ink-soft" aria-hidden="true" />
{issue.character || "(未归属角色)"}
{issue.aspect ? <Badge variant="accent">{issue.aspect}</Badge> : null}
</span>
<span className="shrink-0 font-mono text-2xs text-ink-soft" title="置信度">
{pct}%
</span>
</div>
{issue.where ? <p className="mt-1 text-ink-soft">{issue.where}</p> : null}
{issue.quote ? (
<blockquote className="mt-1 border-l-2 border-line pl-2 text-ink-soft italic">
{issue.quote}
</blockquote>
) : null}
{issue.diagnosis ? <p className="mt-1 text-ink">{issue.diagnosis}</p> : null}
{issue.suggestion ? (
<p className="mt-1 text-ink-soft">{issue.suggestion}</p>
) : null}
</li>
);
}