@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,202 @@
1
- /**
2
- * 大世界页签:境界体系 / 地理区域 / 势力分布 的结构化编辑 + AI 提炼。
3
- * 数据注入每章生成与审稿提示词(renderWorld),保证设定不写飞。
4
- */
5
- import { useEffect, useState } from 'react'
6
- import type { NovelApi } from '../api.ts'
7
- import type { WorldFaction, WorldRealm, WorldRegion, WorldState } from '../../protocol.ts'
8
- import css from './panel.module.css'
9
-
10
- const EMPTY: WorldState = { realms: [], regions: [], factions: [] }
11
-
12
- /** 一个可编辑条目行。 */
13
- function EditableRow({
14
- name,
15
- detail,
16
- onName,
17
- onDetail,
18
- onRemove,
19
- namePlaceholder,
20
- detailPlaceholder,
21
- }: {
22
- name: string
23
- detail: string
24
- onName: (v: string) => void
25
- onDetail: (v: string) => void
26
- onRemove: () => void
27
- namePlaceholder: string
28
- detailPlaceholder: string
29
- }) {
30
- return (
31
- <div style={{ display: 'flex', gap: 'var(--nf-space-6)', alignItems: 'flex-start' }}>
32
- <input className={css.input} style={{ flex: 2, minWidth: 0 }} placeholder={namePlaceholder} value={name} onChange={e => { onName(e.target.value) }} />
33
- <input className={css.input} style={{ flex: 3, minWidth: 0 }} placeholder={detailPlaceholder} value={detail} onChange={e => { onDetail(e.target.value) }} />
34
- <button type="button" className={`${css.button} ${css.buttonSmall}`} title="删除" onClick={onRemove}>×</button>
35
- </div>
36
- )
37
- }
38
-
39
- /** 大世界页签。 */
40
- export function WorldTab({
41
- api,
42
- world,
43
- onChanged,
44
- }: {
45
- api: NovelApi
46
- /** 当前项目的大世界数据(可能为空)。 */
47
- world: WorldState | undefined
48
- /** 保存/提炼成功后由父组件刷新项目状态。 */
49
- onChanged: (world: WorldState) => void
50
- }) {
51
- const [draft, setDraft] = useState<WorldState>(() => world ?? EMPTY)
52
- const [busy, setBusy] = useState(false)
53
- const [error, setError] = useState('')
54
-
55
- // 外部数据变化时同步草稿(提炼/保存/切换书后)。
56
- useEffect(() => {
57
- setDraft(world ?? EMPTY)
58
- }, [world])
59
-
60
- const handleGenerate = async (): Promise<void> => {
61
- setBusy(true)
62
- setError('')
63
- try {
64
- const result = await api.world('generate')
65
- setDraft(result.world)
66
- onChanged(result.world)
67
- } catch (err) {
68
- setError((err as Error).message)
69
- } finally {
70
- setBusy(false)
71
- }
72
- }
73
-
74
- const handleSave = async (): Promise<void> => {
75
- setBusy(true)
76
- setError('')
77
- try {
78
- const result = await api.world('save', draft)
79
- onChanged(result.world)
80
- } catch (err) {
81
- setError((err as Error).message)
82
- } finally {
83
- setBusy(false)
84
- }
85
- }
86
-
87
- const setRealms = (realms: WorldRealm[]): void => setDraft(prev => ({ ...prev, realms }))
88
- const setRegions = (regions: WorldRegion[]): void => setDraft(prev => ({ ...prev, regions }))
89
- const setFactions = (factions: WorldFaction[]): void => setDraft(prev => ({ ...prev, factions }))
90
-
91
- return (
92
- <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-12)' }}>
93
- {error !== '' && <div className={css.card} style={{ borderColor: 'var(--nf-error)' }}><span style={{ color: 'var(--nf-error)' }}>{error}</span></div>}
94
-
95
- <div className={css.card}>
96
- <div className={css.row} style={{ justifyContent: 'space-between' }}>
97
- <span className={css.cardTitle}>大世界</span>
98
- <div className={css.row}>
99
- <button type="button" className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void handleGenerate() }}>
100
- ✨ AI 提炼
101
- </button>
102
- <button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={busy} onClick={() => { void handleSave() }}>
103
- 💾 保存
104
- </button>
105
- </div>
106
- </div>
107
- <span className={css.meta}>
108
- 境界体系按由低到高排序注入章节生成提示词,模型不得随意跳级或自创境界;区域与势力约束地理/势力设定。AI 提炼不满意可逐条编辑后保存。
109
- </span>
110
- </div>
111
-
112
- {/* 境界体系 */}
113
- <div className={css.card}>
114
- <div className={css.row} style={{ justifyContent: 'space-between' }}>
115
- <span className={css.cardTitle}>境界体系({draft.realms.length})</span>
116
- <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setRealms([...draft.realms, { name: '', description: '' }]) }}>
117
- + 新增境界
118
- </button>
119
- </div>
120
- {draft.realms.length === 0 ? (
121
- <span className={css.meta}>暂无境界体系 — 点击 ✨AI 提炼 或手动添加(由低到高)。</span>
122
- ) : (
123
- <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)' }}>
124
- {draft.realms.map((realm, i) => (
125
- <EditableRow
126
- key={i}
127
- name={realm.name}
128
- detail={realm.description}
129
- onName={v => { const next = [...draft.realms]; next[i] = { ...next[i]!, name: v }; setRealms(next) }}
130
- onDetail={v => { const next = [...draft.realms]; next[i] = { ...next[i]!, description: v }; setRealms(next) }}
131
- onRemove={() => { setRealms(draft.realms.filter((_, idx) => idx !== i)) }}
132
- namePlaceholder={`第 ${i + 1} 阶境界名`}
133
- detailPlaceholder="突破条件 / 寿命 / 标志…"
134
- />
135
- ))}
136
- </div>
137
- )}
138
- </div>
139
-
140
- {/* 地理区域 */}
141
- <div className={css.card}>
142
- <div className={css.row} style={{ justifyContent: 'space-between' }}>
143
- <span className={css.cardTitle}>地理区域({draft.regions.length})</span>
144
- <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setRegions([...draft.regions, { name: '', description: '' }]) }}>
145
- + 新增区域
146
- </button>
147
- </div>
148
- {draft.regions.length === 0 ? (
149
- <span className={css.meta}>暂无地理区域 — 大陆 / 海域 / 秘境 / 遗迹…</span>
150
- ) : (
151
- <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)' }}>
152
- {draft.regions.map((region, i) => (
153
- <EditableRow
154
- key={i}
155
- name={region.name}
156
- detail={region.description}
157
- onName={v => { const next = [...draft.regions]; next[i] = { ...next[i]!, name: v }; setRegions(next) }}
158
- onDetail={v => { const next = [...draft.regions]; next[i] = { ...next[i]!, description: v }; setRegions(next) }}
159
- onRemove={() => { setRegions(draft.regions.filter((_, idx) => idx !== i)) }}
160
- namePlaceholder="区域名(如大荒 / 青云山脉)"
161
- detailPlaceholder="描述…"
162
- />
163
- ))}
164
- </div>
165
- )}
166
- </div>
167
-
168
- {/* 势力分布 */}
169
- <div className={css.card}>
170
- <div className={css.row} style={{ justifyContent: 'space-between' }}>
171
- <span className={css.cardTitle}>势力分布({draft.factions.length})</span>
172
- <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setFactions([...draft.factions, { name: '', kind: '宗门', description: '' }]) }}>
173
- + 新增势力
174
- </button>
175
- </div>
176
- {draft.factions.length === 0 ? (
177
- <span className={css.meta}>暂无势力 — 宗门 / 家族 / 王朝 / 组织…</span>
178
- ) : (
179
- <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)' }}>
180
- {draft.factions.map((faction, i) => (
181
- <EditableRow
182
- key={i}
183
- name={`${faction.name}(${faction.kind})`}
184
- detail={faction.description}
185
- onName={v => {
186
- const next = [...draft.factions]
187
- const match = /^(.*)((.*))$/.exec(v)
188
- next[i] = { ...next[i]!, name: (match?.[1] ?? v).trim(), kind: (match?.[2] ?? next[i]!.kind).trim() || '宗门' }
189
- setFactions(next)
190
- }}
191
- onDetail={v => { const next = [...draft.factions]; next[i] = { ...next[i]!, description: v }; setFactions(next) }}
192
- onRemove={() => { setFactions(draft.factions.filter((_, idx) => idx !== i)) }}
193
- namePlaceholder="势力名(类型)如:青云宗(宗门)"
194
- detailPlaceholder="描述…"
195
- />
196
- ))}
197
- </div>
198
- )}
199
- </div>
200
- </div>
201
- )
202
- }
1
+ /**
2
+ * 大世界页签:境界体系 / 地理区域 / 势力分布 的结构化编辑 + 提炼。
3
+ * 数据注入每章生成与审稿提示词(renderWorld),保证设定不写飞。
4
+ */
5
+ import { useEffect, useState } from 'react'
6
+ import type { NovelApi } from '../api.ts'
7
+ import type { WorldFaction, WorldRealm, WorldRegion, WorldState } from '../../protocol.ts'
8
+ import css from './panel.module.css'
9
+
10
+ const EMPTY: WorldState = { realms: [], regions: [], factions: [] }
11
+
12
+ /** 一个可编辑条目行。 */
13
+ function EditableRow({
14
+ name,
15
+ detail,
16
+ onName,
17
+ onDetail,
18
+ onRemove,
19
+ namePlaceholder,
20
+ detailPlaceholder,
21
+ }: {
22
+ name: string
23
+ detail: string
24
+ onName: (v: string) => void
25
+ onDetail: (v: string) => void
26
+ onRemove: () => void
27
+ namePlaceholder: string
28
+ detailPlaceholder: string
29
+ }) {
30
+ return (
31
+ <div style={{ display: 'flex', gap: 'var(--nf-space-6)', alignItems: 'flex-start' }}>
32
+ <input className={css.input} style={{ flex: 2, minWidth: 0 }} placeholder={namePlaceholder} value={name} onChange={e => { onName(e.target.value) }} />
33
+ <input className={css.input} style={{ flex: 3, minWidth: 0 }} placeholder={detailPlaceholder} value={detail} onChange={e => { onDetail(e.target.value) }} />
34
+ <button type="button" className={`${css.button} ${css.buttonSmall}`} title="删除" onClick={onRemove}>×</button>
35
+ </div>
36
+ )
37
+ }
38
+
39
+ /** 大世界页签。 */
40
+ export function WorldTab({
41
+ api,
42
+ world,
43
+ onChanged,
44
+ }: {
45
+ api: NovelApi
46
+ /** 当前项目的大世界数据(可能为空)。 */
47
+ world: WorldState | undefined
48
+ /** 保存/提炼成功后由父组件刷新项目状态。 */
49
+ onChanged: (world: WorldState) => void
50
+ }) {
51
+ const [draft, setDraft] = useState<WorldState>(() => world ?? EMPTY)
52
+ const [busy, setBusy] = useState(false)
53
+ const [error, setError] = useState('')
54
+
55
+ // 外部数据变化时同步草稿(提炼/保存/切换书后)。
56
+ useEffect(() => {
57
+ setDraft(world ?? EMPTY)
58
+ }, [world])
59
+
60
+ const handleGenerate = async (): Promise<void> => {
61
+ setBusy(true)
62
+ setError('')
63
+ try {
64
+ const result = await api.world('generate')
65
+ setDraft(result.world)
66
+ onChanged(result.world)
67
+ } catch (err) {
68
+ setError((err as Error).message)
69
+ } finally {
70
+ setBusy(false)
71
+ }
72
+ }
73
+
74
+ const handleSave = async (): Promise<void> => {
75
+ setBusy(true)
76
+ setError('')
77
+ try {
78
+ const result = await api.world('save', draft)
79
+ onChanged(result.world)
80
+ } catch (err) {
81
+ setError((err as Error).message)
82
+ } finally {
83
+ setBusy(false)
84
+ }
85
+ }
86
+
87
+ const setRealms = (realms: WorldRealm[]): void => setDraft(prev => ({ ...prev, realms }))
88
+ const setRegions = (regions: WorldRegion[]): void => setDraft(prev => ({ ...prev, regions }))
89
+ const setFactions = (factions: WorldFaction[]): void => setDraft(prev => ({ ...prev, factions }))
90
+
91
+ return (
92
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-12)' }}>
93
+ {error !== '' && <div className={css.card} style={{ borderColor: 'var(--nf-error)' }}><span style={{ color: 'var(--nf-error)' }}>{error}</span></div>}
94
+
95
+ <div className={css.card}>
96
+ <div className={css.row} style={{ justifyContent: 'space-between' }}>
97
+ <span className={css.cardTitle}>大世界</span>
98
+ <div className={css.row}>
99
+ <button type="button" className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void handleGenerate() }}>
100
+ 提炼
101
+ </button>
102
+ <button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={busy} onClick={() => { void handleSave() }}>
103
+ 保存
104
+ </button>
105
+ </div>
106
+ </div>
107
+ <span className={css.meta}>
108
+ 境界体系按由低到高排序注入章节生成提示词,模型不得随意跳级或自创境界;区域与势力约束地理/势力设定。提炼不满意可逐条编辑后保存。
109
+ </span>
110
+ </div>
111
+
112
+ {/* 境界体系 */}
113
+ <div className={css.card}>
114
+ <div className={css.row} style={{ justifyContent: 'space-between' }}>
115
+ <span className={css.cardTitle}>境界体系({draft.realms.length})</span>
116
+ <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setRealms([...draft.realms, { name: '', description: '' }]) }}>
117
+ + 新增境界
118
+ </button>
119
+ </div>
120
+ {draft.realms.length === 0 ? (
121
+ <span className={css.meta}>暂无境界体系 — 点击 提炼 或手动添加(由低到高)。</span>
122
+ ) : (
123
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)' }}>
124
+ {draft.realms.map((realm, i) => (
125
+ <EditableRow
126
+ key={i}
127
+ name={realm.name}
128
+ detail={realm.description}
129
+ onName={v => { const next = [...draft.realms]; next[i] = { ...next[i]!, name: v }; setRealms(next) }}
130
+ onDetail={v => { const next = [...draft.realms]; next[i] = { ...next[i]!, description: v }; setRealms(next) }}
131
+ onRemove={() => { setRealms(draft.realms.filter((_, idx) => idx !== i)) }}
132
+ namePlaceholder={`第 ${i + 1} 阶境界名`}
133
+ detailPlaceholder="突破条件 / 寿命 / 标志…"
134
+ />
135
+ ))}
136
+ </div>
137
+ )}
138
+ </div>
139
+
140
+ {/* 地理区域 */}
141
+ <div className={css.card}>
142
+ <div className={css.row} style={{ justifyContent: 'space-between' }}>
143
+ <span className={css.cardTitle}>地理区域({draft.regions.length})</span>
144
+ <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setRegions([...draft.regions, { name: '', description: '' }]) }}>
145
+ + 新增区域
146
+ </button>
147
+ </div>
148
+ {draft.regions.length === 0 ? (
149
+ <span className={css.meta}>暂无地理区域 — 大陆 / 海域 / 秘境 / 遗迹…</span>
150
+ ) : (
151
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)' }}>
152
+ {draft.regions.map((region, i) => (
153
+ <EditableRow
154
+ key={i}
155
+ name={region.name}
156
+ detail={region.description}
157
+ onName={v => { const next = [...draft.regions]; next[i] = { ...next[i]!, name: v }; setRegions(next) }}
158
+ onDetail={v => { const next = [...draft.regions]; next[i] = { ...next[i]!, description: v }; setRegions(next) }}
159
+ onRemove={() => { setRegions(draft.regions.filter((_, idx) => idx !== i)) }}
160
+ namePlaceholder="区域名(如大荒 / 青云山脉)"
161
+ detailPlaceholder="描述…"
162
+ />
163
+ ))}
164
+ </div>
165
+ )}
166
+ </div>
167
+
168
+ {/* 势力分布 */}
169
+ <div className={css.card}>
170
+ <div className={css.row} style={{ justifyContent: 'space-between' }}>
171
+ <span className={css.cardTitle}>势力分布({draft.factions.length})</span>
172
+ <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setFactions([...draft.factions, { name: '', kind: '宗门', description: '' }]) }}>
173
+ + 新增势力
174
+ </button>
175
+ </div>
176
+ {draft.factions.length === 0 ? (
177
+ <span className={css.meta}>暂无势力 — 宗门 / 家族 / 王朝 / 组织…</span>
178
+ ) : (
179
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)' }}>
180
+ {draft.factions.map((faction, i) => (
181
+ <EditableRow
182
+ key={i}
183
+ name={`${faction.name}(${faction.kind})`}
184
+ detail={faction.description}
185
+ onName={v => {
186
+ const next = [...draft.factions]
187
+ const match = /^(.*)((.*))$/.exec(v)
188
+ next[i] = { ...next[i]!, name: (match?.[1] ?? v).trim(), kind: (match?.[2] ?? next[i]!.kind).trim() || '宗门' }
189
+ setFactions(next)
190
+ }}
191
+ onDetail={v => { const next = [...draft.factions]; next[i] = { ...next[i]!, description: v }; setFactions(next) }}
192
+ onRemove={() => { setFactions(draft.factions.filter((_, idx) => idx !== i)) }}
193
+ namePlaceholder="势力名(类型)如:青云宗(宗门)"
194
+ detailPlaceholder="描述…"
195
+ />
196
+ ))}
197
+ </div>
198
+ )}
199
+ </div>
200
+ </div>
201
+ )
202
+ }
@@ -1,13 +1,15 @@
1
1
  /**
2
- * Tiny translation helper for the panel: reads the zh dict with the en dict
3
- * as fallback (the family plugins use a full locale registry; the panel keeps
4
- * a dependency-free helper so the client bundle stays self-contained).
2
+ * Tiny translation helper for the panel.
3
+ *
4
+ * 面板是中文单语界面:文案只在 `locales.ts` 定义一次,这里按类型化键位取值
5
+ * (键位不匹配会在编译期报错),保持客户端 bundle 自包含、不引入运行时语言服务。
6
+ * 需要多语言时再接入 DSH 的 locale 服务,届时只需替换本函数实现。
5
7
  */
