feat(frontend): 编辑器内「续写」——读本章前文生成多候选,点选插入正文
Phase 1(写作工作台重构):编辑器「续写」按钮复用 continue 生成器(with_prior_chapter 自动读该章已写正文承接),每次产出一条候选并累加成多候选(useContinue,已单测), 「再来一个」可多生成几版;作者点某条「插入正文」才追加到章末(HITL,不静默写入)。 续写前 flush 自动保存,确保读到最新草稿。润色/续写结果卡互斥不并占。
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
PenLine,
|
||||
Square,
|
||||
Wand2,
|
||||
WrapText,
|
||||
} from "lucide-react";
|
||||
|
||||
import { AppShell } from "@/components/AppShell";
|
||||
@@ -38,6 +39,7 @@ import { ChapterList, ChapterListContent } from "./ChapterList";
|
||||
import { ChapterAssistant, AssistantContent } from "./ChapterAssistant";
|
||||
import { Editor, type EditorSelection } from "./Editor";
|
||||
import { RefinePanel } from "./RefinePanel";
|
||||
import { ContinuePanel } from "./ContinuePanel";
|
||||
|
||||
interface WorkbenchProps {
|
||||
project: ProjectResponse;
|
||||
@@ -71,6 +73,7 @@ export function Workbench({
|
||||
// 选区级 AI 动作(润色/再沟通):编辑器上报的最新选区 + 结果卡开合。
|
||||
const [selection, setSelection] = useState<EditorSelection | null>(null);
|
||||
const [refineOpen, setRefineOpen] = useState(false);
|
||||
const [continueOpen, setContinueOpen] = useState(false);
|
||||
const toast = useToast();
|
||||
const autosave = useAutosave(project.id, chapterNo, initialText);
|
||||
const stream = useDraftStream();
|
||||
@@ -131,6 +134,28 @@ export function Workbench({
|
||||
setSelection(null);
|
||||
};
|
||||
|
||||
// 打开润色/续写互斥,避免两张结果卡同时占位。
|
||||
const openRefine = (): void => {
|
||||
setContinueOpen(false);
|
||||
setRefineOpen(true);
|
||||
};
|
||||
|
||||
// 续写前先 flush 自动保存,确保 continue 读到的是最新草稿(with_prior_chapter 读库)。
|
||||
const openContinue = (): void => {
|
||||
autosave.flush();
|
||||
setRefineOpen(false);
|
||||
setContinueOpen(true);
|
||||
};
|
||||
|
||||
// 插入续写候选:追加到章末(续写语义),落回草稿。
|
||||
const onInsertContinuation = (candidate: string): void => {
|
||||
const base = text.trimEnd();
|
||||
const next = base.length > 0 ? `${base}\n\n${candidate}` : candidate;
|
||||
setText(next);
|
||||
autosave.onChange(next);
|
||||
setContinueOpen(false);
|
||||
};
|
||||
|
||||
// 字数统计非空白字符(与中文读者直觉一致:标点/空格不计入正文体量)。
|
||||
const wordCount = text.replace(/\s+/g, "").length;
|
||||
const closePanel = (): void => setMobilePanel(null);
|
||||
@@ -189,6 +214,14 @@ export function Workbench({
|
||||
onClose={() => setRefineOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
{continueOpen ? (
|
||||
<ContinuePanel
|
||||
projectId={project.id}
|
||||
chapterNo={chapterNo}
|
||||
onInsert={onInsertContinuation}
|
||||
onClose={() => setContinueOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
<div className="flex-1 overflow-auto px-6 py-8">
|
||||
<Editor
|
||||
value={text}
|
||||
@@ -209,7 +242,8 @@ export function Workbench({
|
||||
onWrite={onWrite}
|
||||
onStop={stream.stop}
|
||||
canRefine={canRefine}
|
||||
onRefineSelection={() => setRefineOpen(true)}
|
||||
onRefineSelection={openRefine}
|
||||
onContinue={openContinue}
|
||||
/>
|
||||
</section>
|
||||
|
||||
@@ -400,6 +434,8 @@ interface ToolbarProps {
|
||||
// 选区级润色:有非空选区时可用;点击打开润色/再沟通结果卡。
|
||||
canRefine: boolean;
|
||||
onRefineSelection: () => void;
|
||||
// 续写:读本章前文续写候选。
|
||||
onContinue: () => void;
|
||||
}
|
||||
|
||||
function Toolbar({
|
||||
@@ -415,6 +451,7 @@ function Toolbar({
|
||||
onStop,
|
||||
canRefine,
|
||||
onRefineSelection,
|
||||
onContinue,
|
||||
}: ToolbarProps) {
|
||||
return (
|
||||
<div className="sticky bottom-0 z-10 border-t border-line bg-panel/95 px-4 py-2 backdrop-blur sm:px-6 sm:py-3">
|
||||
@@ -433,16 +470,22 @@ function Toolbar({
|
||||
</Button>
|
||||
)}
|
||||
{!streaming ? (
|
||||
<Button
|
||||
onClick={onRefineSelection}
|
||||
disabled={!canRefine}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
title={canRefine ? undefined : "先在正文里选中一段再润色"}
|
||||
>
|
||||
<Wand2 className="h-4 w-4" aria-hidden="true" />
|
||||
润色选段
|
||||
</Button>
|
||||
<>
|
||||
<Button onClick={onContinue} variant="secondary" size="sm">
|
||||
<WrapText className="h-4 w-4" aria-hidden="true" />
|
||||
续写
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onRefineSelection}
|
||||
disabled={!canRefine}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
title={canRefine ? undefined : "先在正文里选中一段再润色"}
|
||||
>
|
||||
<Wand2 className="h-4 w-4" aria-hidden="true" />
|
||||
润色选段
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
{streaming ? (
|
||||
// ThinkingIndicator 自带 role=status,开始时播报一次「生成中」;
|
||||
|
||||
Reference in New Issue
Block a user