import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { invoke } from '@tauri-apps/api/core' import './web-novel.css' import { AuthorModuleCenter } from './AuthorModuleCenter' import { AuthorWritingSidecar } from './AuthorWritingSidecar' type WorkspaceMode = 'writing' | 'modules' | 'story' | 'review' | 'operations' | 'settings' interface WorkSummary { workId: string title: string penName: string genre: string workKind: 'LONG_NOVEL' | 'SHORT_NOVEL' | 'SHORT_DRAMA' workflowStatus: string targetWords: number totalWords: number chapterCount: number publishedChapterCount: number revision: number updatedAtUnixMs: number } interface Work { workId: string title: string penName: string genre: string workKind: 'LONG_NOVEL' | 'SHORT_NOVEL' | 'SHORT_DRAMA' synopsis: string contractStatus: string copyrightStatus: string workflowStatus: string targetWords: number revision: number createdAtUnixMs: number updatedAtUnixMs: number } interface Volume { volumeId: string workId: string title: string position: number revision: number } interface ChapterSummary { chapterId: string volumeId: string workId: string title: string position: number synopsis: string workflowStatus: string scheduledAtUnixMs?: number wordCount: number revision: number updatedAtUnixMs: number } interface Chapter extends ChapterSummary { content: string createdAtUnixMs: number } interface StoryEntity { entityId: string workId: string entityType: string name: string aliases: string[] description: string firstChapterId?: string revision: number } interface StoryRelation { relationId: string sourceEntityId: string targetEntityId: string relationType: string note: string } interface Foreshadow { foreshadowId: string title: string setupChapterId: string payoffChapterId?: string status: string note: string revision: number } interface ReviewNote { noteId: string chapterId: string note: string status: string createdAtUnixMs: number } interface WorkflowEvent { eventId: string chapterId: string fromStatus: string toStatus: string note: string createdAtUnixMs: number } interface Checkpoint { checkpointId: string name: string description: string chapterCount: number wordCount: number createdAtUnixMs: number } interface Metric { metricId: string metricDate: string views: number follows: number comments: number paidReaders: number revenueCents: number sourceLabel: string revision: number } interface Operations { totalWords: number draftChapters: number editorReviewChapters: number approvedChapters: number scheduledChapters: number publishedChapters: number openReviewNotes: number openForeshadows: number latestViews: number latestFollows: number latestComments: number latestPaidReaders: number latestRevenueCents: number } interface WorkDetail { work: Work volumes: Volume[] chapters: ChapterSummary[] entities: StoryEntity[] relations: StoryRelation[] foreshadows: Foreshadow[] reviewNotes: ReviewNote[] workflowEvents: WorkflowEvent[] checkpoints: Checkpoint[] metrics: Metric[] operations: Operations } interface WorkspaceSnapshot { state: string works: WorkSummary[] workCount: number storage: string authority: string } interface ContinuityIssue { issueId: string severity: string code: string title: string detail: string objectId: string } interface ContinuityAudit { state: string issueCount: number blockingIssueCount: number warningIssueCount: number issues: ContinuityIssue[] checkedAtUnixMs: number } interface ImportSectionPreview { ordinal: number title: string wordCount: number sceneCount: number preview: string } interface DocumentImportPreview { importId: string sourceFilename: string sourceFormat: string sourceSha256: string sourceBytes: number detectedFamily: 'NOVEL' | 'OUTLINE' | 'SCRIPT' detectedTitle: string detectedPenName: string detectedGenre: string sectionCount: number totalWordCount: number prefaceWordCount: number sections: ImportSectionPreview[] } interface DocumentImportReceipt { workId: string chapterCount: number wordCount: number firstChapterTitle: string lastChapterTitle: string } const EMPTY_OPERATIONS: Operations = { totalWords: 0, draftChapters: 0, editorReviewChapters: 0, approvedChapters: 0, scheduledChapters: 0, publishedChapters: 0, openReviewNotes: 0, openForeshadows: 0, latestViews: 0, latestFollows: 0, latestComments: 0, latestPaidReaders: 0, latestRevenueCents: 0, } const STATUS_LABELS: Record = { DRAFT: '草稿', SELF_REVIEW: '作者自检', EDITOR_REVIEW: '编辑审核', REVISION_REQUIRED: '退修', APPROVED: '审核通过', SCHEDULED: '待发布', PUBLISHED: '已发布', } const NEXT_STATUS: Record = { DRAFT: ['SELF_REVIEW'], SELF_REVIEW: ['DRAFT', 'EDITOR_REVIEW'], EDITOR_REVIEW: ['REVISION_REQUIRED', 'APPROVED'], REVISION_REQUIRED: ['DRAFT', 'EDITOR_REVIEW'], APPROVED: ['REVISION_REQUIRED', 'SCHEDULED', 'PUBLISHED'], SCHEDULED: ['APPROVED', 'PUBLISHED'], PUBLISHED: ['REVISION_REQUIRED'], } function errorMessage(error: unknown) { const raw = error instanceof Error ? error.message : String(error) return raw.replace(/^HOLOLAKE_WEBNOVEL_[A-Z_]+:\s*/, '') } function formatTime(value: number) { return new Date(value).toLocaleString('zh-CN', { hour12: false }) } function statusLabel(value: string) { return STATUS_LABELS[value] || value } function outlineRows(content: string) { return content.split(/\n+/).map((raw, index) => { const line = raw.trim() const field = line.match(/^([^::]{1,18})[::]\s*(.*)$/) if (field) return { key: index, kind: 'field', label: field[1].trim(), value: field[2].trim() } if (/^[-—•·*①②③④⑤⑥⑦⑧⑨⑩]/.test(line)) return { key: index, kind: 'beat', label: '情节点', value: line.replace(/^[-—•·*]\s*/, '') } return { key: index, kind: 'note', label: '叙事说明', value: line } }).filter((item) => item.value) } export function WebNovelWorkspace({ onBack }: { onBack: () => void }) { const [snapshot, setSnapshot] = useState(null) const [detail, setDetail] = useState(null) const [activeChapter, setActiveChapter] = useState(null) const [mode, setMode] = useState('writing') const [busy, setBusy] = useState(false) const [message, setMessage] = useState('正在打开当前账号的网文工作空间…') const [createFeedback, setCreateFeedback] = useState<{ state: 'idle' | 'pending' | 'success' | 'error'; text: string }>({ state: 'idle', text: '' }) const [audit, setAudit] = useState(null) const [newWork, setNewWork] = useState({ title: '', penName: '', genre: '', workKind: 'LONG_NOVEL' as 'LONG_NOVEL' | 'SHORT_NOVEL' | 'SHORT_DRAMA' }) const [entityDraft, setEntityDraft] = useState({ entityType: 'CHARACTER', name: '', aliases: '', description: '' }) const [foreshadowDraft, setForeshadowDraft] = useState({ title: '', setupChapterId: '', payoffChapterId: '', note: '' }) const [relationDraft, setRelationDraft] = useState({ sourceEntityId: '', targetEntityId: '', relationType: '人物关系', note: '' }) const [reviewDraft, setReviewDraft] = useState('') const [metricDraft, setMetricDraft] = useState({ metricDate: new Date().toISOString().slice(0, 10), views: '0', follows: '0', comments: '0', paidReaders: '0', revenueYuan: '0', sourceLabel: '人工录入' }) const [importPreview, setImportPreview] = useState(null) const [importDraft, setImportDraft] = useState({ targetMode: 'CREATE_NEW', targetWorkId: '', title: '', penName: '', genre: '' }) const chapterRef = useRef(null) const dirtyRef = useRef(false) const saveTimer = useRef(null) const saveQueue = useRef>(Promise.resolve()) const flushRef = useRef<() => Promise>(async () => undefined) const activityRef = useRef({ activityMs: 0, wordsDelta: 0, lastTypedAt: 0, workId: '', chapterId: '' }) const flushWritingActivity = useCallback(async () => { const pending = activityRef.current if (!pending.workId || !pending.chapterId || pending.activityMs < 1000) return activityRef.current = { ...pending, activityMs: 0, wordsDelta: 0 } const now = new Date() const localDate = new Date(now.getTime() - now.getTimezoneOffset() * 60_000).toISOString().slice(0, 10) try { await invoke('record_web_novel_writing_activity', { input: { activityId: `WN-ACT-${crypto.randomUUID()}`, workId: pending.workId, chapterId: pending.chapterId, localDate, activeMs: Math.min(300_000, Math.round(pending.activityMs)), wordsDelta: Math.max(-100_000, Math.min(100_000, pending.wordsDelta)), } }) } catch { activityRef.current.activityMs += pending.activityMs activityRef.current.wordsDelta += pending.wordsDelta } }, []) const refreshSnapshot = useCallback(async (preferWorkId?: string) => { const next = await invoke('get_web_novel_workspace_snapshot') setSnapshot(next) const workId = preferWorkId || detail?.work.workId || next.works[0]?.workId if (workId) { const nextDetail = await invoke('read_web_novel_work', { input: { workId } }) setDetail(nextDetail) } else { setDetail(null) } return next }, [detail?.work.workId]) useEffect(() => { let cancelled = false invoke('get_web_novel_workspace_snapshot') .then(async (next) => { if (cancelled) return setSnapshot(next) if (next.works[0]) { const nextDetail = await invoke('read_web_novel_work', { input: { workId: next.works[0].workId } }) if (!cancelled) setDetail(nextDetail) } if (!cancelled) setMessage(next.works.length ? '工作空间已加载' : '当前账号还没有作品,请先创建一本。') }) .catch((error) => !cancelled && setMessage(`无法打开工作空间:${errorMessage(error)}`)) return () => { cancelled = true if (saveTimer.current !== null) window.clearTimeout(saveTimer.current) void flushWritingActivity() void flushRef.current() } }, [flushWritingActivity]) useEffect(() => { const timer = window.setInterval(() => void flushWritingActivity(), 30_000) return () => window.clearInterval(timer) }, [flushWritingActivity]) useEffect(() => { chapterRef.current = activeChapter }, [activeChapter]) const flushChapterSave = useCallback(async () => { if (saveTimer.current !== null) { window.clearTimeout(saveTimer.current) saveTimer.current = null } if (!chapterRef.current || !dirtyRef.current) return saveQueue.current saveQueue.current = saveQueue.current.then(async () => { const draft = chapterRef.current if (!draft || !dirtyRef.current) return dirtyRef.current = false const payload = { chapterId: draft.chapterId, title: draft.title, synopsis: draft.synopsis, content: draft.content, expectedRevision: draft.revision, saveReason: 'AUTOSAVE', } try { const saved = await invoke('save_web_novel_chapter', { input: payload }) setActiveChapter((current) => { if (!current || current.chapterId !== saved.chapterId) return current const unchanged = current.title === payload.title && current.synopsis === payload.synopsis && current.content === payload.content const next = unchanged ? saved : { ...current, revision: saved.revision, wordCount: saved.wordCount, updatedAtUnixMs: saved.updatedAtUnixMs } chapterRef.current = next return next }) setDetail((current) => current ? { ...current, chapters: current.chapters.map((chapter) => chapter.chapterId === saved.chapterId ? saved : chapter), operations: { ...current.operations, totalWords: current.operations.totalWords - (current.chapters.find((chapter) => chapter.chapterId === saved.chapterId)?.wordCount || 0) + saved.wordCount }, } : current) setMessage(`已保存 · 修订 ${saved.revision}`) } catch (error) { dirtyRef.current = true setMessage(`保存失败:${errorMessage(error)}`) } }) return saveQueue.current }, []) flushRef.current = flushChapterSave const updateChapter = (patch: Partial) => { setActiveChapter((current) => { if (!current) return current if (typeof patch.content === 'string' && patch.content !== current.content) { const now = Date.now() const pending = activityRef.current const sameChapter = pending.chapterId === current.chapterId const elapsed = sameChapter && pending.lastTypedAt && now - pending.lastTypedAt < 5_000 ? now - pending.lastTypedAt : 1_000 const count = (value: string) => Array.from(value).filter((character) => !/\s/.test(character)).length activityRef.current = { activityMs: (sameChapter ? pending.activityMs : 0) + elapsed, wordsDelta: (sameChapter ? pending.wordsDelta : 0) + count(patch.content) - count(current.content), lastTypedAt: now, workId: current.workId, chapterId: current.chapterId, } if (activityRef.current.activityMs >= 15_000) void flushWritingActivity() } const next = { ...current, ...patch } chapterRef.current = next return next }) dirtyRef.current = true setMessage('正在保存…') if (saveTimer.current !== null) window.clearTimeout(saveTimer.current) saveTimer.current = window.setTimeout(() => void flushChapterSave(), 650) } const runAction = async (action: () => Promise) => { setBusy(true) try { await action() } catch (error) { setMessage(errorMessage(error)) } finally { setBusy(false) } } const selectWork = (workId: string) => void runAction(async () => { await flushChapterSave() const next = await invoke('read_web_novel_work', { input: { workId } }) setDetail(next) setActiveChapter(null) setAudit(null) setMessage(`已打开《${next.work.title}》`) }) const createWork = async () => { if (!newWork.title.trim() || !newWork.penName.trim() || !newWork.genre.trim()) { const text = '请把作品名、笔名和类型填写完整。' setCreateFeedback({ state: 'error', text }) setMessage(text) return } setBusy(true) setCreateFeedback({ state: 'pending', text: '正在建立作品与第一卷…' }) try { const next = await invoke('create_web_novel_work', { input: newWork }) setDetail(next) setNewWork({ title: '', penName: '', genre: '', workKind: 'LONG_NOVEL' }) await refreshSnapshot(next.work.workId) const text = `《${next.work.title}》已创建,第一卷已经可以开始写作。` setCreateFeedback({ state: 'success', text }) setMessage(text) } catch (error) { const text = `作品没有创建:${errorMessage(error)}` setCreateFeedback({ state: 'error', text }) setMessage(text) } finally { setBusy(false) } } const inspectDocument = () => void runAction(async () => { const preview = await invoke('inspect_web_novel_document_from_dialog') if (!preview) { setMessage('已取消选择文档。') return } setImportPreview(preview) setImportDraft({ targetMode: 'CREATE_NEW', targetWorkId: detail?.work.workId || '', title: preview.detectedTitle, penName: preview.detectedPenName, genre: preview.detectedGenre, }) setMessage(`已真实读取「${preview.sourceFilename}」,等待确认拆分结果。`) }) const commitDocumentImport = () => void runAction(async () => { if (!importPreview) return if (importDraft.targetMode === 'CREATE_NEW' && (!importDraft.title.trim() || !importDraft.penName.trim() || !importDraft.genre.trim())) { setMessage('请确认作品名、笔名和类型。') return } if (importDraft.targetMode === 'APPEND_EXISTING' && !importDraft.targetWorkId) { setMessage('请选择要追加到的作品。') return } const receipt = await invoke('commit_web_novel_document_import', { input: { importId: importPreview.importId, ...importDraft, targetWorkId: importDraft.targetWorkId || null, } }) setImportPreview(null) const next = await refreshSnapshot(receipt.workId) const importedDetail = await invoke('read_web_novel_work', { input: { workId: receipt.workId } }) setDetail(importedDetail) const firstImported = importedDetail.chapters.find((chapter) => chapter.title === receipt.firstChapterTitle) if (firstImported) { const chapter = await invoke('read_web_novel_chapter', { input: { chapterId: firstImported.chapterId } }) setActiveChapter(chapter) chapterRef.current = chapter } setMode('writing') setSnapshot(next) setMessage(`导入完成:${receipt.chapterCount} 个分段、${receipt.wordCount.toLocaleString()} 字,首章已从数据库重新打开。`) }) const createVolume = () => void runAction(async () => { if (!detail) return const title = window.prompt('新卷名称', `第${detail.volumes.length + 1}卷`) if (!title?.trim()) return const next = await invoke('create_web_novel_volume', { input: { workId: detail.work.workId, title } }) setDetail(next) setMessage(`已创建「${title}」`) }) const createChapter = (volumeId: string) => void runAction(async () => { if (!detail) return await flushWritingActivity() await flushChapterSave() const count = detail.chapters.filter((item) => item.volumeId === volumeId).length const unit = detail.work.workKind === 'SHORT_DRAMA' ? '集' : '章' const title = window.prompt(`新${unit}名称`, `第${count + 1}${unit}`) if (!title?.trim()) return const chapter = await invoke('create_web_novel_chapter', { input: { workId: detail.work.workId, volumeId, title } }) setActiveChapter(chapter) chapterRef.current = chapter dirtyRef.current = false const next = await invoke('read_web_novel_work', { input: { workId: detail.work.workId } }) setDetail(next) setMode('writing') setMessage('章节已创建,正文会自动保存并生成版本。') }) const openChapter = (chapterId: string) => void runAction(async () => { await flushWritingActivity() await flushChapterSave() const chapter = await invoke('read_web_novel_chapter', { input: { chapterId } }) setActiveChapter(chapter) chapterRef.current = chapter dirtyRef.current = false setMode('writing') setMessage(`已打开「${chapter.title}」`) }) const formatActiveChapter = async (preset: string) => { await flushChapterSave() const chapter = chapterRef.current if (!chapter) return const saved = await invoke('format_web_novel_chapter', { input: { chapterId: chapter.chapterId, expectedRevision: chapter.revision, preset } }) setActiveChapter(saved) chapterRef.current = saved setDetail(await invoke('read_web_novel_work', { input: { workId: saved.workId } })) setMessage(`一键排版完成:段落之间已留一行;原稿已保留,当前为修订 ${saved.revision}。`) } const formatWholeWork = async (preset: string) => { await flushChapterSave() if (!detail) return const receipt = await invoke<{ formattedChapterCount: number }>('format_web_novel_work', { input: { workId: detail.work.workId, preset } }) const next = await invoke('read_web_novel_work', { input: { workId: detail.work.workId } }) setDetail(next) if (chapterRef.current) { const chapter = await invoke('read_web_novel_chapter', { input: { chapterId: chapterRef.current.chapterId } }) setActiveChapter(chapter) chapterRef.current = chapter } setMessage(`整部作品排版完成:${receipt.formattedChapterCount} 章已处理,每章原稿均保留在版本链。`) } const transitionChapter = (toStatus: string) => void runAction(async () => { await flushChapterSave() const chapter = chapterRef.current if (!chapter) return const note = window.prompt(`转为“${statusLabel(toStatus)}”的说明`, '') ?? '' let scheduledAtUnixMs: number | undefined if (toStatus === 'SCHEDULED') { const raw = window.prompt('计划发布时间(例如 2026-08-20 20:00)', '') if (!raw) return scheduledAtUnixMs = new Date(raw.replace(' ', 'T')).getTime() if (!Number.isFinite(scheduledAtUnixMs)) { setMessage('发布时间格式无法识别。') return } } const saved = await invoke('transition_web_novel_chapter', { input: { chapterId: chapter.chapterId, toStatus, scheduledAtUnixMs, note, expectedRevision: chapter.revision } }) setActiveChapter(saved) chapterRef.current = saved const next = await invoke('read_web_novel_work', { input: { workId: saved.workId } }) setDetail(next) setMessage(`章节已进入“${statusLabel(saved.workflowStatus)}”`) }) const saveWorkSettings = () => void runAction(async () => { if (!detail) return const next = await invoke('save_web_novel_work', { input: { workId: detail.work.workId, title: detail.work.title, penName: detail.work.penName, genre: detail.work.genre, workKind: detail.work.workKind, synopsis: detail.work.synopsis, contractStatus: detail.work.contractStatus, copyrightStatus: detail.work.copyrightStatus, targetWords: detail.work.targetWords, expectedRevision: detail.work.revision, } }) setDetail(next) await refreshSnapshot(next.work.workId) setMessage('作品资料已保存。') }) const createEntity = () => void runAction(async () => { if (!detail || !entityDraft.name.trim()) return await invoke('upsert_web_novel_story_entity', { input: { workId: detail.work.workId, entityId: null, entityType: entityDraft.entityType, name: entityDraft.name, aliases: entityDraft.aliases.split(/[,,]/).map((item) => item.trim()).filter(Boolean), description: entityDraft.description, firstChapterId: activeChapter?.chapterId || null, expectedRevision: null, } }) setEntityDraft({ entityType: 'CHARACTER', name: '', aliases: '', description: '' }) await refreshSnapshot(detail.work.workId) setMessage('设定卡已写入作品资料库。') }) const createRelation = () => void runAction(async () => { if (!detail || !relationDraft.sourceEntityId || !relationDraft.targetEntityId) return await invoke('create_web_novel_story_relation', { input: { workId: detail.work.workId, ...relationDraft } }) setRelationDraft({ sourceEntityId: '', targetEntityId: '', relationType: '人物关系', note: '' }) await refreshSnapshot(detail.work.workId) setMessage('对象关系已建立。') }) const createForeshadow = () => void runAction(async () => { if (!detail || !foreshadowDraft.title.trim() || !foreshadowDraft.setupChapterId) return await invoke('upsert_web_novel_foreshadow', { input: { workId: detail.work.workId, foreshadowId: null, title: foreshadowDraft.title, setupChapterId: foreshadowDraft.setupChapterId, payoffChapterId: foreshadowDraft.payoffChapterId || null, status: foreshadowDraft.payoffChapterId ? 'PAID_OFF' : 'OPEN', note: foreshadowDraft.note, expectedRevision: null, } }) setForeshadowDraft({ title: '', setupChapterId: '', payoffChapterId: '', note: '' }) await refreshSnapshot(detail.work.workId) setMessage('伏笔已登记,可进入连续性检查。') }) const createReviewNote = () => void runAction(async () => { if (!detail || !activeChapter || !reviewDraft.trim()) return await invoke('create_web_novel_review_note', { input: { workId: detail.work.workId, chapterId: activeChapter.chapterId, note: reviewDraft } }) setReviewDraft('') await refreshSnapshot(detail.work.workId) setMessage('编辑意见已记录。') }) const resolveReviewNote = (noteId: string) => void runAction(async () => { if (!detail) return await invoke('resolve_web_novel_review_note', { input: { noteId } }) await refreshSnapshot(detail.work.workId) setMessage('编辑意见已解决。') }) const createCheckpoint = () => void runAction(async () => { if (!detail) return await flushChapterSave() const name = window.prompt('检查点名称', `手动检查点 · ${new Date().toLocaleString('zh-CN')}`) if (!name?.trim()) return await invoke('create_web_novel_checkpoint', { input: { workId: detail.work.workId, name, description: '由工作台手动创建' } }) await refreshSnapshot(detail.work.workId) setMessage('整部作品检查点已创建。') }) const restoreCheckpoint = (checkpoint: Checkpoint) => void runAction(async () => { if (!window.confirm(`确定恢复“${checkpoint.name}”吗?当前章节内容会先保留在版本记录中。`)) return const next = await invoke('restore_web_novel_checkpoint', { input: { checkpointId: checkpoint.checkpointId } }) setDetail(next) setActiveChapter(null) setMessage('检查点已恢复,当前编辑器已关闭以避免旧修订覆盖。') }) const runAudit = () => void runAction(async () => { if (!detail) return await flushChapterSave() const next = await invoke('run_web_novel_continuity_audit', { input: { workId: detail.work.workId } }) setAudit(next) setMessage(next.state === 'PASS' ? '连续性检查通过。' : `发现 ${next.issueCount} 个需要处理的问题。`) }) const saveMetric = () => void runAction(async () => { if (!detail) return const integer = (value: string) => Math.max(0, Math.floor(Number(value) || 0)) await invoke('save_web_novel_metric', { input: { workId: detail.work.workId, metricDate: metricDraft.metricDate, views: integer(metricDraft.views), follows: integer(metricDraft.follows), comments: integer(metricDraft.comments), paidReaders: integer(metricDraft.paidReaders), revenueCents: Math.round(Math.max(0, Number(metricDraft.revenueYuan) || 0) * 100), sourceLabel: metricDraft.sourceLabel, expectedRevision: null, } }) await refreshSnapshot(detail.work.workId) setMessage('运营数据已写入当前账号。') }) const exportMarkdown = () => void runAction(async () => { if (!detail) return await flushChapterSave() const receipt = await invoke<{ path: string; chapterCount: number; wordCount: number } | null>('export_web_novel_markdown', { input: { workId: detail.work.workId } }) setMessage(receipt ? `已导出 ${receipt.chapterCount} 章、${receipt.wordCount} 字。` : '已取消导出。') }) const chapterName = useMemo(() => new Map(detail?.chapters.map((chapter) => [chapter.chapterId, chapter.title]) || []), [detail?.chapters]) const entityName = useMemo(() => new Map(detail?.entities.map((entity) => [entity.entityId, entity.name]) || []), [detail?.entities]) const activeVolume = detail?.volumes.find((volume) => volume.volumeId === activeChapter?.volumeId) const isOutlineChapter = Boolean(activeVolume?.title.includes('细纲') || detail?.work.genre.includes('细纲')) const activeOutlineRows = useMemo(() => outlineRows(activeChapter?.content || ''), [activeChapter?.content]) if (!snapshot) { return

正在唤醒网文工作引擎

{message}

} return
WEB NOVEL / LOCAL ENGINE{detail ? `《${detail.work.title}》` : '轻量作者工作台 · 模块拆分验收副本'}
{busy ? '引擎正在处理…' : message}
{detail ? <>
{mode === 'writing' &&
{activeChapter ?
updateChapter({ title: event.target.value })}/>
{statusLabel(activeChapter.workflowStatus)}{Array.from(activeChapter.content).filter((character) => !/\s/.test(character)).length.toLocaleString()} 字 · 修订 {activeChapter.revision}
updateChapter({ synopsis: event.target.value })}/> {isOutlineChapter ?