"use client"; import { useRef, useState } from "react"; import { Check, MessagesSquare, RotateCcw, Square, 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 { friendlyError } from "@/lib/errors/messages"; import { foldClarifications, needsClarifyGate } from "@/lib/workbench/clarify"; import { useChapterRewrite } from "@/lib/workbench/useChapterRewrite"; import { useRewriteClarify } from "@/lib/workbench/useRewriteClarify"; import type { AiTurnInput, AiTurnMessage, } from "@/lib/workbench/useAiConversation"; import { ChoiceChips } from "./ChoiceChips"; // 门控阈值:整章意见 trim 后短于此字数才自动预检反问(清晰意见不交往返税)。 const CLARIFY_MIN_CHARS = 10; interface ChapterRewritePanelProps { projectId: string; chapterNo: number; // 当前整章正文(首轮重写的 prior_draft)。 currentDraft: string; // 接受某一版 → 替换整章正文(HITL,作者显式点选才落草稿)。 onAccept: (text: string) => void; // 留痕:每版重写记 [author(折后意见), ai(版本正文)],含缓冲的 clarify Q&A;同 thread 迭代。 appendMessages: (turn: AiTurnInput) => void; onClose: () => void; } // 整章「再沟通 / 重写」结果卡(内联,非模态):作者给意见 → 意见含糊/极短时 AI 先反问给方向选项(预检、只读、 // HITL)→ 就着当前整章 + 完整记忆注入流式重写一版 → 版本栈可多轮迭代 → 接受某版才替换整章正文(不变量 #3)。 export function ChapterRewritePanel({ projectId, chapterNo, currentDraft, onAccept, appendMessages, onClose, }: ChapterRewritePanelProps) { const rewrite = useChapterRewrite(); const clarify = useRewriteClarify(); const [feedback, setFeedback] = useState(""); const [pendingFeedback, setPendingFeedback] = useState(null); // 本面板一次会话 = 一个 thread;多版迭代 + 澄清往复归此组,版本号本地累加。 const threadId = useRef(crypto.randomUUID()).current; const versionNoRef = useRef(0); const hasVersions = rewrite.versions.length > 0; const clarifying = clarify.status === "checking"; const canSend = feedback.trim().length > 0 && !rewrite.isStreaming && !clarifying; const firstQuestion = clarify.decision?.questions[0] ?? null; const asking = clarify.status === "asking" && firstQuestion !== null; // 下一轮以最新版正文为基础(无版本则用当前整章正文)迭代。 const priorDraft = rewrite.latest?.text ?? currentDraft; const err = rewrite.state.phase === "error" ? rewrite.state.error : null; // 折入澄清答案(或原意见)后流式重写,结束后留痕;clarifyTurns 非空时把缓冲 Q&A 一并落库。 const runSend = (fb: string, clarifyTurns: AiTurnMessage[] | null): void => { clarify.reset(); setPendingFeedback(null); setFeedback(""); void (async () => { const v = await rewrite.send(projectId, chapterNo, fb, priorDraft); if (!v) return; // 停止/失败/空版:不留痕。 versionNoRef.current += 1; const versionNo = versionNoRef.current; const aiRow: AiTurnMessage = { role: "ai", content: v.text, meta: { version_no: versionNo }, }; // clarify 分支:作者答复 IS 本轮 author 输入;否则记 author(折后意见, meta{version_no})。 const messages = clarifyTurns ? [...clarifyTurns, aiRow] : [ { role: "author" as const, content: fb, meta: { version_no: versionNo }, }, aiRow, ]; appendMessages({ threadId, kind: "rewrite", chapterNo, messages }); })(); }; // 「重写整章 / 再改一版」:意见含糊/极短(或 force)先预检反问;否则直接流式重写。 const onSend = async (force: boolean): Promise => { const fb = feedback.trim(); if (!fb || rewrite.isStreaming || clarifying) return; if (force || needsClarifyGate(fb, CLARIFY_MIN_CHARS)) { const vm = await clarify.check(projectId, chapterNo, fb, priorDraft); if (vm.needClarification && vm.questions.length > 0) { setPendingFeedback(fb); // 记住原意见,等作者答完折进去 return; } } runSend(fb, null); }; // 作者答复澄清(点选 value 或自由输入)→ 缓冲 Q&A(原意见→反问→答复)+ 折进意见 → 流式重写。 const onClarifyAnswer = (answer: string): void => { const q = clarify.decision?.questions[0]; const question = q?.question ?? ""; const base = pendingFeedback ?? feedback.trim(); const clarifyTurns: AiTurnMessage[] = [ { role: "author", content: base }, { role: "ai", content: question, meta: { parent_kind: "rewrite", 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 }, ]; runSend(foldClarifications(base, [{ question, answer }]), clarifyTurns); }; return (