@waterwx/dsh-novel-forge 2.1.2-alpha → 2.1.3

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 (254) hide show
  1. package/README.md +278 -368
  2. package/lib/client.js +19568 -21381
  3. package/lib/index.js +5158 -3958
  4. package/lib/types/action-guard.d.ts +61 -0
  5. package/lib/types/ai-scan.d.ts +80 -12
  6. package/lib/types/assets.d.ts +7 -9
  7. package/lib/types/assistant.d.ts +25 -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 +49 -29
  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/llmLive.d.ts +12 -14
  14. package/lib/types/client/locales.d.ts +12 -67
  15. package/lib/types/client/mount.d.ts +0 -2
  16. package/lib/types/client/panel/AdaptModeView.d.ts +1 -1
  17. package/lib/types/client/panel/AiTag.d.ts +1 -0
  18. package/lib/types/client/panel/AssetsTab.d.ts +1 -1
  19. package/lib/types/client/panel/AssistantTab.d.ts +4 -1
  20. package/lib/types/client/panel/AuthorAssetsView.d.ts +1 -1
  21. package/lib/types/client/panel/AuthorHome.d.ts +12 -12
  22. package/lib/types/client/panel/CmdPalette.d.ts +12 -0
  23. package/lib/types/client/panel/CreateBookView.d.ts +1 -1
  24. package/lib/types/client/panel/GlobalAssetLibraryView.d.ts +1 -1
  25. package/lib/types/client/panel/ImportModal.d.ts +1 -1
  26. package/lib/types/client/panel/LiveFeedLog.d.ts +8 -0
  27. package/lib/types/client/panel/ModelManager.d.ts +2 -2
  28. package/lib/types/client/panel/NovelPanel.d.ts +7 -2
  29. package/lib/types/client/panel/PageHeader.d.ts +18 -0
  30. package/lib/types/client/panel/ProgressConsole.d.ts +1 -1
  31. package/lib/types/client/panel/PromptSlotsView.d.ts +11 -0
  32. package/lib/types/client/panel/ReaderView.d.ts +1 -1
  33. package/lib/types/client/panel/RunPanel.d.ts +1 -1
  34. package/lib/types/client/panel/SettingsView.d.ts +13 -10
  35. package/lib/types/client/panel/ShelfView.d.ts +11 -2
  36. package/lib/types/client/panel/SubPage.d.ts +27 -0
  37. package/lib/types/client/panel/TensionView.d.ts +18 -0
  38. package/lib/types/client/panel/TimelineView.d.ts +18 -0
  39. package/lib/types/client/panel/WorldTab.d.ts +1 -1
  40. package/lib/types/client/panel/helpers.d.ts +5 -3
  41. package/lib/types/client/panel/market-radar-state.d.ts +56 -0
  42. package/lib/types/client/panel/outline-markdown.d.ts +112 -0
  43. package/lib/types/client/panel/status-badge.d.ts +13 -0
  44. package/lib/types/client/panel/storage-parse.d.ts +46 -0
  45. package/lib/types/client/panel/toast.d.ts +2 -0
  46. package/lib/types/client/panel/usePersistentState.d.ts +17 -0
  47. package/lib/types/client/panel/views.d.ts +21 -35
  48. package/lib/types/client/run-status.d.ts +21 -0
  49. package/lib/types/client/sidebar-entry.d.ts +0 -2
  50. package/lib/types/digest-parse.d.ts +29 -0
  51. package/lib/types/docx-core.d.ts +16 -0
  52. package/lib/types/docx.d.ts +2 -3
  53. package/lib/types/engine.d.ts +122 -103
  54. package/lib/types/home.d.ts +11 -0
  55. package/lib/types/index.d.ts +21 -13
  56. package/lib/types/llm-live.d.ts +62 -36
  57. package/lib/types/llm-retry.d.ts +38 -0
  58. package/lib/types/market-radar-scan.d.ts +0 -1
  59. package/lib/types/market-radar-sources.d.ts +1 -1
  60. package/lib/types/migrate-output.d.ts +25 -0
  61. package/lib/types/model-capability.d.ts +119 -0
  62. package/lib/types/model-request.d.ts +36 -0
  63. package/lib/types/output-budget.d.ts +86 -0
  64. package/lib/types/plotline-align.d.ts +35 -0
  65. package/lib/types/prompt-slots.d.ts +57 -0
  66. package/lib/types/prompting.d.ts +2 -27
  67. package/lib/types/protocol.d.ts +479 -595
  68. package/lib/types/revision.d.ts +76 -0
  69. package/lib/types/routes.d.ts +9 -3
  70. package/lib/types/run-selection.d.ts +51 -0
  71. package/lib/types/run.d.ts +31 -0
  72. package/lib/types/settings-persist.d.ts +59 -0
  73. package/lib/types/snapshots.d.ts +46 -0
  74. package/lib/types/stage-contract.d.ts +58 -0
  75. package/lib/types/stream-fallback.d.ts +60 -0
  76. package/lib/types/strip-headings.d.ts +33 -0
  77. package/lib/types/style-fingerprint.d.ts +78 -0
  78. package/lib/types/tension.d.ts +53 -0
  79. package/lib/types/timeline.d.ts +37 -0
  80. package/package.json +41 -28
  81. package/assets/styles/arcane-thick-3d.webp +0 -0
  82. package/assets/styles/chinese-ink.webp +0 -0
  83. package/assets/styles/clay-stopmotion-3d.webp +0 -0
  84. package/assets/styles/dark-ghibli.webp +0 -0
  85. package/assets/styles/dark-gothic-3d.webp +0 -0
  86. package/assets/styles/film-photo.webp +0 -0
  87. package/assets/styles/guofeng-heavy.webp +0 -0
  88. package/assets/styles/guoman-2d-gufeng.webp +0 -0
  89. package/assets/styles/guoman-3d-tianyuan.webp +0 -0
  90. package/assets/styles/guoman-3d-xuanhuan.webp +0 -0
  91. package/assets/styles/horror-game-3d.webp +0 -0
  92. package/assets/styles/hyperreal-3d.webp +0 -0
  93. package/assets/styles/japan-cel-dark.webp +0 -0
  94. package/assets/styles/korean-webtoon.webp +0 -0
  95. package/assets/styles/low-cyberpunk-3d.webp +0 -0
  96. package/assets/styles/nextgen-cg.webp +0 -0
  97. package/assets/styles/noir-film.webp +0 -0
  98. package/assets/styles/openworld-survival.webp +0 -0
  99. package/assets/styles/paper-cut-3d.webp +0 -0
  100. package/assets/styles/pixar-adult-3d.webp +0 -0
  101. package/assets/styles/pop-marvel-2d.webp +0 -0
  102. package/assets/styles/sandiao-qban.webp +0 -0
  103. package/assets/styles/shinkai-anime.webp +0 -0
  104. package/assets/styles/tactical-stealth.webp +0 -0
  105. package/assets/styles/urban-lightreal-3d.webp +0 -0
  106. package/assets/styles/vaporwave-3d.webp +0 -0
  107. package/assets/styles/vhs90.webp +0 -0
  108. package/lib/types/client/panel/BookshelfBar.d.ts +0 -11
  109. package/lib/types/client/panel/FlowGuide.d.ts +0 -22
  110. package/lib/types/client/panel/MangaRoleLibrary.d.ts +0 -20
  111. package/lib/types/client/panel/MangaWorkspace.d.ts +0 -12
  112. package/lib/types/client/panel/ModelPicker.d.ts +0 -11
  113. package/lib/types/client/panel/ProgressHomeView.d.ts +0 -5
  114. package/lib/types/client/panel/PropLibrary.d.ts +0 -8
  115. package/lib/types/client/panel/SceneLibrary.d.ts +0 -16
  116. package/lib/types/client/panel/StoryboardTab.d.ts +0 -19
  117. package/lib/types/client/panel/StyleCard.d.ts +0 -8
  118. package/lib/types/client/panel/StyleGallery.d.ts +0 -1
  119. package/lib/types/comic-presets.d.ts +0 -12
  120. package/lib/types/manga-genre-rules.d.ts +0 -22
  121. package/lib/types/prompt-utils.d.ts +0 -35
  122. package/lib/types/review-policy.d.ts +0 -97
  123. package/lib/types/shot-language.d.ts +0 -46
  124. package/lib/types/src/ai-scan.d.ts +0 -36
  125. package/lib/types/src/assets.d.ts +0 -37
  126. package/lib/types/src/assistant.d.ts +0 -45
  127. package/lib/types/src/author-assets.d.ts +0 -25
  128. package/lib/types/src/bookshelf.d.ts +0 -42
  129. package/lib/types/src/client/api.d.ts +0 -234
  130. package/lib/types/src/client/docx.d.ts +0 -15
  131. package/lib/types/src/client/index.d.ts +0 -14
  132. package/lib/types/src/client/locales.d.ts +0 -212
  133. package/lib/types/src/client/mount.d.ts +0 -9
  134. package/lib/types/src/client/panel/AdaptModeView.d.ts +0 -5
  135. package/lib/types/src/client/panel/AssetsTab.d.ts +0 -12
  136. package/lib/types/src/client/panel/AssistantTab.d.ts +0 -7
  137. package/lib/types/src/client/panel/AuthorAssetsView.d.ts +0 -4
  138. package/lib/types/src/client/panel/AuthorHome.d.ts +0 -32
  139. package/lib/types/src/client/panel/BookshelfBar.d.ts +0 -11
  140. package/lib/types/src/client/panel/CreateBookView.d.ts +0 -9
  141. package/lib/types/src/client/panel/FlowGuide.d.ts +0 -22
  142. package/lib/types/src/client/panel/GlobalAssetLibraryView.d.ts +0 -4
  143. package/lib/types/src/client/panel/ImportModal.d.ts +0 -7
  144. package/lib/types/src/client/panel/MangaRoleLibrary.d.ts +0 -20
  145. package/lib/types/src/client/panel/MangaWorkspace.d.ts +0 -12
  146. package/lib/types/src/client/panel/ModelManager.d.ts +0 -14
  147. package/lib/types/src/client/panel/NovelPanel.d.ts +0 -13
  148. package/lib/types/src/client/panel/ProgressConsole.d.ts +0 -17
  149. package/lib/types/src/client/panel/ProgressHomeView.d.ts +0 -5
  150. package/lib/types/src/client/panel/PropLibrary.d.ts +0 -8
  151. package/lib/types/src/client/panel/ReaderView.d.ts +0 -8
  152. package/lib/types/src/client/panel/ReasoningSection.d.ts +0 -10
  153. package/lib/types/src/client/panel/RunPanel.d.ts +0 -5
  154. package/lib/types/src/client/panel/SceneLibrary.d.ts +0 -16
  155. package/lib/types/src/client/panel/SettingsView.d.ts +0 -16
  156. package/lib/types/src/client/panel/ShelfView.d.ts +0 -15
  157. package/lib/types/src/client/panel/StoryboardTab.d.ts +0 -19
  158. package/lib/types/src/client/panel/StyleCard.d.ts +0 -8
  159. package/lib/types/src/client/panel/StyleGallery.d.ts +0 -1
  160. package/lib/types/src/client/panel/WorldTab.d.ts +0 -10
  161. package/lib/types/src/client/panel/controller.d.ts +0 -19
  162. package/lib/types/src/client/panel/helpers.d.ts +0 -18
  163. package/lib/types/src/client/panel/views.d.ts +0 -77
  164. package/lib/types/src/client/sidebar-entry.d.ts +0 -13
  165. package/lib/types/src/client/text.d.ts +0 -9
  166. package/lib/types/src/comic-presets.d.ts +0 -12
  167. package/lib/types/src/docx.d.ts +0 -19
  168. package/lib/types/src/engine.d.ts +0 -402
  169. package/lib/types/src/index.d.ts +0 -79
  170. package/lib/types/src/manga-genre-rules.d.ts +0 -22
  171. package/lib/types/src/novel-context.d.ts +0 -80
  172. package/lib/types/src/prompt-utils.d.ts +0 -35
  173. package/lib/types/src/protocol.d.ts +0 -2176
  174. package/lib/types/src/review-policy.d.ts +0 -97
  175. package/lib/types/src/routes.d.ts +0 -29
  176. package/lib/types/src/run.d.ts +0 -32
  177. package/lib/types/src/shot-language.d.ts +0 -46
  178. package/lib/types/src/story-beat-language.d.ts +0 -35
  179. package/lib/types/src/style-library.d.ts +0 -33
  180. package/lib/types/story-beat-language.d.ts +0 -35
  181. package/lib/types/style-library.d.ts +0 -33
  182. package/src/ai-scan.ts +0 -179
  183. package/src/assets.ts +0 -1774
  184. package/src/assistant.ts +0 -915
  185. package/src/author-assets.ts +0 -147
  186. package/src/bookshelf.ts +0 -167
  187. package/src/client/api.ts +0 -716
  188. package/src/client/css-modules.d.ts +0 -8
  189. package/src/client/docx.ts +0 -69
  190. package/src/client/index.ts +0 -34
  191. package/src/client/llmLive.ts +0 -101
  192. package/src/client/locales.ts +0 -418
  193. package/src/client/mount.tsx +0 -97
  194. package/src/client/mount.tsx._new +0 -97
  195. package/src/client/mount.tsx._nf +0 -97
  196. package/src/client/mount.tsx._tmp +0 -97
  197. package/src/client/panel/AdaptModeView.tsx +0 -535
  198. package/src/client/panel/AssetsTab.tsx +0 -458
  199. package/src/client/panel/AssistantTab.tsx +0 -339
  200. package/src/client/panel/AuthorAssetsView.tsx +0 -208
  201. package/src/client/panel/AuthorHome.tsx +0 -234
  202. package/src/client/panel/BookAnalysisView.tsx +0 -60
  203. package/src/client/panel/BookshelfBar.tsx +0 -170
  204. package/src/client/panel/CreateBookView.tsx +0 -371
  205. package/src/client/panel/DirectorView.tsx +0 -200
  206. package/src/client/panel/FlowGuide.tsx +0 -84
  207. package/src/client/panel/GlobalAssetLibraryView.tsx +0 -172
  208. package/src/client/panel/IdeaInspirationView.tsx +0 -71
  209. package/src/client/panel/ImportModal.tsx +0 -303
  210. package/src/client/panel/KnowledgeBaseView.tsx +0 -73
  211. package/src/client/panel/LiveFeedLog.tsx +0 -76
  212. package/src/client/panel/MangaRoleLibrary.tsx +0 -1002
  213. package/src/client/panel/MangaWorkspace.tsx +0 -715
  214. package/src/client/panel/MarketRadarView.tsx +0 -374
  215. package/src/client/panel/ModelManager.tsx +0 -205
  216. package/src/client/panel/NovelPanel.tsx +0 -4721
  217. package/src/client/panel/ProgressConsole.tsx +0 -63
  218. package/src/client/panel/ProgressHomeView.tsx +0 -87
  219. package/src/client/panel/PropLibrary.tsx +0 -175
  220. package/src/client/panel/ReaderView.tsx +0 -306
  221. package/src/client/panel/ReasoningSection.tsx +0 -31
  222. package/src/client/panel/RunPanel.tsx +0 -204
  223. package/src/client/panel/SceneLibrary.tsx +0 -453
  224. package/src/client/panel/SettingsView.tsx +0 -289
  225. package/src/client/panel/ShelfView.tsx +0 -242
  226. package/src/client/panel/StoryboardTab.tsx +0 -472
  227. package/src/client/panel/StyleCard.tsx +0 -90
  228. package/src/client/panel/StyleGallery.tsx +0 -58
  229. package/src/client/panel/WorldTab.tsx +0 -202
  230. package/src/client/panel/controller.ts +0 -45
  231. package/src/client/panel/helpers.ts +0 -67
  232. package/src/client/panel/panel.module.css +0 -4359
  233. package/src/client/panel/views.tsx +0 -314
  234. package/src/client/sidebar-entry.ts +0 -122
  235. package/src/client/text.ts +0 -28
  236. package/src/comic-presets.ts +0 -67
  237. package/src/docx.ts +0 -83
  238. package/src/engine.ts +0 -5636
  239. package/src/global-assets.ts +0 -59
  240. package/src/index.ts +0 -346
  241. package/src/llm-live.ts +0 -68
  242. package/src/manga-genre-rules.ts +0 -87
  243. package/src/market-radar-scan.ts +0 -41
  244. package/src/market-radar-sources.ts +0 -173
  245. package/src/novel-context.ts +0 -287
  246. package/src/prompt-utils.ts +0 -73
  247. package/src/prompting.ts +0 -159
  248. package/src/protocol.ts +0 -2565
  249. package/src/review-policy.ts +0 -161
  250. package/src/routes.ts +0 -3939
  251. package/src/run.ts +0 -338
  252. package/src/shot-language.ts +0 -181
  253. package/src/story-beat-language.ts +0 -130
  254. package/src/style-library.ts +0 -84
