"use client"; import Link from "next/link"; import { useEffect, useRef, useState } from "react"; import { AppShell } from "@/components/AppShell"; 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 { ChapterList } from "./ChapterList"; import { ChapterAssistant } from "./ChapterAssistant"; import { Editor } from "./Editor"; interface WorkbenchProps { project: ProjectResponse; // RSC 种入的已存草稿正文(GET .../draft);无草稿(404)→ ""(空编辑器)。 initialText?: string; } // 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 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); }; const onEditorChange = (value: string): void => { setText(value); autosave.onChange(value); }; const wordCount = text.length; return (
); } // 写章失败提示:友好文案(不暴露 raw code)+ 可选「去设置」动作(F4)。 function StreamErrorNote({ streamError, }: { streamError: { code: string; message: string }; }) { const friendly = friendlyError(streamError.code, streamError.message); return (

{friendly.text} {friendly.actionHref ? ( <> {" "} {friendly.actionLabel} ) : null}

); } interface ToolbarProps { projectId: string; chapterNo: number; wordCount: number; savedLabel: string | null; saveStatus: ReturnType["status"]; streaming: boolean; streamError: { code: string; message: string } | null; onWrite: () => void; onStop: () => void; } function Toolbar({ projectId, chapterNo, wordCount, savedLabel, saveStatus, streaming, streamError, onWrite, onStop, }: ToolbarProps) { return (
{streamError ? : null}
{streaming ? ( ) : ( )} {streaming ? ( 生成中… {wordCount.toLocaleString("en-US")} 字 ) : ( 字数 {wordCount.toLocaleString("en-US")} )} 大纲 伏笔 审稿 → {saveStatus === "saving" ? "保存中…" : saveStatus === "error" ? "保存失败" : (savedLabel ?? "尚未保存")}
); }