Files
writer-work-flow/apps/web/lib/workbench/useContinue.ts
Yaojia Wang 9195cf36f3 feat(frontend): 编辑器内「续写」——读本章前文生成多候选,点选插入正文
Phase 1(写作工作台重构):编辑器「续写」按钮复用 continue 生成器(with_prior_chapter
自动读该章已写正文承接),每次产出一条候选并累加成多候选(useContinue,已单测),
「再来一个」可多生成几版;作者点某条「插入正文」才追加到章末(HITL,不静默写入)。
续写前 flush 自动保存,确保读到最新草稿。润色/续写结果卡互斥不并占。
2026-07-07 19:41:57 +02:00

73 lines
2.3 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 { 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 };
}