"use client"; import { useEffect, useRef, useState } from "react"; import { Check, MessageSquarePlus, MessagesSquare, RotateCcw, X } from "lucide-react"; import { ThinkingIndicator } from "@/components/ThinkingIndicator"; import { Button } from "@/components/ui/Button"; import { SectionHeader } from "@/components/ui/SectionHeader"; import { StatusNote } from "@/components/ui/StatusNote"; import { TextArea } from "@/components/ui/TextArea"; import { useRefine } from "@/lib/workbench/useRefine"; import { useClarify } from "@/lib/workbench/useClarify"; import { foldClarifications, needsClarifyGate } from "@/lib/workbench/clarify"; import type { AiTurnInput, AiTurnMessage, } from "@/lib/workbench/useAiConversation"; import { ChoiceChips } from "./ChoiceChips"; // 门控阈值:再沟通意见 trim 后短于此字数才自动预检反问(清晰意见不交往返税)。 const CLARIFY_MIN_CHARS = 10; interface RefinePanelProps { projectId: string; chapterNo: number; // 选中原文;打开即对其回炉一次。 original: string; // 接受回填:把选段替换为该产出(调用方按内容重锚落回草稿)。 onAccept: (refined: string) => void; // 留痕:把本轮往复(含缓冲的 clarify Q&A)作为一批 append(fail-soft,不阻塞回炉)。 appendMessages: (turn: AiTurnInput) => void; onClose: () => void; } // 润色 / 再沟通结果卡(内联,非模态):打开即润色选段;作者可「再沟通」追加意见迭代出新版, // 择版接受回填正文,或放弃。原文只读、生成不写库(不变量 #3),accept 才动草稿。 export function RefinePanel({ projectId, chapterNo, original, onAccept, appendMessages, onClose, }: RefinePanelProps) { const { status, versions, latest, refine, recommunicate } = useRefine(); const clarify = useClarify(); const [instruction, setInstruction] = useState(""); const [pendingInstruction, setPendingInstruction] = useState(null); const ranRef = useRef(false); // 本面板一次会话 = 一个 thread;所有润色/再沟通/澄清往复归此组。版本号 repo 无关,本地累加。 const threadId = useRef(crypto.randomUUID()).current; const versionNoRef = useRef(0); // 一版 ai 产出气泡:content=产出,meta 存版本号 + 原选段(每版都存,供任意版 re-accept 重锚)。 const aiRow = (refined: string): AiTurnMessage => { versionNoRef.current += 1; return { role: "ai", content: refined, meta: { version_no: versionNoRef.current, segment: original }, }; }; // 打开即对选段回炉一次(仅一次,避免重复请求);成功后留痕 [author(原选段), ai(润色)]。 useEffect(() => { if (ranRef.current) return; ranRef.current = true; void (async () => { const v = await refine(projectId, chapterNo, original); if (v) { appendMessages({ threadId, kind: "refine", chapterNo, messages: [{ role: "author", content: original }, aiRow(v.refined)], }); } })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [projectId, chapterNo, original, refine]); const busy = status === "refining"; const clarifying = clarify.status === "checking"; const firstQuestion = clarify.decision?.questions[0] ?? null; const asking = clarify.status === "asking" && firstQuestion !== null; // 折入澄清答案(或原意见)后回炉,结束后留痕;clarifyTurns 非空时把缓冲的 Q&A 一并落库。 const runRecommunicate = ( finalInstruction: string, clarifyTurns: AiTurnMessage[] | null, ): void => { clarify.reset(); setPendingInstruction(null); setInstruction(""); void (async () => { const v = await recommunicate(projectId, chapterNo, finalInstruction); if (!v) return; // clarify 分支:作者答复 IS 本轮 author 输入,不再另加 author(意见) 气泡;否则记 author(意见)。 const messages = clarifyTurns ? [...clarifyTurns, aiRow(v.refined)] : [{ role: "author" as const, content: finalInstruction }, aiRow(v.refined)]; appendMessages({ threadId, kind: "refine", chapterNo, messages }); })(); }; // 「再改一版」:意见含糊/极短(或 force)先预检反问;否则直接回炉。 const onRecommunicate = async (force: boolean): Promise => { const trimmed = instruction.trim(); if (!trimmed || busy || clarifying) return; if (force || needsClarifyGate(trimmed, CLARIFY_MIN_CHARS)) { const segment = latest?.refined ?? original; const vm = await clarify.check(projectId, chapterNo, segment, trimmed); if (vm.needClarification && vm.questions.length > 0) { setPendingInstruction(trimmed); // 记住原意见,等作者答完折进去 return; } } runRecommunicate(trimmed, null); }; // 作者答复澄清(点选 value 或自由输入)→ 缓冲 Q&A(原意见→反问→答复)+ 折进意见 → 回炉。 const onClarifyAnswer = (answer: string): void => { const q = clarify.decision?.questions[0]; const question = q?.question ?? ""; const base = pendingInstruction ?? instruction.trim(); // 缓冲三条:原含糊意见(author) → AI 反问(ai, 选项进 meta) → 作者答复(author),随本轮产出一起落库。 const clarifyTurns: AiTurnMessage[] = [ { role: "author", content: base }, { role: "ai", content: question, meta: { parent_kind: "refine", options: (q?.options ?? []).map((o) => ({ label: o.label, value: o.value, })), allow_free_text: q?.allowFreeText ?? false, verification: clarify.decision?.verification ?? "", }, }, { role: "author", content: answer }, ]; runRecommunicate(foldClarifications(base, [{ question, answer }]), clarifyTurns); }; return (
原文

{original}

改写 {versions.length > 0 ? ( 第 {versions.length} 版 ) : null}
{busy && !latest ? ( ) : latest ? (

{latest.refined}

) : status === "error" ? ( ) : null}