"use client"; import Link from "next/link"; import { useEffect, useRef, useState } from "react"; import { AppShell } from "@/components/AppShell"; import { Drawer } from "@/components/Drawer"; 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 } from "@/lib/workbench/chapter"; import { composeDirective, STYLE_PRESETS } from "@/lib/workbench/directive"; import { ChapterList, ChapterListContent } from "./ChapterList"; import { ChapterAssistant, AssistantContent } from "./ChapterAssistant"; import { Editor } from "./Editor"; interface WorkbenchProps { project: ProjectResponse; // RSC 种入的已存草稿正文(GET .../draft);无草稿(404)→ ""(空编辑器)。 initialText?: string; } // 移动端右栏(目录/助手)经抽屉触达;桌面用三栏栅格。 type MobilePanel = "chapters" | "assistant" | null; // M1:单章工作台。章节列表为占位(无章节列表端点);默认第 1 章。 // WORKBENCH_CHAPTER_NO 移到 lib/workbench/chapter.ts(非客户端模块),供 Server Component 安全导入。 export function Workbench({ project, initialText = "" }: WorkbenchProps) { const chapterNo = WORKBENCH_CHAPTER_NO; // 用已存草稿作初值;流式开始后由下方 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([]); const autosave = useAutosave(project.id, chapterNo); const stream = useDraftStream(); const lastStreamText = useRef(""); // 流式 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 wordCount = text.length; const closePanel = (): void => setMobilePanel(null); return (
setMobilePanel("chapters")} onOpenAssistant={() => setMobilePanel("assistant")} />
{/* 移动端:目录 / 本章助手经抽屉触达(桌面已在栅格里,抽屉 lg:hidden)。 */}
); } 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) { return (
本章指令(可选)