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:
@@ -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 ? (
|
||||
|
||||
Reference in New Issue
Block a user