@waterwx/dsh-novel-forge 1.10.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/README.md +287 -281
  2. package/lib/client.js +19572 -18127
  3. package/lib/index.js +12171 -6932
  4. package/lib/types/action-guard.d.ts +61 -0
  5. package/lib/types/ai-scan.d.ts +30 -0
  6. package/lib/types/assets.d.ts +20 -9
  7. package/lib/types/assistant.d.ts +29 -0
  8. package/lib/types/author-assets.d.ts +1 -3
  9. package/lib/types/bookshelf.d.ts +13 -3
  10. package/lib/types/client/api.d.ts +137 -21
  11. package/lib/types/client/chip-prefs.d.ts +36 -0
  12. package/lib/types/client/docx.d.ts +2 -2
  13. package/lib/types/client/index.d.ts +1 -1
  14. package/lib/types/client/llmLive.d.ts +31 -0
  15. package/lib/types/client/locales.d.ts +15 -70
  16. package/lib/types/client/mount.d.ts +0 -2
  17. package/lib/types/client/panel/AiTag.d.ts +1 -0
  18. package/lib/types/client/panel/AssistantTab.d.ts +4 -1
  19. package/lib/types/client/panel/AuthorHome.d.ts +13 -9
  20. package/lib/types/client/panel/BookAnalysisView.d.ts +4 -0
  21. package/lib/types/client/panel/CmdPalette.d.ts +12 -0
  22. package/lib/types/client/panel/CreateBookView.d.ts +5 -1
  23. package/lib/types/client/panel/DirectorView.d.ts +7 -0
  24. package/lib/types/client/panel/IdeaInspirationView.d.ts +6 -0
  25. package/lib/types/client/panel/KnowledgeBaseView.d.ts +4 -0
  26. package/lib/types/client/panel/LiveFeedLog.d.ts +13 -0
  27. package/lib/types/client/panel/MarketRadarView.d.ts +5 -0
  28. package/lib/types/client/panel/ModelManager.d.ts +2 -2
  29. package/lib/types/client/panel/NovelPanel.d.ts +6 -1
  30. package/lib/types/client/panel/PageHeader.d.ts +18 -0
  31. package/lib/types/client/panel/PromptSlotsView.d.ts +11 -0
  32. package/lib/types/client/panel/SettingsView.d.ts +12 -7
  33. package/lib/types/client/panel/ShelfView.d.ts +10 -1
  34. package/lib/types/client/panel/SubPage.d.ts +27 -0
  35. package/lib/types/client/panel/TensionView.d.ts +18 -0
  36. package/lib/types/client/panel/TimelineView.d.ts +18 -0
  37. package/lib/types/client/panel/helpers.d.ts +5 -3
  38. package/lib/types/client/panel/market-radar-state.d.ts +56 -0
  39. package/lib/types/client/panel/toast.d.ts +2 -0
  40. package/lib/types/client/panel/views.d.ts +21 -35
  41. package/lib/types/client/run-status.d.ts +21 -0
  42. package/lib/types/client/sidebar-entry.d.ts +0 -2
  43. package/lib/types/docx-core.d.ts +16 -0
  44. package/lib/types/docx.d.ts +2 -3
  45. package/lib/types/engine.d.ts +83 -79
  46. package/lib/types/global-assets.d.ts +6 -0
  47. package/lib/types/home.d.ts +11 -0
  48. package/lib/types/index.d.ts +27 -13
  49. package/lib/types/llm-live.d.ts +70 -0
  50. package/lib/types/llm-retry.d.ts +38 -0
  51. package/lib/types/market-radar-scan.d.ts +18 -0
  52. package/lib/types/market-radar-sources.d.ts +31 -0
  53. package/lib/types/migrate-output.d.ts +25 -0
  54. package/lib/types/model-capability.d.ts +93 -0
  55. package/lib/types/novel-context.d.ts +80 -0
  56. package/lib/types/output-budget.d.ts +86 -0
  57. package/lib/types/prompt-slots.d.ts +57 -0
  58. package/lib/types/prompting.d.ts +10 -0
  59. package/lib/types/protocol.d.ts +829 -522
  60. package/lib/types/revision.d.ts +76 -0
  61. package/lib/types/routes.d.ts +9 -3
  62. package/lib/types/run.d.ts +23 -1
  63. package/lib/types/settings-persist.d.ts +59 -0
  64. package/lib/types/snapshots.d.ts +46 -0
  65. package/lib/types/stage-contract.d.ts +58 -0
  66. package/lib/types/tension.d.ts +53 -0
  67. package/lib/types/timeline.d.ts +37 -0
  68. package/package.json +50 -28
  69. package/src/action-guard.ts +127 -0
  70. package/src/ai-scan.ts +155 -0
  71. package/src/assets.ts +1801 -535
  72. package/src/assistant.ts +1098 -770
  73. package/src/author-assets.ts +131 -147
  74. package/src/bookshelf.ts +193 -167
  75. package/src/client/api.ts +767 -607
  76. package/src/client/chip-prefs.ts +121 -0
  77. package/src/client/docx.ts +32 -69
  78. package/src/client/index.ts +3 -1
  79. package/src/client/llmLive.ts +111 -0
  80. package/src/client/locales.ts +15 -274
  81. package/src/client/mount.tsx +4 -3
  82. package/src/client/panel/AdaptModeView.tsx +535 -520
  83. package/src/client/panel/AiTag.tsx +7 -0
  84. package/src/client/panel/AssetsTab.tsx +448 -458
  85. package/src/client/panel/AssistantTab.tsx +355 -301
  86. package/src/client/panel/AuthorAssetsView.tsx +205 -207
  87. package/src/client/panel/AuthorHome.tsx +309 -202
  88. package/src/client/panel/BookAnalysisView.tsx +63 -0
  89. package/src/client/panel/CmdPalette.tsx +102 -0
  90. package/src/client/panel/CreateBookView.tsx +366 -352
  91. package/src/client/panel/DirectorView.tsx +200 -0
  92. package/src/client/panel/GlobalAssetLibraryView.tsx +168 -172
  93. package/src/client/panel/IdeaInspirationView.tsx +76 -0
  94. package/src/client/panel/ImportModal.tsx +302 -302
  95. package/src/client/panel/KnowledgeBaseView.tsx +125 -0
  96. package/src/client/panel/LiveFeedLog.tsx +204 -0
  97. package/src/client/panel/MarketRadarView.tsx +415 -0
  98. package/src/client/panel/ModelManager.tsx +205 -205
  99. package/src/client/panel/NovelPanel.tsx +5559 -4655
  100. package/src/client/panel/PageHeader.tsx +37 -0
  101. package/src/client/panel/ProgressConsole.tsx +63 -64
  102. package/src/client/panel/PromptSlotsView.tsx +97 -0
  103. package/src/client/panel/ReaderView.tsx +306 -306
  104. package/src/client/panel/RunPanel.tsx +56 -24
  105. package/src/client/panel/SettingsView.tsx +175 -112
  106. package/src/client/panel/ShelfView.tsx +63 -39
  107. package/src/client/panel/SubPage.tsx +42 -0
  108. package/src/client/panel/TensionView.tsx +212 -0
  109. package/src/client/panel/TimelineView.tsx +208 -0
  110. package/src/client/panel/WorldTab.tsx +202 -202
  111. package/src/client/panel/helpers.ts +7 -5
  112. package/src/client/panel/market-radar-state.ts +79 -0
  113. package/src/client/panel/panel.module.css +5767 -4199
  114. package/src/client/panel/toast.ts +51 -0
  115. package/src/client/panel/views.tsx +316 -314
  116. package/src/client/run-status.ts +171 -0
  117. package/src/client/sidebar-entry.ts +3 -3
  118. package/src/docx-core.ts +53 -0
  119. package/src/docx.ts +44 -83
  120. package/src/engine.ts +1610 -1541
  121. package/src/global-assets.ts +58 -0
  122. package/src/home.ts +33 -0
  123. package/src/index.ts +184 -103
  124. package/src/llm-live.ts +245 -0
  125. package/src/llm-retry.ts +77 -0
  126. package/src/market-radar-scan.ts +35 -0
  127. package/src/market-radar-sources.ts +173 -0
  128. package/src/migrate-output.ts +76 -0
  129. package/src/model-capability.ts +215 -0
  130. package/src/novel-context.ts +286 -0
  131. package/src/output-budget.ts +147 -0
  132. package/src/prompt-slots.ts +125 -0
  133. package/src/prompting.ts +26 -0
  134. package/src/protocol.ts +2454 -2148
  135. package/src/revision.ts +283 -0
  136. package/src/routes.ts +3795 -3359
  137. package/src/run.ts +113 -17
  138. package/src/settings-persist.ts +114 -0
  139. package/src/snapshots.ts +174 -0
  140. package/src/stage-contract.ts +186 -0
  141. package/src/tension.ts +251 -0
  142. package/src/timeline.ts +159 -0
  143. package/assets/styles/arcane-thick-3d.webp +0 -0
  144. package/assets/styles/chinese-ink.webp +0 -0
  145. package/assets/styles/clay-stopmotion-3d.webp +0 -0
  146. package/assets/styles/dark-ghibli.webp +0 -0
  147. package/assets/styles/dark-gothic-3d.webp +0 -0
  148. package/assets/styles/film-photo.webp +0 -0
  149. package/assets/styles/guofeng-heavy.webp +0 -0
  150. package/assets/styles/horror-game-3d.webp +0 -0
  151. package/assets/styles/japan-cel-dark.webp +0 -0
  152. package/assets/styles/korean-webtoon.webp +0 -0
  153. package/assets/styles/low-cyberpunk-3d.webp +0 -0
  154. package/assets/styles/nextgen-cg.webp +0 -0
  155. package/assets/styles/noir-film.webp +0 -0
  156. package/assets/styles/openworld-survival.webp +0 -0
  157. package/assets/styles/paper-cut-3d.webp +0 -0
  158. package/assets/styles/pixar-adult-3d.webp +0 -0
  159. package/assets/styles/pop-marvel-2d.webp +0 -0
  160. package/assets/styles/shinkai-anime.webp +0 -0
  161. package/assets/styles/tactical-stealth.webp +0 -0
  162. package/assets/styles/vaporwave-3d.webp +0 -0
  163. package/assets/styles/vhs90.webp +0 -0
  164. package/lib/types/client/panel/BookshelfBar.d.ts +0 -11
  165. package/lib/types/client/panel/FlowGuide.d.ts +0 -21
  166. package/lib/types/client/panel/MangaRoleLibrary.d.ts +0 -18
  167. package/lib/types/client/panel/MangaWorkspace.d.ts +0 -15
  168. package/lib/types/client/panel/ModelPicker.d.ts +0 -11
  169. package/lib/types/client/panel/ProgressHomeView.d.ts +0 -5
  170. package/lib/types/client/panel/SceneLibrary.d.ts +0 -14
  171. package/lib/types/client/panel/StoryboardTab.d.ts +0 -19
  172. package/lib/types/client/panel/StyleCard.d.ts +0 -8
  173. package/lib/types/client/panel/StyleGallery.d.ts +0 -1
  174. package/lib/types/comic-presets.d.ts +0 -12
  175. package/lib/types/shot-language.d.ts +0 -46
  176. package/lib/types/story-beat-language.d.ts +0 -35
  177. package/lib/types/style-library.d.ts +0 -33
  178. package/skills/chapter-batch/SKILL.md +0 -81
  179. package/src/client/mount.tsx._new +0 -97
  180. package/src/client/mount.tsx._nf +0 -97
  181. package/src/client/mount.tsx._tmp +0 -97
  182. package/src/client/panel/BookshelfBar.tsx +0 -170
  183. package/src/client/panel/FlowGuide.tsx +0 -83
  184. package/src/client/panel/MangaRoleLibrary.tsx +0 -810
  185. package/src/client/panel/MangaWorkspace.tsx +0 -359
  186. package/src/client/panel/ProgressHomeView.tsx +0 -87
  187. package/src/client/panel/SceneLibrary.tsx +0 -396
  188. package/src/client/panel/StoryboardTab.tsx +0 -466
  189. package/src/client/panel/StyleCard.tsx +0 -90
  190. package/src/client/panel/StyleGallery.tsx +0 -58
  191. package/src/comic-presets.ts +0 -67
  192. package/src/shot-language.ts +0 -181
  193. package/src/story-beat-language.ts +0 -130
  194. package/src/style-library.ts +0 -78
