"use client"; import Link from "next/link"; import { useEffect, useId, useRef, useState, type RefObject } from "react"; import { BookOpen, ChevronDown, ClipboardCheck, FileText, Library, PanelLeft, PanelRight, PenLine, Sparkles, Square, Wand2, WrapText, } from "lucide-react"; import { AppShell } from "@/components/AppShell"; import { Drawer } from "@/components/Drawer"; import { ThinkingIndicator } from "@/components/ThinkingIndicator"; import { useToast } from "@/components/Toast"; 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 type { ProjectResponse } from "@/lib/api/types"; import { friendlyError } from "@/lib/errors/messages"; import { useAutosave } from "@/lib/autosave/useAutosave"; import { useDraftStream } from "@/lib/stream/useDraftStream"; import { WORKBENCH_CHAPTER_NO, type ChapterEntry, } from "@/lib/workbench/chapter"; import { composeDirective, PLOT_PRESETS, STYLE_PRESETS, type Preset, } from "@/lib/workbench/directive"; import { applyRefinement } from "@/lib/workbench/refineApply"; import { buttonClass } from "@/lib/ui/variants"; import { ChapterList, ChapterListContent } from "./ChapterList"; import { ChapterAssistant, AssistantContent } from "./ChapterAssistant"; import { Editor, type EditorSelection } from "./Editor"; import { RefinePanel } from "./RefinePanel"; import { ContinuePanel } from "./ContinuePanel"; import { InlineToolbox } from "./InlineToolbox"; interface WorkbenchProps { project: ProjectResponse; // 当前章号(来自写作路由 `?chapter=N`);缺省回退第 1 章。 chapterNo?: number; // RSC 种入的已存草稿正文(GET .../draft);无草稿(404)→ ""(空编辑器)。 initialText?: string; // 大纲章节目录(GET .../outline);无大纲 → 空数组(目录回退到仅当前章 + 去大纲提示)。 chapters?: ChapterEntry[]; } // 移动端右栏(目录/助手)经抽屉触达;桌面用三栏栅格。 type MobilePanel = "chapters" | "assistant" | null; // M1:单章工作台。章节列表为占位(无章节列表端点);默认第 1 章。 // WORKBENCH_CHAPTER_NO 移到 lib/workbench/chapter.ts(非客户端模块),供 Server Component 安全导入。 export function Workbench({ project, chapterNo = WORKBENCH_CHAPTER_NO, initialText = "", chapters = [], }: WorkbenchProps) { // 用已存草稿作初值;流式开始后由下方 effect 覆盖(流不会被初值反扑—— // stream.state.text 起始为空,仅当 streaming/done/aborted 且变化才写回)。 const [text, setText] = useState(initialText); const [mobilePanel, setMobilePanel] = useState(null); // 本章指令(T4-b):自由文本 + 风格预设 → 写本章时组装传入 draft 生成。 const [directive, setDirective] = useState(""); const [presetIds, setPresetIds] = useState([]); // 选区级 AI 动作(润色/再沟通):编辑器上报的最新选区 + 结果卡开合。 const [selection, setSelection] = useState(null); const [refineOpen, setRefineOpen] = useState(false); const [continueOpen, setContinueOpen] = useState(false); const [toolboxOpen, setToolboxOpen] = useState(false); const toast = useToast(); const autosave = useAutosave(project.id, chapterNo, initialText); const stream = useDraftStream(); const lastStreamText = useRef(""); const canRefine = selection !== null && selection.text.trim().length > 0; const chapterTriggerRef = useRef(null); const assistantTriggerRef = useRef(null); // 流式 token 累积进编辑器(打字机);停止/结束后已生成部分留在草稿。 useEffect(() => { const streamed = stream.state.text; if ( (stream.state.phase === "streaming" || stream.state.phase === "done" || stream.state.phase === "aborted") && streamed !== lastStreamText.current ) { lastStreamText.current = streamed; setText(streamed); autosave.onChange(streamed); } }, [stream.state.phase, stream.state.text, autosave]); const onWrite = (): void => { void stream.start( project.id, chapterNo, composeDirective(directive, presetIds), ); }; const togglePreset = (id: string): void => { setPresetIds((prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id], ); }; const onEditorChange = (value: string): void => { setText(value); autosave.onChange(value); }; // 接受润色/再沟通产出:按内容重锚把选段替换为产出并落草稿;原文已变则提示重选(不盲替换)。 const onAcceptRefine = (refined: string): void => { if (selection) { const next = applyRefinement(text, selection.text, refined, { start: selection.start, end: selection.end, }); if (next === null) { toast("正文已改动,找不到原选段,请重新选择后再润色。", "error"); } else { setText(next); autosave.onChange(next); } } setRefineOpen(false); setSelection(null); }; // 三张 AI 结果卡(润色/续写/工具箱)互斥,避免同时占位。 const openRefine = (): void => { setContinueOpen(false); setToolboxOpen(false); setRefineOpen(true); }; // 续写前先 flush 自动保存,确保 continue 读到的是最新草稿(with_prior_chapter 读库)。 const openContinue = (): void => { autosave.flush(); setRefineOpen(false); setToolboxOpen(false); setContinueOpen(true); }; const openToolbox = (): void => { setRefineOpen(false); setContinueOpen(false); setToolboxOpen(true); }; // 追加一段文本到章末并落草稿(续写候选 / 工具箱产物共用)。 const appendToDraft = (chunk: string): void => { const base = text.trimEnd(); const next = base.length > 0 ? `${base}\n\n${chunk}` : chunk; setText(next); autosave.onChange(next); }; const onInsertContinuation = (candidate: string): void => { appendToDraft(candidate); setContinueOpen(false); }; // 字数统计非空白字符(与中文读者直觉一致:标点/空格不计入正文体量)。 const wordCount = text.replace(/\s+/g, "").length; const closePanel = (): void => setMobilePanel(null); // 流式里程碑播报(仅终结句,非逐 token):完成/停止/失败时写入 role=status 区。 const liveMessage = ((): string => { switch (stream.state.phase) { case "done": return `本章生成完成,共 ${wordCount} 字`; case "aborted": return "已停止"; case "error": return stream.state.error ? friendlyError(stream.state.error.code, stream.state.error.message) .text : "本章生成失败,请稍后重试。"; default: return ""; } })(); return (
setMobilePanel("chapters")} onOpenAssistant={() => setMobilePanel("assistant")} chapterTriggerRef={chapterTriggerRef} assistantTriggerRef={assistantTriggerRef} /> {refineOpen && canRefine && selection ? ( setRefineOpen(false)} /> ) : null} {continueOpen ? ( setContinueOpen(false)} /> ) : null} {toolboxOpen ? ( { appendToDraft(chunk); setToolboxOpen(false); }} onClose={() => setToolboxOpen(false)} /> ) : null}
{/* 窄屏 / 中屏(1024–1280):目录 / 本章助手经抽屉触达;≥xl 已在三栏栅格里直显。 */}
); } interface MobileContextBarProps { chapterNo: number; onOpenChapters: () => void; onOpenAssistant: () => void; chapterTriggerRef: RefObject; assistantTriggerRef: RefObject; } function MobileContextBar({ chapterNo, onOpenChapters, onOpenAssistant, chapterTriggerRef, assistantTriggerRef, }: MobileContextBarProps) { return (
第 {chapterNo} 章
); } interface DirectivePanelProps { directive: string; onDirectiveChange: (value: string) => void; presetIds: readonly string[]; onTogglePreset: (id: string) => void; } // 本章指令面板(T4-b):可折叠的 textarea + 风格快捷预设 chips。 // 默认折叠(不抢编辑视野);指令覆盖/补充大纲节拍,写本章时组装传入生成。 function DirectivePanel({ directive, onDirectiveChange, presetIds, onTogglePreset, }: DirectivePanelProps) { const panelId = useId(); const activeCount = presetIds.length + (directive.trim().length > 0 ? 1 : 0); return (
{activeCount > 0 ? `${activeCount} 项指令` : "可选"}