Files
writer-work-flow/apps/web/components/review/AcceptPanel.tsx
Yaojia Wang 29265fa0fd feat(web): 审稿改稿闭环——一键采纳/AI改写/正文高亮定位编辑/伏笔确认/草稿自动保存/章节目录
- 冲突「采纳改法」:有补丁瞬时改入终稿并选中;无补丁但可定位则采纳时调 AI(refiner)重写该段套入;定位不到提示手改
- 正文改为行内高亮叠层编辑器(与页面同底色、随内容增高、无滚动条),点冲突卡/锚点整页滚到并选中+闪烁
- 仅对可在正文定位的冲突显示「跳转」/锚点;locate 支持从 where 引号抽原文,杜绝定位失败刷屏
- 伏笔建议卡:新埋一键登记(预填表单)/ 疑似回收一键标记 CLOSED(复用现有端点)
- 审稿页草稿自动保存(防抖 PUT /draft),与「验收本章」解耦,刷新不丢
- 写作路由带章号 ?chapter=N + 验收成功面板/大纲页「写下一章」入口;写作目录从大纲列出全部章节

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 18:28:29 +02:00

135 lines
4.5 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 Link from "next/link";
import type { AcceptResponse } from "@/lib/api/types";
import { buildAcceptPreview } from "@/lib/review/accept-preview";
import { ThinkingIndicator } from "@/components/ThinkingIndicator";
interface AcceptPanelProps {
projectId: string;
chapterNo: number;
conflictCount: number;
unresolvedCount: number;
// R3当前伏笔建议数验收前清单提醒只读不自动落库
foreshadowCount: number;
accepting: boolean;
result: AcceptResponse | null;
onAccept: () => void;
}
// 验收 gateUX §9 / §8.4):未决禁验收(置灰 + 文案),验收后呈现「本次将更新」清单。
export function AcceptPanel({
projectId,
chapterNo,
conflictCount,
unresolvedCount,
foreshadowCount,
accepting,
result,
onAccept,
}: AcceptPanelProps) {
const blocked = unresolvedCount > 0;
const preview = buildAcceptPreview({
conflictCount,
foreshadowSuggestions: foreshadowCount,
});
if (result) {
return (
<div className="rounded border border-pass/50 bg-panel p-4">
<h3 className="mb-2 text-sm font-semibold text-pass"></h3>
<ul className="space-y-1 text-xs text-ink-soft">
<li>
<span className="font-mono text-ink">v{result.accepted_version}</span>
</li>
<li>
{result.digest_added ? "已新增一行" : "未变更"}
</li>
<li>
<span className="font-mono text-ink">
{result.decisions_recorded}
</span>{" "}
</li>
{result.review_id ? (
<li className="truncate">
<span className="font-mono">{result.review_id}</span>
</li>
) : null}
</ul>
{/* 验收闭环 → 写下一章入口(修「验收后无路可走」的断点)。 */}
<div className="mt-3 flex flex-wrap items-center gap-2">
<Link
href={`/projects/${projectId}/write?chapter=${chapterNo + 1}`}
className="rounded bg-cinnabar px-4 py-2 text-sm text-panel hover:opacity-90"
>
{chapterNo + 1}
</Link>
<Link
href={`/projects/${projectId}/outline`}
className="rounded border border-line px-4 py-2 text-sm text-ink hover:border-cinnabar hover:text-cinnabar"
>
</Link>
</div>
</div>
);
}
return (
<div className="rounded border border-line bg-panel p-4">
{blocked ? (
<p className="mb-2 text-xs text-conflict" role="status">
{unresolvedCount}
</p>
) : (
<>
<p className="mb-2 text-xs text-ink-soft">
{conflictCount === 0
? "无冲突,可直接验收。"
: "全部冲突已裁决,可验收本章。"}
</p>
{/* R3验收前「本次将更新」清单预期落库口径以验收回执为准。 */}
<div className="mb-3 rounded border border-line/70 bg-bg/50 p-3">
<h4 className="mb-1.5 text-xs font-semibold text-ink">
</h4>
<ul className="space-y-1.5">
{preview.map((item) => (
<li key={item.label} className="flex gap-2 text-xs text-ink-soft">
<span className="text-cinnabar" aria-hidden="true">
</span>
<span>
<span className="text-ink">{item.label}</span>
<span className="text-ink-soft">{item.detail}</span>
</span>
</li>
))}
</ul>
<p className="mt-2 text-[11px] text-ink-soft/70">
</p>
</div>
</>
)}
<button
type="button"
onClick={onAccept}
disabled={blocked || accepting}
aria-disabled={blocked || accepting}
className="w-full rounded bg-cinnabar px-4 py-2 text-sm text-panel hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40"
>
{accepting ? (
<ThinkingIndicator label="验收中" className="justify-center" />
) : (
"全部处理完 → 验收本章"
)}
</button>
</div>
);
}