feat(frontend): 整章「再沟通/重写」面板——给意见流式重写整章,版本栈迭代,接受替换正文

WFW-8 整章级再沟通(前端)。底栏新增「整章重写」→ ChapterRewritePanel:
- 作者给意见 → useChapterRewrite 消费 POST .../rewrite 的 SSE 流(复用 lib/stream/sse
  帧解析),流式 live 展示新一版;
- 每轮完成压入版本栈,「再改一版」以最新版为 prior_draft + 新意见迭代(多轮对话式);
- 接受某版才「替换整章正文」落草稿(HITL,AI 绝不静默覆盖);停/错误/网络异常均归一处理。
四张 AI 结果卡(润色/续写/工具箱/整章重写)互斥。gen:api 重生成 TS 客户端。
门禁绿:tsc/lint/vitest 636(+7 useChapterRewrite)/build/coverage 95.36%。
This commit is contained in:
Yaojia Wang
2026-07-08 07:12:04 +02:00
parent 6ed9c89430
commit 542c2abac9
5 changed files with 567 additions and 1 deletions

View File

@@ -12,6 +12,7 @@ import {
PanelLeft,
PanelRight,
PenLine,
RefreshCw,
Sparkles,
Square,
Wand2,
@@ -48,6 +49,7 @@ import { Editor, type EditorSelection } from "./Editor";
import { RefinePanel } from "./RefinePanel";
import { ContinuePanel } from "./ContinuePanel";
import { InlineToolbox } from "./InlineToolbox";
import { ChapterRewritePanel } from "./ChapterRewritePanel";
import { ContextDrawer, CONTEXT_DRAWER_ENABLED } from "./ContextDrawer";
import { GenrePicker } from "./GenrePicker";
import { GENRES } from "@/lib/wizard/wizard";
@@ -87,6 +89,8 @@ export function Workbench({
const [refineOpen, setRefineOpen] = useState(false);
const [continueOpen, setContinueOpen] = useState(false);
const [toolboxOpen, setToolboxOpen] = useState(false);
// WFW-8整章「再沟通/重写」结果卡开合(意见 → 流式重写一版 → 版本栈 → 接受替换整章)。
const [rewriteOpen, setRewriteOpen] = useState(false);
// WFW-7首次写章的极轻 genre 采集门。项目无题材 → 写章前先点一个题材(仅临时并入本次指令,不落库)。
const genreGate = useGenreGate(project.genre ?? null);
const [genrePromptOpen, setGenrePromptOpen] = useState(false);
@@ -170,10 +174,11 @@ export function Workbench({
setSelection(null);
};
// 张 AI 结果卡(润色/续写/工具箱)互斥,避免同时占位。
// 张 AI 结果卡(润色/续写/工具箱/整章重写)互斥,避免同时占位。
const openRefine = (): void => {
setContinueOpen(false);
setToolboxOpen(false);
setRewriteOpen(false);
setRefineOpen(true);
};
@@ -182,15 +187,31 @@ export function Workbench({
autosave.flush();
setRefineOpen(false);
setToolboxOpen(false);
setRewriteOpen(false);
setContinueOpen(true);
};
const openToolbox = (): void => {
setRefineOpen(false);
setContinueOpen(false);
setRewriteOpen(false);
setToolboxOpen(true);
};
const openRewrite = (): void => {
setRefineOpen(false);
setContinueOpen(false);
setToolboxOpen(false);
setRewriteOpen(true);
};
// 接受某一版整章重写 → 替换整章正文并落草稿HITL作者点选才落
const onAcceptRewrite = (next: string): void => {
setText(next);
autosave.onChange(next);
setRewriteOpen(false);
};
// 追加一段文本到章末并落草稿(续写候选 / 工具箱产物共用)。
const appendToDraft = (chunk: string): void => {
const base = text.trimEnd();
@@ -289,6 +310,15 @@ export function Workbench({
onClose={() => setToolboxOpen(false)}
/>
) : null}
{rewriteOpen ? (
<ChapterRewritePanel
projectId={project.id}
chapterNo={chapterNo}
currentDraft={text}
onAccept={onAcceptRewrite}
onClose={() => setRewriteOpen(false)}
/>
) : null}
<div className="flex-1 overflow-auto px-6 py-8">
{chapters.length === 0 ? (
<StatusNote variant="info" className="mb-4">
@@ -317,6 +347,7 @@ export function Workbench({
onRefineSelection={openRefine}
onContinue={openContinue}
onToolbox={openToolbox}
onRewrite={openRewrite}
onOpenContext={() => setContextOpen(true)}
contextTriggerRef={contextTriggerRef}
/>
@@ -549,6 +580,8 @@ interface ToolbarProps {
onContinue: () => void;
// 工具箱:编辑器内联调用生成器,结果回填正文。
onToolbox: () => void;
// 整章再沟通/重写WFW-8
onRewrite: () => void;
// 打开上下文速查抽屉WFW-6
onOpenContext: () => void;
contextTriggerRef: RefObject<HTMLButtonElement | null>;
@@ -569,6 +602,7 @@ function Toolbar({
onRefineSelection,
onContinue,
onToolbox,
onRewrite,
onOpenContext,
contextTriggerRef,
}: ToolbarProps) {
@@ -608,6 +642,10 @@ function Toolbar({
<Sparkles className="h-4 w-4" aria-hidden="true" />
</Button>
<Button onClick={onRewrite} variant="secondary" size="sm">
<RefreshCw className="h-4 w-4" aria-hidden="true" />
</Button>
</>
) : null}
{streaming ? (