@@ -1,202 +1,309 @@
1
- /**
2
- * 作者级首页容器:书架页左侧导航 + 内容区。
3
- * 导航复用工作区 .panelNav 玻璃卡片样式(同款),导航项为作者级:书架/改编/资产库/全局资产库/AI 进度/设置。
4
- */
5
- import { useState, useRef, useEffect } from 'react'
6
- import { Library, Wand2, Boxes, Brush, BarChart3, Settings } from 'lucide-react'
7
- import type { NovelApi } from '../api.ts'
8
- import type { BookshelfSnapshot } from '../../protocol.ts'
9
- import { ShelfView } from './ShelfView.tsx'
10
- import { AdaptModeView } from './AdaptModeView.tsx'
11
- import { AuthorAssetsView } from './AuthorAssetsView.tsx'
12
- import { GlobalAssetLibraryView } from './GlobalAssetLibraryView.tsx'
13
- import { ProgressConsole, type ProgressLine } from './ProgressConsole.tsx'
14
- import { SettingsView } from './SettingsView.tsx'
15
- import css from './panel.module.css'
16
-
17
- /** 构建时注入的插件版本(tsdown define 替换为字符串字面量)。 */
18
- declare const __NOVEL_FORGE_VERSION__: string | undefined
19
- const PLUGIN_VERSION: string = typeof __NOVEL_FORGE_VERSION__ !== 'undefined' ? __NOVEL_FORGE_VERSION__ : '0.0.0'
20
- /** GitHub 仓库地址(关于区块点击跳转)。 */
21
- const REPO_URL = 'https://github.com/watersxya/dsh-novel-forge'
22
-
23
- type AuthorNav = 'shelf' | 'adapt' | 'assets' | 'library' | 'progress' | 'settings';
24
-
25
- const NAV_ITEMS: Array<{ id: Exclude<AuthorNav, 'settings'>; label: string; icon: JSX.Element; hint: string }> = [
26
- { id: 'shelf', label: '书架', icon: <Library size={18} />, hint: '我的书' },
27
- { id: 'adapt', label: '改编模式', icon: <Wand2 size={18} />, hint: '上传全文→可改范围' },
28
- { id: 'assets', label: '作者资产库', icon: <Boxes size={18} />, hint: '跨书总数据' },
29
- { id: 'library', label: '全局写作资产库', icon: <Brush size={18} />, hint: '内置题材/规则/模板' },
30
- { id: 'progress', label: 'AI 进度', icon: <BarChart3 size={18} />, hint: '书架级聚合进度' },
31
- ];
32
-
33
-
34
- export function AuthorHome({ api, shelf, onOpenBook, onReadBook, onAddBook, onImportBook, onOpenSettings, onTheme, onBackground, onOpacity, adaptEnabled, progress, busy, busyLabel, liveBar, onClearProgress }: {
35
- api: NovelApi; shelf: BookshelfSnapshot;
36
- onOpenBook: (id: string) => void; onReadBook: (id: string) => void; onAddBook: () => void; onImportBook: () => void;
37
- /** 兼容旧入口:首页设置现为独立设置页,此回调保留但不再使用。 */
38
- onOpenSettings?: () => void;
39
- /** 主题/模式/密度变化回调(供面板根容器实时生效)。 */
40
- onTheme?: (theme: 'liquid' | 'neumorph' | 'macos' | 'clay', mode: 'system' | 'light' | 'dark', density: 'comfort' | 'compact' | 'spacious') => void;
41
- /** 自定义背景变化回调。 */
42
- onBackground?: (bg: string | undefined, blur: number) => void;
43
- /** 玻璃透明度变化回调。 */
44
- onOpacity?: (n: number) => void;
45
- /** 是否启用改编模式(默认 false=隐藏入口)。 */
46
- adaptEnabled?: boolean;
47
- /** AI 进度实时状态(与书内共享同一份)。 */
48
- progress?: ProgressLine[];
49
- busy?: boolean;
50
- busyLabel?: string;
51
- liveBar?: { text: string; ratio?: number } | null;
52
- onClearProgress?: () => void;
53
- }) {
54
- const [nav, setNav] = useState<AuthorNav>('shelf');
55
- const [updating, setUpdating] = useState(false);
56
- const [progressOpen, setProgressOpen] = useState(false);
57
- const [progressPos, setProgressPos] = useState(() => {
58
- try {
59
- const raw = window.localStorage.getItem('dsh-novel-forge.home.progress.float')
60
- if (raw !== null) { const p = JSON.parse(raw) as { x?: unknown; y?: unknown }; return { x: typeof p.x === 'number' ? p.x : 60, y: typeof p.y === 'number' ? p.y : 120 } }
61
- } catch { /* ignore */ }
62
- return { x: 60, y: 120 }
63
- });
64
- const [progressSize, setProgressSize] = useState(() => {
65
- try {
66
- const raw = window.localStorage.getItem('dsh-novel-forge.home.progress.size')
67
- if (raw !== null) { const p = JSON.parse(raw) as { w?: unknown; h?: unknown }; return { w: typeof p.w === 'number' ? p.w : 460, h: typeof p.h === 'number' ? p.h : 420 } }
68
- } catch { /* ignore */ }
69
- return { w: 460, h: 420 }
70
- });
71
- const dragState = useRef<{ type: 'move' | 'resize'; startX: number; startY: number; origX: number; origY: number; origW: number; origH: number } | null>(null);
72
-
73
- useEffect(() => {
74
- try {
75
- window.localStorage.setItem('dsh-novel-forge.home.progress.float', JSON.stringify(progressPos))
76
- window.localStorage.setItem('dsh-novel-forge.home.progress.size', JSON.stringify(progressSize))
77
- } catch { /* ignore */ }
78
- }, [progressPos, progressSize]);
79
-
80
- useEffect(() => {
81
- const onMove = (e: MouseEvent): void => {
82
- const s = dragState.current
83
- if (s === null) return
84
- if (s.type === 'move') {
85
- setProgressPos({ x: Math.max(-340, Math.min(s.origX + e.clientX - s.startX, 3000)), y: Math.max(0, Math.min(s.origY + e.clientY - s.startY, 3000)) })
86
- } else {
87
- setProgressSize({ w: Math.max(320, Math.min(s.origW + e.clientX - s.startX, 1400)), h: Math.max(220, Math.min(s.origH + e.clientY - s.startY, 1200)) })
88
- }
89
- }
90
- const onUp = (): void => { dragState.current = null }
91
- window.addEventListener('mousemove', onMove)
92
- window.addEventListener('mouseup', onUp)
93
- return () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp) }
94
- }, []);
95
-
96
- /** npm 最新版本(更新检测;null = 未检测/检测失败)。 */
97
- const [npmLatest, setNpmLatest] = useState<string | null>(null)
98
- useEffect(() => {
99
- let cancelled = false
100
- void (async () => {
101
- try {
102
- const r = await fetch('https://registry.npmjs.org/@waterwx%2Fdsh-novel-forge/latest')
103
- if (!r.ok) return
104
- const data = (await r.json()) as { version?: unknown }
105
- if (!cancelled && typeof data.version === 'string') setNpmLatest(data.version)
106
- } catch { /* best-effort */ }
107
- })()
108
- return () => { cancelled = true }
109
- }, [])
110
-
111
- const doUpdate = async (): Promise<void> => {
112
- setUpdating(true)
113
- try {
114
- const res = await api.pluginUpdate()
115
- window.alert(res.message)
116
- } catch (err) {
117
- window.alert('更新失败:' + ((err as Error).message ?? String(err)))
118
- } finally {
119
- setUpdating(false)
120
- }
121
- };
122
-
123
- return (
124
- <div className={css.authorHome}>
125
- <nav className={css.panelNav} aria-label="作者级导航">
126
- <div className={css.navTitle}>
127
- <div className={css.navTitleLogo}>书</div>
128
- <div style={{ minWidth: 0 }}>
129
- <div className={css.navTitleName}>小说工坊</div>
130
- <div className={css.navTitleBook}>作者级 · 跨书</div>
131
- </div>
132
- </div>
133
- <div className={css.navGroup}>
134
- <div className={css.navGroupLabel}>作者</div>
135
- {NAV_ITEMS.filter(item => item.id !== 'adapt' || adaptEnabled).map(item => (
136
- <button key={item.id} type="button" role="tab" aria-selected={nav === item.id} data-active={nav === item.id ? '' : undefined} className={css.navTab} title={item.hint} onClick={() => { if (item.id === 'progress') { setProgressOpen(o => !o); return } setNav(item.id) }}>
137
- <span className={css.navTabIcon}>{item.icon}</span>
138
- <span className={css.navTabLabel}>{item.label}</span>
139
- {item.id === 'progress' && busy === true && <span className={css.navTabDot} />}
140
- </button>
141
- ))}
142
- </div>
143
- <div className={css.navSpacer} />
144
- <button type="button" role="tab" aria-selected={nav === 'settings'} data-active={nav === 'settings' ? '' : undefined} className={css.navTab} title="设置" onClick={() => setNav('settings')}>
145
- <span className={css.navTabIcon}><Settings size={18} /></span>
146
- <span className={css.navTabLabel}>设置</span>
147
- </button>
148
- <div className={css.navAbout}>
149
- <button type="button" className={css.navAboutRow} title="打开 GitHub 仓库" onClick={() => { window.open(REPO_URL, '_blank', 'noopener') }}>
150
- <span>ℹ️ v{PLUGIN_VERSION}</span>
151
- <span className={css.meta}>GitHub ↗</span>
152
- </button>
153
- {npmLatest !== null && npmLatest !== PLUGIN_VERSION && (
154
- <button type="button" className={css.navAboutUpdate} title="点击自动更新(下载最新 npm 版,完成后请重启 DSH)" disabled={updating} onClick={() => { void doUpdate() }}>
155
- {updating ? '🔄 更新中…' : `🔄 更新到 v${npmLatest}`}
156
- </button>
157
- )}
158
- </div>
159
- </nav>
160
- <div className={css.authorContent}>
161
- {nav === 'shelf' && <ShelfView api={api} shelf={shelf} onOpenBook={onOpenBook} onReadBook={onReadBook} onAddBook={onAddBook} onImportBook={onImportBook} />}
162
- {nav === 'adapt' && <AdaptModeView api={api} onOpenBook={onOpenBook} />}
163
- {nav === 'assets' && <AuthorAssetsView api={api} />}
164
- {nav === 'library' && <GlobalAssetLibraryView api={api} />}
165
- {nav === 'settings' && <SettingsView api={api} onTheme={onTheme} onBackground={onBackground} onOpacity={onOpacity} />}
166
- </div>
167
- {progressOpen && (
168
- <div className={css.assistantFloat} style={{ left: progressPos.x, top: progressPos.y, width: progressSize.w, height: progressSize.h }}>
169
- <div
170
- className={css.assistantFloatHeader}
171
- onMouseDown={e => {
172
- e.preventDefault()
173
- dragState.current = { type: 'move', startX: e.clientX, startY: e.clientY, origX: progressPos.x, origY: progressPos.y, origW: progressSize.w, origH: progressSize.h }
174
- }}
175
- >
176
- <span>
177
- 📊 AI 进度
178
- {busy && <span style={{ color: 'var(--nf-accent)' }}> 🟢 任务进行中</span>}
179
- <span className={css.meta}>(拖动标题栏移动 · 右下角拉大小)</span>
180
- </span>
181
- <span style={{ display: 'flex', alignItems: 'center', gap: 'var(--nf-space-4)' }}>
182
- {(progress ?? []).length > 0 && <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={onClearProgress} title="清空活动记录">清空</button>}
183
- <button type="button" className={css.iconButton} title="关闭" aria-label="关闭AI进度" onClick={() => setProgressOpen(false)}>×</button>
184
- </span>
185
- </div>
186
- <div className={css.assistantFloatBody} style={{ padding: 'var(--nf-space-10)', display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-8)', overflow: 'hidden', flex: 1, minHeight: 0 }}>
187
- <div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
188
- <ProgressConsole progress={progress ?? []} busy={busy ?? false} busyLabel={busyLabel ?? ''} liveBar={liveBar ?? null} onClear={() => onClearProgress?.()} />
189
- </div>
190
- </div>
191
- <div
192
- style={{ width: 14, height: 14, position: 'absolute', right: 0, bottom: 0, cursor: 'nwse-resize' }}
193
- onMouseDown={e => {
194
- e.preventDefault()
195
- dragState.current = { type: 'resize', startX: e.clientX, startY: e.clientY, origX: progressPos.x, origY: progressPos.y, origW: progressSize.w, origH: progressSize.h }
196
- }}
197
- />
198
- </div>
199
- )}
200
- </div>
201
- );
202
- }
1
+ /**
2
+ * 作者级首页容器:书架页左侧导航 + 内容区。
3
+ * 导航复用工作区 .panelNav 玻璃卡片样式(同款),导航项为作者级:书架/改编/资产库/全局资产库/AI 进度/设置。
4
+ */
5
+ import { useState, useRef, useEffect } from 'react'
6
+ import { Library, Boxes, Brush, Radar, FileSearch, Lightbulb } from 'lucide-react'
7
+ import type { NovelApi } from '../api.ts'
8
+ import type { BookshelfSnapshot } from '../../protocol.ts'
9
+ import { ShelfView } from './ShelfView.tsx'
10
+ import { AdaptModeView } from './AdaptModeView.tsx'
11
+ import { AuthorAssetsView } from './AuthorAssetsView.tsx'
12
+ import { GlobalAssetLibraryView } from './GlobalAssetLibraryView.tsx'
13
+ import { ProgressConsole, type ProgressLine } from './ProgressConsole.tsx'
14
+ import { SettingsView } from './SettingsView.tsx'
15
+ import LiveFeedLog from './LiveFeedLog.tsx'
16
+ import MarketRadarView from './MarketRadarView.tsx'
17
+ import BookAnalysisView from './BookAnalysisView.tsx'
18
+ import IdeaInspirationView from './IdeaInspirationView.tsx'
19
+ import css from './panel.module.css'
20
+
21
+ /** 构建时注入的插件版本(tsdown define 替换为字符串字面量)。 */
22
+ declare const __NOVEL_FORGE_VERSION__: string | undefined
23
+ const PLUGIN_VERSION: string = typeof __NOVEL_FORGE_VERSION__ !== 'undefined' ? __NOVEL_FORGE_VERSION__ : '0.0.0'
24
+ /** GitHub 仓库地址(关于区块点击跳转)。 */
25
+ const REPO_URL = 'https://github.com/watersxya/dsh-novel-forge'
26
+
27
+ type AuthorNav = 'shelf' | 'adapt' | 'assets' | 'library' | 'marketRadar' | 'bookAnalysis' | 'ideaInspiration';
28
+ type AuthorSubNav = AuthorNav;
29
+
30
+ /** 顶层 tab(横向,墨案:创作 / 灵感策划 / 资产 / 设置)+ 组内二级页。 */
31
+ const NAV_TABS: Array<{
32
+ id: 'shelf' | 'marketRadar' | 'assets';
33
+ label: string;
34
+ icon: JSX.Element;
35
+ hint: string;
36
+ /** 组内二级页(首项即该 tab 的默认页)。 */
37
+ children: Array<{ id: AuthorSubNav; label: string; icon: JSX.Element }>;
38
+ }> = [
39
+ {
40
+ id: 'shelf', label: '创作', icon: <Library size={18} />, hint: '我的书 · 改编模式 · 库存',
41
+ /* 改编模式入口移到书架页头(与「导入已有小说」并排),此处只留书架一个子页。 */
42
+ children: [
43
+ { id: 'shelf', label: '我的书', icon: <Library size={14} /> },
44
+ ],
45
+ },
46
+ {
47
+ id: 'marketRadar', label: '灵感策划', icon: <Radar size={18} />, hint: '题材雷达 · 创意灵感 · 书分析',
48
+ children: [
49
+ { id: 'marketRadar', label: '题材雷达', icon: <Radar size={14} /> },
50
+ { id: 'ideaInspiration', label: '创意灵感', icon: <Lightbulb size={14} /> },
51
+ { id: 'bookAnalysis', label: '书分析', icon: <FileSearch size={14} /> },
52
+ ],
53
+ },
54
+ {
55
+ id: 'assets', label: '资产', icon: <Boxes size={18} />, hint: '作者资产库 · 全局资产库',
56
+ children: [
57
+ { id: 'assets', label: '作者资产库', icon: <Brush size={14} /> },
58
+ { id: 'library', label: '全局资产库', icon: <Boxes size={14} /> },
59
+ ],
60
+ },
61
+ ];
62
+
63
+
64
+ export function AuthorHome({ api, shelf, onOpenBook, onReadBook, onAddBook, onImportBook, onBackground, progress, busy, busyLabel, liveBar, onClearProgress, onUseIdea, themeMode, themeDensity, onToggleMode, onChangeThemeMode, onChangeThemeDensity }: {
65
+ /** 当前显示模式(顶栏「墨纸 / 纸白」切换用)。 */
66
+ themeMode?: 'system' | 'light' | 'dark';
67
+ /** 当前界面密度。 */
68
+ themeDensity?: 'comfort' | 'compact' | 'spacious';
69
+ /** 切换墨纸 / 纸白(由 NovelPanel 落 localStorage)。 */
70
+ onToggleMode?: () => void;
71
+ /** 改变全局显示模式(联动整个面板)。 */
72
+ onChangeThemeMode: (m: 'system' | 'light' | 'dark') => void;
73
+ /** 改变全局界面密度(联动整个面板)。 */
74
+ onChangeThemeDensity: (d: 'comfort' | 'compact' | 'spacious') => void;
75
+ api: NovelApi; shelf: BookshelfSnapshot;
76
+ onOpenBook: (id: string) => void; onReadBook: (id: string) => void; onAddBook: () => void; onImportBook: () => void;
77
+ /** 创意灵感 → 采纳某个灵感带入开书向导。 */
78
+ onUseIdea?: (idea: import('../../protocol.ts').IdeaInspirationResult['ideas'][number]) => void;
79
+ /** 自定义背景变化回调。 */
80
+ onBackground?: (bg: string | undefined, blur: number) => void;
81
+ /** AI 进度实时状态(与书内共享同一份)。 */
82
+ progress?: ProgressLine[];
83
+ busy?: boolean;
84
+ busyLabel?: string;
85
+ liveBar?: { text: string; ratio?: number } | null;
86
+ onClearProgress?: () => void;
87
+ }) {
88
+ const [nav, setNav] = useState<AuthorNav>('shelf');
89
+ // 设置改为右滑抽屉(与总编台参数抽屉同交互),不再是全屏导航页。
90
+ const [settingsOpen, setSettingsOpen] = useState(false);
91
+ /** 当前是否深色(墨纸):显式 dark,或跟随系统且系统为深色。 */
92
+ const darkNow = themeMode === 'dark'
93
+ || (themeMode !== 'light' && typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches);
94
+ const [updating, setUpdating] = useState(false);
95
+ const [progressOpen, setProgressOpen] = useState(false);
96
+ const [progressPos, setProgressPos] = useState(() => {
97
+ try {
98
+ const raw = window.localStorage.getItem('dsh-novel-forge.home.progress.float')
99
+ if (raw !== null) { const p = JSON.parse(raw) as { x?: unknown; y?: unknown }; return { x: typeof p.x === 'number' ? p.x : 60, y: typeof p.y === 'number' ? p.y : 120 } }
100
+ } catch { /* ignore */ }
101
+ return { x: 60, y: 120 }
102
+ });
103
+ const [progressSize, setProgressSize] = useState(() => {
104
+ try {
105
+ const raw = window.localStorage.getItem('dsh-novel-forge.home.progress.size')
106
+ if (raw !== null) { const p = JSON.parse(raw) as { w?: unknown; h?: unknown }; return { w: typeof p.w === 'number' ? p.w : 460, h: typeof p.h === 'number' ? p.h : 420 } }
107
+ } catch { /* ignore */ }
108
+ return { w: 460, h: 420 }
109
+ });
110
+ const dragState = useRef<{ type: 'move' | 'resize'; startX: number; startY: number; origX: number; origY: number; origW: number; origH: number } | null>(null);
111
+
112
+ useEffect(() => {
113
+ try {
114
+ window.localStorage.setItem('dsh-novel-forge.home.progress.float', JSON.stringify(progressPos))
115
+ window.localStorage.setItem('dsh-novel-forge.home.progress.size', JSON.stringify(progressSize))
116
+ } catch { /* ignore */ }
117
+ }, [progressPos, progressSize]);
118
+
119
+ useEffect(() => {
120
+ const onMove = (e: MouseEvent): void => {
121
+ const s = dragState.current
122
+ if (s === null) return
123
+ if (s.type === 'move') {
124
+ setProgressPos({ x: Math.max(-340, Math.min(s.origX + e.clientX - s.startX, 3000)), y: Math.max(0, Math.min(s.origY + e.clientY - s.startY, 3000)) })
125
+ } else {
126
+ setProgressSize({ w: Math.max(320, Math.min(s.origW + e.clientX - s.startX, 1400)), h: Math.max(220, Math.min(s.origH + e.clientY - s.startY, 1200)) })
127
+ }
128
+ }
129
+ const onUp = (): void => { dragState.current = null }
130
+ window.addEventListener('mousemove', onMove)
131
+ window.addEventListener('mouseup', onUp)
132
+ return () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp) }
133
+ }, []);
134
+
135
+ /** npm 最新版本(更新检测;null = 未检测/检测失败)。 */
136
+ const [npmLatest, setNpmLatest] = useState<string | null>(null)
137
+ useEffect(() => {
138
+ let cancelled = false
139
+ void (async () => {
140
+ try {
141
+ const r = await fetch('https://registry.npmjs.org/@waterwx%2Fdsh-novel-forge/latest')
142
+ if (!r.ok) return
143
+ const data = (await r.json()) as { version?: unknown }
144
+ if (!cancelled && typeof data.version === 'string') setNpmLatest(data.version)
145
+ } catch { /* best-effort */ }
146
+ })()
147
+ return () => { cancelled = true }
148
+ }, [])
149
+
150
+ const doUpdate = async (): Promise<void> => {
151
+ setUpdating(true)
152
+ try {
153
+ const res = await api.pluginUpdate()
154
+ window.alert(res.message)
155
+ } catch (err) {
156
+ window.alert('更新失败:' + ((err as Error).message ?? String(err)))
157
+ } finally {
158
+ setUpdating(false)
159
+ }
160
+ };
161
+
162
+ /** 下放到书架工具行右端的组入口(创作 / 灵感策划 / 资产)——设置仍留顶栏。 */
163
+ const shelfNavExtra = (
164
+ <>
165
+ {NAV_TABS.map(tab => {
166
+ const inGroup = tab.children.some(c => c.id === nav)
167
+ return (
168
+ <button
169
+ key={tab.id}
170
+ type="button"
171
+ role="tab"
172
+ aria-selected={inGroup}
173
+ data-active={inGroup ? '' : undefined}
174
+ className={css.authorNavBtn}
175
+ title={tab.hint}
176
+ onClick={() => { const first = tab.children[0]; if (first !== undefined) setNav(first.id) }}
177
+ >
178
+ {tab.label}
179
+ </button>
180
+ )
181
+ })}
182
+ </>
183
+ )
184
+
185
+ return (
186
+ <div className={css.authorHome}>
187
+ <nav className={css.authorTopNav} aria-label="作者级导航">
188
+ <div className={css.navTitle}>
189
+ <div className={css.navTitleLogo}>书</div>
190
+ <div style={{ minWidth: 0 }}>
191
+ <div className={css.navTitleName}>小说工坊<span className={css.brandEn}> · AI Novel Workbench</span></div>
192
+ <div className={css.navTitleBook}>一个 AI 智能小说助手 · 作者级 · 跨书</div>
193
+ </div>
194
+ </div>
195
+ {/* 顶行右侧:仅留设置(创作/灵感策划/资产已下放书架工具行) */}
196
+ <div className={css.navSpacer} />
197
+ <div className={css.authorTopTabs}>
198
+ <button
199
+ type="button"
200
+ className={css.authorNavBtn}
201
+ title="设置"
202
+ onClick={() => { setSettingsOpen(true) }}
203
+ >
204
+ 设置
205
+ </button>
206
+ </div>
207
+ {/* 墨纸 / 纸白 切换(对齐 design-sample-workbench.html 的 #modeToggle) */}
208
+ <button
209
+ type="button"
210
+ className={`${css.button} ${css.buttonSmall}`}
211
+ title={darkNow ? '切换到纸白(浅色)' : '切换到墨纸(深色)'}
212
+ onClick={() => { onToggleMode?.() }}
213
+ >
214
+ {darkNow ? '☀ 纸白' : '☾ 墨纸'}
215
+ </button>
216
+ {/* 关于区(版本 / GitHub / 更新)置顶栏最末位 */}
217
+ <div className={css.navAbout}>
218
+ <button type="button" className={css.navAboutRow} title="打开 GitHub 仓库" onClick={() => { window.open(REPO_URL, '_blank', 'noopener') }}>
219
+ <span>ℹ v{PLUGIN_VERSION}</span>
220
+ <span className={css.meta}>GitHub ↗</span>
221
+ </button>
222
+ {npmLatest !== null && npmLatest !== PLUGIN_VERSION && (
223
+ <button type="button" className={css.navAboutUpdate} title="点击自动更新(下载最新 npm 版,完成后请重启 DSH)" disabled={updating} onClick={() => { void doUpdate() }}>
224
+ {updating ? ' 更新中…' : ` 更新到 v${npmLatest}`}
225
+ </button>
226
+ )}
227
+ </div>
228
+ </nav>{/* 第二行:当前组的子页按钮(仅在该组有多个子页时出现,对齐 demo 的 .group-nav) */}
229
+ {(() => {
230
+ const activeTab = NAV_TABS.find(t => t.children.some(c => c.id === nav))
231
+ if (activeTab === undefined || activeTab.children.length <= 1) return null
232
+ return (
233
+ <nav className={css.authorNav} aria-label={`${activeTab.label}下的子页面`}>
234
+ {activeTab.children.map(c => (
235
+ <button
236
+ key={c.id}
237
+ type="button"
238
+ role="tab"
239
+ aria-selected={nav === c.id}
240
+ data-active={nav === c.id ? '' : undefined}
241
+ className={css.authorNavBtn}
242
+ onClick={() => { setNav(c.id) }}
243
+ >
244
+ {c.label}
245
+ </button>
246
+ ))}
247
+ </nav>
248
+ )
249
+ })()}
250
+ <div className={css.authorContent}>
251
+ {nav === 'shelf' && <ShelfView api={api} shelf={shelf} toolbarExtra={shelfNavExtra} onOpenBook={onOpenBook} onReadBook={onReadBook} onAddBook={onAddBook} onImportBook={onImportBook} onOpenAdapt={() => { setNav('adapt') }} />}
252
+ {nav === 'adapt' && <AdaptModeView api={api} onOpenBook={onOpenBook} />}
253
+ {nav === 'assets' && <AuthorAssetsView api={api} />}
254
+ {nav === 'library' && <GlobalAssetLibraryView api={api} />}
255
+ {nav === 'marketRadar' && <MarketRadarView api={api} bookId={shelf?.activeBookId ?? undefined} />}
256
+ {nav === 'ideaInspiration' && <IdeaInspirationView api={api} onUseIdea={onUseIdea} />}
257
+ {nav === 'bookAnalysis' && <BookAnalysisView api={api} />}
258
+ </div>
259
+ {/* 设置右滑抽屉(点遮罩/✕ 回书架) */}
260
+ {settingsOpen && (
261
+ <div className={css.settingsDrawerOverlay} onClick={e => { if (e.target === e.currentTarget) setSettingsOpen(false) }}>
262
+ <aside className={css.settingsDrawer} role="dialog" aria-modal="true" aria-label="设置">
263
+ <div className={css.settingsDrawerHead}>
264
+ <span className={css.cardTitle}>设置 · 全局</span>
265
+ <button type="button" className={`${css.button} ${css.buttonSmall}`} title="关闭" onClick={() => { setSettingsOpen(false) }}>✕</button>
266
+ </div>
267
+ <div className={css.settingsDrawerBody}>
268
+ <SettingsView api={api} variant="drawer" onBackground={onBackground} themeMode={themeMode ?? 'system'} themeDensity={themeDensity ?? 'comfort'} onChangeThemeMode={onChangeThemeMode} onChangeThemeDensity={onChangeThemeDensity} />
269
+ </div>
270
+ </aside>
271
+ </div>
272
+ )}
273
+ {progressOpen && (
274
+ <div className={css.assistantFloat} style={{ left: progressPos.x, top: progressPos.y, width: progressSize.w, height: progressSize.h }}>
275
+ <div
276
+ className={css.assistantFloatHeader}
277
+ onMouseDown={e => {
278
+ e.preventDefault()
279
+ dragState.current = { type: 'move', startX: e.clientX, startY: e.clientY, origX: progressPos.x, origY: progressPos.y, origW: progressSize.w, origH: progressSize.h }
280
+ }}
281
+ >
282
+ <span>
283
+ AI 进度
284
+ {busy && <span style={{ color: 'var(--nf-accent)' }}> 任务进行中</span>}
285
+ <span className={css.meta}>(拖动标题栏移动 · 右下角拉大小)</span>
286
+ </span>
287
+ <span style={{ display: 'flex', alignItems: 'center', gap: 'var(--nf-space-4)' }}>
288
+ {(progress ?? []).length > 0 && <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={onClearProgress} title="清空活动记录">清空</button>}
289
+ <button type="button" className={css.iconButton} title="关闭" aria-label="关闭AI进度" onClick={() => setProgressOpen(false)}>×</button>
290
+ </span>
291
+ </div>
292
+ <div className={css.assistantFloatBody} style={{ padding: 'var(--nf-space-10)', display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-8)', overflow: 'hidden', flex: 1, minHeight: 0 }}>
293
+ <div style={{ flex: '0 2 auto', minHeight: 0, maxHeight: '42%', display: 'flex', flexDirection: 'column' }}>
294
+ <ProgressConsole progress={progress ?? []} busy={busy ?? false} busyLabel={busyLabel ?? ''} liveBar={liveBar ?? null} onClear={() => onClearProgress?.()} />
295
+ </div>
296
+ <LiveFeedLog />
297
+ </div>
298
+ <div
299
+ style={{ width: 14, height: 14, position: 'absolute', right: 0, bottom: 0, cursor: 'nwse-resize' }}
300
+ onMouseDown={e => {
301
+ e.preventDefault()
302
+ dragState.current = { type: 'resize', startX: e.clientX, startY: e.clientY, origX: progressPos.x, origY: progressPos.y, origW: progressSize.w, origH: progressSize.h }
303
+ }}
304
+ />
305
+ </div>
306
+ )}
307
+ </div>
308
+ );
309
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * 书分析/拆书:输入一本书/章节文本 → 卖点/结构/可借鉴点/风险。
3
+ */
4
+ import { useState } from 'react'
5
+ import type { NovelApi } from '../api.ts'
6
+ import type { BookAnalysisResult } from '../../protocol.ts'
7
+ import css from './panel.module.css'
8
+ import { SubPage } from './SubPage.tsx'
9
+
10
+ const SECTIONS: Array<{ key: keyof BookAnalysisResult; label: string; color: string }> = [
11
+ { key: 'sellingPoints', label: '卖点', color: 'var(--nf-success)' },
12
+ { key: 'structure', label: '结构', color: 'var(--nf-info)' },
13
+ { key: 'lessons', label: '可借鉴', color: 'var(--nf-accent)' },
14
+ { key: 'risks', label: '风险', color: 'var(--nf-error)' },
15
+ ]
16
+
17
+ export default function BookAnalysisView({ api }: { api: NovelApi }): JSX.Element {
18
+ const [text, setText] = useState('')
19
+ const [busy, setBusy] = useState(false)
20
+ const [error, setError] = useState('')
21
+ const [result, setResult] = useState<BookAnalysisResult | null>(null)
22
+
23
+ const run = async (): Promise<void> => {
24
+ if (text.trim().length < 50) { setError('文本过短(<50 字符),请粘贴一段书/章节内容'); return }
25
+ setBusy(true); setError('')
26
+ try {
27
+ const r = await api.bookAnalysis(text.trim())
28
+ setResult(r.result)
29
+ } catch (e) { setError((e as Error).message) } finally { setBusy(false) }
30
+ }
31
+
32
+ return (
33
+ <SubPage
34
+ title="书分析 / 拆书"
35
+ meta="粘贴一本书/章节文本 → 提炼卖点、结构、可借鉴点与风险(不照搬具体作品)。"
36
+ actions={(
37
+ <button type="button" className={`${css.button} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void run() }}>
38
+ {busy ? '分析中…' : '开始分析'}
39
+ </button>
40
+ )}
41
+ >
42
+ <div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-10)' }}>
43
+ <span className={css.meta}>粘贴文本后点开始分析,结果按卖点/结构/可借鉴/风险分四区呈现。</span>
44
+ <textarea className={css.input} style={{ minHeight: 180, resize: 'vertical' }} value={text} onChange={e => setText(e.target.value)} placeholder="粘贴要分析的书/章节正文…" />
45
+ </div>
46
+
47
+ {error !== '' && <div style={{ color: 'var(--nf-error)', fontSize: 13 }}> {error}</div>}
48
+
49
+ {result !== null && (
50
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', gap: 'var(--nf-space-10)' }}>
51
+ {SECTIONS.map(s => (
52
+ <div key={s.key} className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-8)' }}>
53
+ <div style={{ fontSize: 13, fontWeight: 600, color: s.color }}>{s.label}({(result[s.key] ?? []).length})</div>
54
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
55
+ {(result[s.key] ?? []).length === 0 ? <span style={{ color: 'var(--nf-text-2)', fontSize: 12 }}>无</span> : (result[s.key] ?? []).map((x, i) => <div key={i} style={{ fontSize: 12, borderLeft: '3px solid ' + s.color, paddingLeft: 6 }}>{x}</div>)}
56
+ </div>
57
+ </div>
58
+ ))}
59
+ </div>
60
+ )}
61
+ </SubPage>
62
+ )
63
+ }