@@ -1,339 +0,0 @@
1
- /**
2
- * AI 助手(编辑老师):与 AI 编辑对话讨论剧情,助手通过动作指令直接修改
3
- * 大纲 / 道藏 / 章节。流式渲染回复;工具调用显示为步骤卡片(中文名 +
4
- * 状态 + 耗时),顶部状态条展示当前在做什么;支持清空对话记录。
5
- */
6
- import { useCallback, useEffect, useRef, useState } from 'react'
7
- import type { NovelApi } from '../api.ts'
8
- import { tt } from './helpers.ts'
9
- import type { AssistantFrame, AssistantMessage } from '../../protocol.ts'
10
- import css from './panel.module.css'
11
-
12
- /** 工具中文名 + 图标映射(作者看得懂)。 */
13
- const TOOL_LABELS: Record<string, { icon: string; label: string }> = {
14
- book_overview: { icon: '📖', label: '读取全书上下文' },
15
- impact_analysis: { icon: '🔗', label: '影响分析' },
16
- outline_text: { icon: '📄', label: '读取大纲' },
17
- outline_replace: { icon: '📝', label: '修改大纲' },
18
- bible_set_rule: { icon: '📖', label: '修改设定规则' },
19
- bible_set_redline: { icon: '🚫', label: '修改写作红线' },
20
- chapter_text: { icon: '📄', label: '读取章节' },
21
- chapter_rewrite: { icon: '✏️', label: '修订章节' },
22
- chapter_generate: { icon: '✨', label: '生成章节' },
23
- chapter_review: { icon: '🔍', label: 'AI 审稿' },
24
- foreshadow_add: { icon: '🪤', label: '新增伏笔' },
25
- foreshadow_update: { icon: '🪤', label: '更新伏笔' },
26
- export_txt: { icon: '📦', label: '导出 TXT' },
27
- assets_status: { icon: '🎨', label: '查看写作资产' },
28
- assets_set_genre: { icon: '🏷️', label: '设置题材' },
29
- assets_set_progression: { icon: '📈', label: '设置推进模式' },
30
- assets_add_rule: { icon: '🚫', label: '新增反AI规则' },
31
- book_analysis: { icon: '📊', label: '拆书分析' },
32
- director_advice: { icon: '🎬', label: '自动编辑' },
33
- knowledge_add: { icon: '📚', label: '加入知识库' },
34
- knowledge_search: { icon: '🔍', label: '检索知识库' },
35
- plotline_list: { icon: '🧵', label: '查看剧情线' },
36
- plotline_add: { icon: '🧵', label: '新增剧情线' },
37
- director_todo_add: { icon: '✅', label: '记入编辑待办' },
38
- director_todo_list: { icon: '✅', label: '查看编辑待办' },
39
- knowledge_list: { icon: '📚', label: '列出全部知识库' },
40
- breakdown: { icon: '🔍', label: '拆书分析' },
41
- audit: { icon: '🛡️', label: '全书质检' },
42
- blurb: { icon: '📄', label: '生成简介' },
43
- error: { icon: '⚠️', label: '出错了' },
44
- }
45
-
46
- /** 需要渲染成结构化结果卡片的工具(其余工具结果仍走文字回复,避免大段章节/总纲刷屏)。 */
47
- const CARD_TOOLS = new Set(['director_advice', 'book_analysis', 'breakdown', 'audit', 'knowledge_list', 'plotline_list', 'director_todo_list'])
48
-
49
- /** One chat bubble (either side). */
50
- interface ChatLine {
51
- id: number
52
- role: 'user' | 'assistant'
53
- text: string
54
- /** Tool steps interleaved with the assistant reply. */
55
- tools: Array<{ name: string; status: 'start' | 'done' | 'error'; detail?: string; startedAt?: number; elapsedMs?: number }>
56
- /** Live output while a tool runs (generated text streamed into the bubble). */
57
- live?: string
58
- /** 结构化工具完整结果(用于渲染成卡片,如拆书/导演/质检/知识库列表)。 */
59
- resultCard?: { name: string; text: string }
60
- }
61
-
62
- /** Props. */
63
- export interface AssistantTabProps {
64
- api: NovelApi
65
- }
66
-
67
- /** The assistant conversation tab. */
68
- export function AssistantTab({ api }: AssistantTabProps) {
69
- const [lines, setLines] = useState<ChatLine[]>([])
70
- const [input, setInput] = useState('')
71
- const [busy, setBusy] = useState(false)
72
- const [error, setError] = useState('')
73
- const [notice, setNotice] = useState('')
74
- /** 思考耗时(秒,busy 时每秒刷新)。 */
75
- const [thinkSeconds, setThinkSeconds] = useState(0)
76
- const idRef = useRef(0)
77
- const scrollRef = useRef<HTMLDivElement | null>(null)
78
-
79
- /** busy 计时:显示"已思考 Xs",让作者知道在干活。 */
80
- useEffect(() => {
81
- if (!busy) {
82
- setThinkSeconds(0)
83
- return
84
- }
85
- const started = Date.now()
86
- const timer = window.setInterval(() => {
87
- setThinkSeconds(Math.floor((Date.now() - started) / 1000))
88
- }, 1000)
89
- return () => { window.clearInterval(timer) }
90
- }, [busy])
91
-
92
- /** Append a bubble (or extend the current assistant bubble). */
93
- const pushLine = useCallback((line: Omit<ChatLine, 'id'>) => {
94
- setLines(prev => {
95
- const last = prev[prev.length - 1]
96
- // Extend the live assistant bubble while streaming.
97
- if (line.role === 'assistant' && last !== undefined && last.role === 'assistant' && last.tools.length === 0) {
98
- return [...prev.slice(0, -1), { ...last, text: last.text + line.text }]
99
- }
100
- return [...prev, { ...line, id: idRef.current++ }]
101
- })
102
- }, [])
103
-
104
- /** Push a tool event: start 创建步骤,done/error 更新同名步骤(含耗时)。 */
105
- const pushTool = useCallback((tool: ChatLine['tools'][number]) => {
106
- setLines(prev => {
107
- const last = prev[prev.length - 1]
108
- if (last === undefined || last.role !== 'assistant') {
109
- return [...prev, {
110
- id: idRef.current++,
111
- role: 'assistant',
112
- text: '',
113
- tools: [{ ...tool, startedAt: tool.status === 'start' ? Date.now() : undefined }],
114
- }]
115
- }
116
- const tools = [...last.tools]
117
- if (tool.status === 'start') {
118
- tools.push({ ...tool, startedAt: Date.now() })
119
- } else {
120
- // 找到同名未完成的步骤,更新状态与耗时。
121
- for (let i = tools.length - 1; i >= 0; i--) {
122
- const step = tools[i]!
123
- if (step.name === tool.name && step.status === 'start') {
124
- tools[i] = {
125
- ...step,
126
- status: tool.status,
127
- detail: tool.detail,
128
- elapsedMs: Date.now() - (step.startedAt ?? Date.now()),
129
- }
130
- break
131
- }
132
- }
133
- }
134
- return [...prev.slice(0, -1), { ...last, tools, live: undefined }]
135
- })
136
- }, [])
137
-
138
- /** Append live tool output onto the current assistant bubble. */
139
- const pushToolDelta = useCallback((text: string) => {
140
- setLines(prev => {
141
- const last = prev[prev.length - 1]
142
- if (last === undefined || last.role !== 'assistant') return prev
143
- return [...prev.slice(0, -1), { ...last, live: (last.live ?? '') + text }]
144
- })
145
- }, [])
146
-
147
- /** 把结构化工具的结果挂到当前助手气泡(用于渲染结果卡片)。 */
148
- const pushResultCard = useCallback((card: { name: string; text: string }) => {
149
- setLines(prev => {
150
- const last = prev[prev.length - 1]
151
- if (last === undefined || last.role !== 'assistant') {
152
- return [...prev, { id: idRef.current++, role: 'assistant', text: '', tools: [], resultCard: card }]
153
- }
154
- return [...prev.slice(0, -1), { ...last, resultCard: card }]
155
- })
156
- }, [])
157
-
158
- /** 当前进行中的工具(状态条显示)。 */
159
- const activeTool = (() => {
160
- if (!busy) return null
161
- for (let i = lines.length - 1; i >= 0; i--) {
162
- const line = lines[i]!
163
- if (line.role !== 'assistant') continue
164
- for (let j = line.tools.length - 1; j >= 0; j--) {
165
- if (line.tools[j]!.status === 'start') return line.tools[j]!
166
- }
167
- }
168
- return null
169
- })()
170
-
171
- /** Load persisted history on mount. */
172
- useEffect(() => {
173
- let cancelled = false
174
- void (async () => {
175
- try {
176
- const history = await api.assistantHistory()
177
- if (cancelled) return
178
- const restored: ChatLine[] = []
179
- for (const entry of history) {
180
- if (entry.role === 'user') {
181
- restored.push({ id: idRef.current++, role: 'user', text: entry.content, tools: [] })
182
- } else if (entry.role === 'assistant') {
183
- restored.push({ id: idRef.current++, role: 'assistant', text: entry.content, tools: [] })
184
- }
185
- }
186
- setLines(restored)
187
- } catch (err) {
188
- if (!cancelled) setError((err as Error).message)
189
- }
190
- })()
191
- return () => { cancelled = true }
192
- }, [api])
193
-
194
- /** Auto-scroll to the newest line. */
195
- useEffect(() => {
196
- scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight })
197
- }, [lines])
198
-
199
- /** 清空对话记录(服务端删除历史 + 本地清空)。 */
200
- const handleClear = async (): Promise<void> => {
201
- if (!window.confirm('清空全部聊天记录?此操作不可恢复(不影响大纲/设定/章节)。')) return
202
- setBusy(true)
203
- setError('')
204
- try {
205
- await api.assistantClear()
206
- setLines([])
207
- setNotice('对话已清空,编辑老师会重新了解项目')
208
- } catch (err) {
209
- setError((err as Error).message)
210
- } finally {
211
- setBusy(false)
212
- }
213
- }
214
-
215
- /** Send one message. */
216
- const handleSend = async (): Promise<void> => {
217
- const message = input.trim()
218
- if (message === '' || busy) return
219
- setInput('')
220
- setError('')
221
- setNotice('')
222
- pushLine({ role: 'user', text: message, tools: [] })
223
- // Start an empty assistant bubble.
224
- setLines(prev => [...prev, { id: idRef.current++, role: 'assistant', text: '', tools: [] }])
225
- setBusy(true)
226
- try {
227
- await api.assistant(message, (frame: AssistantFrame) => {
228
- if (frame.type === 'delta') {
229
- pushLine({ role: 'assistant', text: frame.text, tools: [] })
230
- } else if (frame.type === 'tool') {
231
- pushTool({
232
- name: frame.name,
233
- status: frame.status,
234
- detail: frame.detail,
235
- })
236
- } else if (frame.type === 'toolDelta') {
237
- pushToolDelta(frame.text)
238
- } else if (frame.type === 'toolResult') {
239
- pushResultCard({ name: frame.name, text: frame.text })
240
- } else if (frame.type === 'error') {
241
- pushTool({ name: 'error', status: 'error', detail: frame.message })
242
- }
243
- })
244
- } catch (err) {
245
- setError((err as Error).message)
246
- } finally {
247
- setBusy(false)
248
- }
249
- }
250
-
251
- return (
252
- <div className={css.card} style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
253
- <div className={css.row} style={{ justifyContent: 'space-between', alignItems: 'center', gap: 'var(--nf-space-8)' }}>
254
- <span className={css.meta}>{tt('assistant.hint')}</span>
255
- <button type="button" className={css.iconButton} title="清空聊天记录" aria-label="清空聊天记录" onClick={() => { void handleClear() }}>
256
- 🗑️
257
- </button>
258
- </div>
259
- {notice !== '' && <span style={{ color: 'var(--nf-success)', fontSize: 'var(--nf-fs-12)' }}>{notice}</span>}
260
- {error !== '' && <span style={{ color: 'var(--nf-error)', fontSize: 'var(--nf-fs-12)' }}>{tt('common.error')}: {error}</span>}
261
- {/* 状态条:编辑老师正在做什么 */}
262
- <div className={`${css.assistantStatus} ${busy ? css.assistantStatusBusy : ''}`}>
263
- {busy
264
- ? `🤖 编辑老师 · ${activeTool !== null ? `正在「${TOOL_LABELS[activeTool.name]?.label ?? activeTool.name}」` : `正在思考…(已 ${thinkSeconds}s)`}`
265
- : '💬 编辑老师 · 等你开口'}
266
- </div>
267
- <div
268
- ref={scrollRef}
269
- className={css.chatScroll}
270
- >
271
- {lines.length === 0 && <span className={css.meta}>{tt('assistant.empty')}</span>}
272
- {lines.map(line => (
273
- <div key={line.id} className={line.role === 'user' ? css.chatBubbleUser : css.chatBubbleAssistant}>
274
- {line.role === 'user' && <div className={css.chatRole}>你</div>}
275
- {line.text !== '' && <div style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{line.text}</div>}
276
- {line.live !== undefined && line.live !== '' && (
277
- <div className={css.toolLive}>{line.live}</div>
278
- )}
279
- {/* 步骤卡片 */}
280
- {line.tools.length > 0 && (
281
- <div className={css.toolSteps}>
282
- {line.tools.map((tool, i) => {
283
- const meta = TOOL_LABELS[tool.name] ?? { icon: '⚙️', label: tool.name }
284
- return (
285
- <div
286
- key={i}
287
- className={`${css.toolStep} ${tool.status === 'error' ? css.toolStepError : tool.status === 'start' ? css.toolStepActive : ''}`}
288
- >
289
- <span className={css.toolStepIcon}>{meta.icon}</span>
290
- <span className={css.toolStepName}>{meta.label}</span>
291
- <span className={css.toolStepStatus}>
292
- {tool.status === 'start'
293
- ? '⏳ 进行中'
294
- : tool.status === 'done'
295
- ? `✓ ${tool.elapsedMs !== undefined ? `${(tool.elapsedMs / 1000).toFixed(1)}s` : ''}`
296
- : '✗ 失败'}
297
- </span>
298
- {tool.status === 'error' && tool.detail !== undefined && (
299
- <span className={css.toolStepDetail}>{tool.detail}</span>
300
- )}
301
- </div>
302
- )
303
- })}
304
- </div>
305
- )}
306
- {/* 结构化结果卡片(拆书/导演/质检/知识库列表等) */}
307
- {line.resultCard !== undefined && CARD_TOOLS.has(line.resultCard.name) && (
308
- <div style={{ marginTop: 6, border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-10, 10px)', background: 'var(--nf-bg-inset)', padding: 'var(--nf-space-8, 8px)' }}>
309
- <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--nf-accent)', marginBottom: 4 }}>
310
- {TOOL_LABELS[line.resultCard.name]?.icon ?? ''} {TOOL_LABELS[line.resultCard.name]?.label ?? line.resultCard.name}
311
- </div>
312
- <pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', margin: 0, fontSize: 12, lineHeight: 1.5, color: 'var(--nf-text)', maxHeight: 240, overflowY: 'auto' }}>{line.resultCard.text}</pre>
313
- </div>
314
- )}
315
- </div>
316
- ))}
317
- {busy && <span className={css.meta} style={{ color: 'var(--nf-accent)' }}>…</span>}
318
- </div>
319
- <div className={css.row} style={{ marginTop: 'var(--nf-space-8)' }}>
320
- <textarea
321
- className={css.textarea}
322
- style={{ minHeight: 64, flex: 1 }}
323
- placeholder={tt('assistant.placeholder')}
324
- value={input}
325
- onChange={e => { setInput(e.target.value) }}
326
- onKeyDown={e => {
327
- if (e.key === 'Enter' && !e.shiftKey) {
328
- e.preventDefault()
329
- void handleSend()
330
- }
331
- }}
332
- />
333
- <button type="button" className={`${css.button} ${css.buttonPrimary}`} disabled={busy || input.trim() === ''} onClick={() => { void handleSend() }}>
334
- {tt('assistant.send')}
335
- </button>
336
- </div>
337
- </div>
338
- )
339
- }
@@ -1,208 +0,0 @@
1
- /**
2
- * 作者资产库/总数据页:跨书可复用的笔法/红线/套路/角色模板/世界观模板。
3
- * 支持 新增 / 编辑 / 删除;数据持久化到 ~/.dsh/dsh-novel-forge-author-assets.json。
4
- */
5
- import { useEffect, useMemo, useState } from 'react'
6
- import { Plus, Trash2, Pencil } from 'lucide-react'
7
- import type { NovelApi } from '../api.ts'
8
- import type { AuthorStyleAsset } from '../../protocol.ts'
9
- import css from './panel.module.css'
10
-
11
- const KIND_LABELS: Record<AuthorStyleAsset['kind'], string> = {
12
- style: '笔法',
13
- antiAi: '红线/反AI',
14
- progression: '推进模式',
15
- genre: '题材',
16
- roleTemplate: '角色模板',
17
- worldTemplate: '世界观模板',
18
- plotBeat: '剧情桥段',
19
- custom: '自定义',
20
- }
21
-
22
- const KIND_OPTIONS = Object.keys(KIND_LABELS) as AuthorStyleAsset['kind'][]
23
-
24
- const KIND_COLORS: Record<AuthorStyleAsset['kind'], string> = {
25
- style: 'var(--nf-info)',
26
- antiAi: 'var(--nf-warn)',
27
- progression: 'var(--nf-accent)',
28
- genre: 'var(--nf-info)',
29
- roleTemplate: 'var(--nf-accent)',
30
- worldTemplate: 'var(--nf-warn)',
31
- plotBeat: 'var(--nf-accent)',
32
- custom: 'var(--nf-muted)',
33
- }
34
-
35
- /** 计算资产的分组(分类):题材按顶层题材名('/' 之前),其余按类型。 */
36
- function assetCategory(a: AuthorStyleAsset): string {
37
- if (a.kind === 'genre') {
38
- const top = a.name.split('/')[0]?.trim()
39
- return top !== undefined && top !== '' ? top : '题材'
40
- }
41
- return KIND_LABELS[a.kind] ?? '其他'
42
- }
43
-
44
- interface EditorState {
45
- id?: string
46
- name: string; kind: AuthorStyleAsset['kind']; summary: string; content: string; tags: string; sourceBooks: string
47
- }
48
-
49
- function emptyEditor(): EditorState {
50
- return { name: '', kind: 'style', summary: '', content: '', tags: '', sourceBooks: '' }
51
- }
52
-
53
- export function AuthorAssetsView({ api }: { api: NovelApi }) {
54
- const [assets, setAssets] = useState<AuthorStyleAsset[]>([]);
55
- const [editing, setEditing] = useState<EditorState | null>(null);
56
- const [error, setError] = useState('');
57
- const [busy, setBusy] = useState(false);
58
- const [kindFilter, setKindFilter] = useState<AuthorStyleAsset['kind'] | 'all'>('all');
59
-
60
- const load = async (): Promise<void> => {
61
- try { const res = await api.authorAssets(); setAssets(res.assets.items) } catch (err) { setError((err as Error).message) }
62
- };
63
-
64
- /** 导入默认:把书架书的写作资产/角色 + 内置全局库批量沉淀到作者资产库。 */
65
- const importDefaults = async (): Promise<void> => {
66
- setBusy(true); setError('');
67
- try { const res = await api.authorAssetsImportDefault(); setAssets(res.assets.items) } catch (err) { setError((err as Error).message) } finally { setBusy(false) }
68
- };
69
-
70
- // 打开即加载;若资产库为空,自动导入默认(书架资产 + 内置全局库)。
71
- useEffect(() => {
72
- let cancelled = false
73
- void (async () => {
74
- try {
75
- const res = await api.authorAssets()
76
- if (cancelled) return
77
- if (res.assets.items.length === 0) {
78
- const imp = await api.authorAssetsImportDefault()
79
- if (!cancelled) setAssets(imp.assets.items)
80
- } else {
81
- setAssets(res.assets.items)
82
- }
83
- } catch (err) {
84
- if (!cancelled) setError((err as Error).message)
85
- }
86
- })()
87
- return () => { cancelled = true }
88
- }, [api]);
89
-
90
- const startNew = (): void => { setEditing(emptyEditor()); setError('') };
91
- const startEdit = (a: AuthorStyleAsset): void => {
92
- setEditing({ id: a.id, name: a.name, kind: a.kind, summary: a.summary, content: a.content, tags: a.tags.join('、'), sourceBooks: a.sourceBooks.join('、') });
93
- setError('');
94
- };
95
-
96
- const save = async (): Promise<void> => {
97
- if (editing === null) return;
98
- if (editing.name.trim() === '') { setError('请填写资产名'); return }
99
- setBusy(true); setError('');
100
- try {
101
- const asset: AuthorStyleAsset = {
102
- id: editing.id ?? '', name: editing.name.trim(), kind: editing.kind, summary: editing.summary.trim(), content: editing.content.trim(),
103
- sourceBooks: editing.sourceBooks.split(/[、,,\s]+/).map(s => s.trim()).filter(Boolean),
104
- tags: editing.tags.split(/[、,,\s]+/).map(s => s.trim()).filter(Boolean),
105
- createdAt: '', updatedAt: '',
106
- };
107
- await api.authorAssetsUpsert(asset); setEditing(null); await load();
108
- } catch (err) { setError((err as Error).message) } finally { setBusy(false) }
109
- };
110
-
111
- const del = async (id: string): Promise<void> => {
112
- setBusy(true); setError('');
113
- try { await api.authorAssetsRemove(id); await load() } catch (err) { setError((err as Error).message) } finally { setBusy(false) }
114
- };
115
-
116
- const visible = useMemo(() => (kindFilter === 'all' ? assets : assets.filter(a => a.kind === kindFilter)), [assets, kindFilter]);
117
-
118
- /** 按分类分组展示(题材按顶层题材名,其余按类型)。 */
119
- const grouped = useMemo(() => {
120
- const map = new Map<string, AuthorStyleAsset[]>()
121
- for (const a of visible) {
122
- const cat = assetCategory(a)
123
- const arr = map.get(cat) ?? []
124
- arr.push(a)
125
- map.set(cat, arr)
126
- }
127
- return Array.from(map.entries()).sort((x, y) => x[0].localeCompare(y[0], 'zh'))
128
- }, [visible]);
129
-
130
- return (
131
- <div className={css.authorPageBody}>
132
- <div className={css.authorPageHero}>
133
- <div className={css.authorPageHeader}>
134
- <div>
135
- <h2 className={css.panelTitle} style={{ margin: 0 }}>🧬 作者资产库 · 总数据</h2>
136
- <span className={css.meta}>跨书可复用的笔法/红线/套路/角色模板/世界观模板。任意新书与改编都能调用。</span>
137
- </div>
138
- <div className={css.rowEnd}>
139
- <button type="button" className={css.button + ' ' + css.buttonSmall} onClick={() => { void importDefaults() }} disabled={busy} title="把书架里所有书的写作资产/角色批量沉淀到作者资产库(内置库请到全局写作资产库浏览/套用)">导入默认资产</button>
140
- <button type="button" className={css.button + ' ' + css.buttonPrimary} onClick={startNew}><Plus size={14} style={{ verticalAlign: -2 }} /> 新增资产</button>
141
- </div>
142
- </div>
143
- <div className={css.assetFilterBar}>
144
- {KIND_OPTIONS.map(k => (
145
- <button key={k} type="button" className={css.assetFilterChip + (kindFilter === k ? ' ' + css.assetFilterChipActive : '')} onClick={() => setKindFilter(kindFilter === k ? 'all' : k)}>{KIND_LABELS[k]} {assets.filter(a => a.kind === k).length}</button>
146
- ))}
147
- </div>
148
- </div>
149
-
150
- {error !== '' && <div className={css.noticeError}>{error}</div>}
151
-
152
- {editing !== null ? (
153
- <div className={css.assetEditor}>
154
- <h3 style={{ margin: 0 }}>{editing.id !== '' && editing.id !== undefined ? '编辑资产' : '新增资产'}</h3>
155
- <div className={css.assetEditorRow}>
156
- <label className={css.assetLabel}>资产名 <input className={css.input} value={editing.name} onChange={e => setEditing({ ...editing, name: e.target.value })} placeholder="如:短句快节奏爽文风" /></label>
157
- <label className={css.assetLabel}>类型 <select className={css.input} value={editing.kind} onChange={e => setEditing({ ...editing, kind: e.target.value as AuthorStyleAsset['kind'] })}>{KIND_OPTIONS.map(k => <option key={k} value={k}>{KIND_LABELS[k]}</option>)}</select></label>
158
- </div>
159
- <label className={css.assetLabel}>一句话摘要 <input className={css.input} value={editing.summary} onChange={e => setEditing({ ...editing, summary: e.target.value })} placeholder="这个资产解决什么问题 / 有什么特点" /></label>
160
- <label className={css.assetLabel}>内容(按类型约定的描述 / 规则 / 结构化文本)
161
- <textarea className={`${css.input} ${css.assetTextarea}`} value={editing.content} onChange={e => setEditing({ ...editing, content: e.target.value })} placeholder={'角色模板示例:\n姓名:林越\n定位:痞坏型主角\n口头禅:"有点意思"\n性格:表面懒散、内里狠辣\n成长弧线:从底层游街到一方枭雄'} />
162
- </label>
163
- <div className={css.assetEditorRow}>
164
- <label className={css.assetLabel}>标签(顿号分隔) <input className={css.input} value={editing.tags} onChange={e => setEditing({ ...editing, tags: e.target.value })} placeholder="扮猪吃虎、快节奏、冷面" /></label>
165
- <label className={css.assetLabel}>来源书(顿号分隔) <input className={css.input} value={editing.sourceBooks} onChange={e => setEditing({ ...editing, sourceBooks: e.target.value })} placeholder="《某某传》《某某录》" /></label>
166
- </div>
167
- <div className={css.rowEnd}><button type="button" className={`${css.button} ${css.buttonPrimary}`} onClick={save} disabled={busy}>{busy ? '保存中…' : '保存'}</button>{(editing.id !== '' && editing.id !== undefined) && <button type="button" className={css.button + ' ' + css.buttonSmall} onClick={() => setEditing(null)}>取消</button>}</div>
168
- </div>
169
- ) : null}
170
-
171
- {visible.length === 0 ? (
172
- <div className={css.shelfEmpty} style={{ minHeight: 160 }}>
173
- <span className={css.shelfEmptyIcon}><Plus size={30} /></span>
174
- <span className={css.shelfEmptyTitle}>还没有资产</span>
175
- <span className={css.meta}>点「新增资产」收藏你的笔法/角色模板/套路,跨书调用。</span>
176
- </div>
177
- ) : (
178
- <div className={css.assetGroups}>
179
- {grouped.map(([cat, items]) => (
180
- <div key={cat} className={css.assetGroup}>
181
- <div className={css.assetGroupHeader}>{cat} <span className={css.meta}>{items.length}</span></div>
182
- <div className={css.assetGrid}>
183
- {items.map(a => (
184
- <div key={a.id} className={css.assetCard}>
185
- <div className={css.assetCardTop}>
186
- <span className={css.assetKind} style={{ color: KIND_COLORS[a.kind] }}>{KIND_LABELS[a.kind]}</span>
187
- <div className={css.rowEnd}>
188
- <button type="button" className={css.iconBtn} title="编辑" onClick={() => startEdit(a)}><Pencil size={13} /></button>
189
- <button type="button" className={css.iconBtn} title="删除" onClick={() => { void del(a.id) }}><Trash2 size={13} /></button>
190
- </div>
191
- </div>
192
- <div className={css.assetCardName}>{a.name}</div>
193
- {a.summary !== '' && <div className={css.meta}>{a.summary}</div>}
194
- <div className={css.assetCardContent}>{a.content}</div>
195
- <div className={css.assetCardFooter}>
196
- {a.tags.map(t => <span key={t} className={css.badge}>{t}</span>)}
197
- {a.sourceBooks.length > 0 && <span className={css.meta}>来源:{a.sourceBooks.join(' / ')}</span>}
198
- </div>
199
- </div>
200
- ))}
201
- </div>
202
- </div>
203
- ))}
204
- </div>
205
- )}
206
- </div>
207
- );
208
- }