feat(frontend): 编辑器内「续写」——读本章前文生成多候选,点选插入正文

Phase 1(写作工作台重构):编辑器「续写」按钮复用 continue 生成器(with_prior_chapter
自动读该章已写正文承接),每次产出一条候选并累加成多候选(useContinue,已单测),
「再来一个」可多生成几版;作者点某条「插入正文」才追加到章末(HITL,不静默写入)。
续写前 flush 自动保存,确保读到最新草稿。润色/续写结果卡互斥不并占。
This commit is contained in:
Yaojia Wang
2026-07-07 19:41:57 +02:00
parent 2f0b84191a
commit 9195cf36f3
4 changed files with 336 additions and 11 deletions

View File

@@ -0,0 +1,72 @@
"use client";
import { useCallback, useState } from "react";
import { api } from "@/lib/api/client";
import { useToast } from "@/components/Toast";
import { generationErrorMessage } from "@/lib/generation/cards";
// 编辑器「续写」:复用 continue 生成器with_prior_chapter自动读该章已写正文承接
// 每次生成一条候选并累加成多候选作者点选某条插入正文HITL不静默写入
export type ContinueStatus = "idle" | "generating" | "ready" | "error";
export interface UseContinue {
status: ContinueStatus;
candidates: string[];
// 生成一条续写候选累加不覆盖chapterNo 指定承接哪一章。
generate: (projectId: string, chapterNo: number) => Promise<void>;
reset: () => void;
}
function extractText(preview: unknown): string {
if (preview && typeof preview === "object" && "text" in preview) {
const text = (preview as { text: unknown }).text;
return typeof text === "string" ? text : "";
}
return "";
}
export function useContinue(): UseContinue {
const [status, setStatus] = useState<ContinueStatus>("idle");
const [candidates, setCandidates] = useState<string[]>([]);
const toast = useToast();
const generate = useCallback<UseContinue["generate"]>(
async (projectId, chapterNo) => {
setStatus("generating");
try {
const { data, error } = await api.POST(
"/projects/{project_id}/skills/{tool_key}/generate",
{
params: { path: { project_id: projectId, tool_key: "continue" } },
body: { brief: "", chapter_no: chapterNo },
},
);
if (error || !data) {
setStatus("error");
toast(generationErrorMessage(error), "error");
return;
}
const text = extractText(data.preview).trim();
if (text.length === 0) {
setStatus("ready");
toast("本次未生成有效续写,请再试一次。", "info");
return;
}
setCandidates((prev) => [...prev, text]);
setStatus("ready");
} catch {
setStatus("error");
toast("续写请求异常,请检查网络。", "error");
}
},
[toast],
);
const reset = useCallback((): void => {
setStatus("idle");
setCandidates([]);
}, []);
return { status, candidates, generate, reset };
}