Files
writer-work-flow/apps/web/lib/workbench/useRefine.ts
Yaojia Wang 2f0b84191a feat(frontend): 编辑器内「润色 / 再沟通」——选段回炉 + 追加意见迭代出版本栈,接受回填
Phase 1(写作工作台重构):编辑器选中一段即可「润色选段」,复用同步 /refine 回炉
(只读、不写库,守不变量 #3);不满意可「再沟通」——以上一版产出为输入 + 作者新意见
再改一版,形成版本栈(useRefine,已单测)。接受回填按内容重锚落回草稿,原文已变则
提示重选、绝不盲替换(applyRefinement,已单测)。Editor 上报选区,结果以内联卡片展示。
2026-07-07 18:26:47 +02:00

113 lines
3.6 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, useMemo, useState } from "react";
import { api } from "@/lib/api/client";
import { useToast } from "@/components/Toast";
// 编辑器内「润色 / 再沟通」核心(复用同步 /refine只读、不写库——不变量 #3
// 润色:对选中段回炉一次。再沟通:以上一版产出为输入 + 作者新意见再回炉,形成版本栈,
// 作者可回看每一版、择一 accept 回填正文accept/回填由组件负责,此处只管生成与版本历史)。
export interface RefineVersion {
// 本次回炉的输入段:首轮 = 选中原文;再沟通 = 上一版产出。
segment: string;
// 本次改写指令(再沟通时为作者的新意见);无则空串。
instruction: string;
refined: string;
}
export type RefineStatus = "idle" | "refining" | "ready" | "error";
export interface UseRefine {
status: RefineStatus;
versions: RefineVersion[];
// 版本栈顶(最新产出)。
latest: RefineVersion | null;
// 最初选中的原文accept 回填的对比基线)。
original: string | null;
refine: (
projectId: string,
chapterNo: number,
segment: string,
instruction?: string,
) => Promise<void>;
// 以上一版产出为输入 + 作者新意见再回炉一次。无历史版本时为空操作。
recommunicate: (
projectId: string,
chapterNo: number,
instruction: string,
) => Promise<void>;
reset: () => void;
}
export function useRefine(): UseRefine {
const [status, setStatus] = useState<RefineStatus>("idle");
const [versions, setVersions] = useState<RefineVersion[]>([]);
const toast = useToast();
const runRefine = useCallback(
async (
projectId: string,
chapterNo: number,
segment: string,
instruction: string,
): Promise<void> => {
setStatus("refining");
try {
const trimmed = instruction.trim();
const { data, error } = await api.POST(
"/projects/{project_id}/chapters/{chapter_no}/refine",
{
params: { path: { project_id: projectId, chapter_no: chapterNo } },
body: trimmed ? { segment, instruction: trimmed } : { segment },
},
);
if (error || !data) {
setStatus("error");
toast("回炉失败,请重试。", "error");
return;
}
setVersions((prev) => [
...prev,
{ segment, instruction: trimmed, refined: data.refined },
]);
setStatus("ready");
} catch {
setStatus("error");
toast("回炉请求异常,请检查网络。", "error");
}
},
[toast],
);
const refine = useCallback<UseRefine["refine"]>(
(projectId, chapterNo, segment, instruction) =>
runRefine(projectId, chapterNo, segment, instruction ?? ""),
[runRefine],
);
const recommunicate = useCallback<UseRefine["recommunicate"]>(
(projectId, chapterNo, instruction) => {
const last = versions[versions.length - 1];
// 无历史版本 → 无从「再沟通」;空操作,不打后端。
if (!last) return Promise.resolve();
return runRefine(projectId, chapterNo, last.refined, instruction);
},
[runRefine, versions],
);
const reset = useCallback((): void => {
setStatus("idle");
setVersions([]);
}, []);
const latest = versions.at(-1) ?? null;
const original = versions[0]?.segment ?? null;
return useMemo(
() => ({ status, versions, latest, original, refine, recommunicate, reset }),
[status, versions, latest, original, refine, recommunicate, reset],
);
}