6
- import { zh, en, type NovelKey } from '../locales.ts'
8
+ import { zh, type NovelKey } from '../locales.ts'
7
9
 
8
10
  /** Translate one key with optional {placeholder} substitution. */
9
11
  export function tt(key: NovelKey, params?: Record<string, string | number>): string {
10
- let text: string = zh[key] ?? en[key] ?? key
12
+ let text: string = zh[key] ?? key
11
13
  if (params !== undefined) {
12
14
  for (const [name, value] of Object.entries(params)) {
13
15
  text = text.replaceAll(`{${name}}`, String(value))
@@ -0,0 +1,79 @@
1
+ /**
2
+ * 热门题材雷达的界面状态规则。
3
+ *
4
+ * 职责边界:本文件只回答「界面上此刻能不能点」,不持有任何数据。抽成纯函数的
5
+ * 理由是它可以离线测——组件渲染不好测,判定逻辑好测。
6
+ *
7
+ * 下面先用**独立表述的验收条件**写清楚要什么,再由两个函数实现;这样即使日后
8
+ * 需求变化,改动方向也是照着条件走,而不是照着某个外部实现的形状走。
9
+ *
10
+ * R1 扫榜或分析进行中,不得改动作品勾选(否则分析输入与界面不一致);
11
+ * R2 未勾选任何作品时不能开始分析;
12
+ * R3 已经有报告之后,仍允许改选作品并**再次**分析 —— 不得退化成"只能查看";
13
+ * R4 信号勾选只在确实换了一份报告时复位;同一份报告的重渲染不得冲掉作者手选。
14
+ */
15
+
16
+ /** 雷达三个入口此刻的可用性,以及分析按钮该显示哪种文案。 */
17
+ export interface RadarControls {
18
+ /** 扫榜按钮是否可点。 */
19
+ canScan: boolean
20
+ /** 作品勾选(含全选 / 清空)是否可改。 */
21
+ canSelectCandidates: boolean
22
+ /** 分析按钮是否可点。 */
23
+ canAnalyze: boolean
24
+ /**
25
+ * 分析按钮的文案标识:
26
+ * - `wait`:还在扫榜,先等榜单;
27
+ * - `pick`:还没勾选作品;
28
+ * - `first`:首次分析;
29
+ * - `again`:已有报告,这次是再出一份。
30
+ */
31
+ analyzeLabel: 'wait' | 'pick' | 'first' | 'again'
32
+ }
33
+
34
+ /**
35
+ * 实现 R1-R3:判定三个入口的可用性。
36
+ * @param state - 当前界面状态。
37
+ * @returns 各入口可用性与分析按钮文案标识。
38
+ */
39
+ export function radarControls(state: {
40
+ /** 正在抓榜单。 */
41
+ scanning: boolean
42
+ /** 正在做 AI 分析。 */
43
+ analyzing: boolean
44
+ /** 已勾选的作品数。 */
45
+ pickedCount: number
46
+ /** 界面上是否已有可查看的报告。 */
47
+ hasReport: boolean
48
+ }): RadarControls {
49
+ if (state.analyzing) {
50
+ // 分析中:三个入口都锁住(R1),按钮文案沿用上一次的状态不必区分。
51
+ return { canScan: false, canSelectCandidates: false, canAnalyze: false, analyzeLabel: 'first' }
52
+ }
53
+ if (state.scanning) {
54
+ return { canScan: false, canSelectCandidates: false, canAnalyze: false, analyzeLabel: 'wait' }
55
+ }
56
+ if (state.pickedCount === 0) {
57
+ return { canScan: true, canSelectCandidates: true, canAnalyze: false, analyzeLabel: 'pick' }
58
+ }
59
+ return {
60
+ canScan: true,
61
+ canSelectCandidates: true,
62
+ canAnalyze: true,
63
+ // R3:有报告也只是换个文案,**不禁用**再分析。
64
+ analyzeLabel: state.hasReport ? 'again' : 'first',
65
+ }
66
+ }
67
+
68
+ /**
69
+ * 实现 R4:本次报告是否**不是**勾选当前所依据的那一份。
70
+ *
71
+ * 传入的是上一次已经据以复位过的报告标识:相同 → 不动作者勾选;不同 → 勾选所依据
72
+ * 的信号集合已经变了,需要按新报告重新给出推荐勾选。
73
+ * @param selectionBasis - 勾选当前依据的报告标识(空串表示尚未复位过)。
74
+ * @param incoming - 本次拿到的报告标识(空串表示旧版产物没有该标识)。
75
+ * @returns 需要按新报告重置勾选时为 true。
76
+ */
77
+ export function isDifferentReport(selectionBasis: string, incoming: string): boolean {
78
+ return selectionBasis !== incoming
79
+ }