@waterwx/dsh-novel-forge 0.1.0 → 1.0.0-alpha

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 (180) hide show
  1. package/README.md +162 -111
  2. package/cordis.patch.yml +1 -1
  3. package/lib/client.js +54165 -2666
  4. package/lib/index.js +11895 -2211
  5. package/lib/types/ai-scan.d.ts +36 -0
  6. package/lib/types/assets.d.ts +19 -4
  7. package/lib/types/assistant.d.ts +6 -0
  8. package/lib/types/author-assets.d.ts +25 -0
  9. package/lib/types/bookshelf.d.ts +8 -1
  10. package/lib/types/client/api.d.ts +235 -3
  11. package/lib/types/client/index.d.ts +1 -1
  12. package/lib/types/client/llmLive.d.ts +33 -0
  13. package/lib/types/client/locales.d.ts +101 -28
  14. package/lib/types/client/panel/AdaptModeView.d.ts +5 -0
  15. package/lib/types/client/panel/AssetsTab.d.ts +6 -1
  16. package/lib/types/client/panel/AuthorAssetsView.d.ts +4 -0
  17. package/lib/types/client/panel/AuthorHome.d.ts +34 -0
  18. package/lib/types/client/panel/BookAnalysisView.d.ts +4 -0
  19. package/lib/types/client/panel/CreateBookView.d.ts +13 -0
  20. package/lib/types/client/panel/DirectorView.d.ts +7 -0
  21. package/lib/types/client/panel/FlowGuide.d.ts +22 -0
  22. package/lib/types/client/panel/GlobalAssetLibraryView.d.ts +4 -0
  23. package/lib/types/client/panel/IdeaInspirationView.d.ts +6 -0
  24. package/lib/types/client/panel/ImportModal.d.ts +7 -0
  25. package/lib/types/client/panel/KnowledgeBaseView.d.ts +4 -0
  26. package/lib/types/client/panel/LiveFeedLog.d.ts +5 -0
  27. package/lib/types/client/panel/MangaRoleLibrary.d.ts +20 -0
  28. package/lib/types/client/panel/MangaWorkspace.d.ts +12 -0
  29. package/lib/types/client/panel/MarketRadarView.d.ts +5 -0
  30. package/lib/types/client/panel/ModelManager.d.ts +14 -0
  31. package/lib/types/client/panel/ModelPicker.d.ts +11 -0
  32. package/lib/types/client/panel/NovelPanel.d.ts +1 -1
  33. package/lib/types/client/panel/ProgressConsole.d.ts +17 -0
  34. package/lib/types/client/panel/ProgressHomeView.d.ts +5 -0
  35. package/lib/types/client/panel/PropLibrary.d.ts +8 -0
  36. package/lib/types/client/panel/ReaderView.d.ts +8 -0
  37. package/lib/types/client/panel/ReasoningSection.d.ts +10 -0
  38. package/lib/types/client/panel/RunPanel.d.ts +5 -0
  39. package/lib/types/client/panel/SceneLibrary.d.ts +16 -0
  40. package/lib/types/client/panel/SettingsView.d.ts +16 -0
  41. package/lib/types/client/panel/ShelfView.d.ts +15 -0
  42. package/lib/types/client/panel/StoryboardTab.d.ts +19 -0
  43. package/lib/types/client/panel/StyleCard.d.ts +8 -0
  44. package/lib/types/client/panel/StyleGallery.d.ts +1 -0
  45. package/lib/types/client/panel/WorldTab.d.ts +10 -0
  46. package/lib/types/client/panel/helpers.d.ts +10 -0
  47. package/lib/types/client/panel/views.d.ts +77 -0
  48. package/lib/types/client/run-status.d.ts +19 -0
  49. package/lib/types/client/text.d.ts +9 -0
  50. package/lib/types/comic-presets.d.ts +12 -0
  51. package/lib/types/engine.d.ts +250 -8
  52. package/lib/types/global-assets.d.ts +6 -0
  53. package/lib/types/index.d.ts +24 -2
  54. package/lib/types/llm-live.d.ts +44 -0
  55. package/lib/types/manga-genre-rules.d.ts +22 -0
  56. package/lib/types/market-radar-scan.d.ts +19 -0
  57. package/lib/types/market-radar-sources.d.ts +31 -0
  58. package/lib/types/novel-context.d.ts +80 -0
  59. package/lib/types/prompt-utils.d.ts +35 -0
  60. package/lib/types/prompting.d.ts +35 -0
  61. package/lib/types/protocol.d.ts +1453 -5
  62. package/lib/types/review-policy.d.ts +97 -0
  63. package/lib/types/run.d.ts +32 -0
  64. package/lib/types/shot-language.d.ts +46 -0
  65. package/lib/types/src/ai-scan.d.ts +36 -0
  66. package/lib/types/src/assets.d.ts +37 -0
  67. package/lib/types/src/assistant.d.ts +45 -0
  68. package/lib/types/src/author-assets.d.ts +25 -0
  69. package/lib/types/src/bookshelf.d.ts +42 -0
  70. package/lib/types/src/client/api.d.ts +234 -0
  71. package/lib/types/src/client/docx.d.ts +15 -0
  72. package/lib/types/src/client/index.d.ts +14 -0
  73. package/lib/types/src/client/locales.d.ts +212 -0
  74. package/lib/types/src/client/mount.d.ts +9 -0
  75. package/lib/types/src/client/panel/AdaptModeView.d.ts +5 -0
  76. package/lib/types/src/client/panel/AssetsTab.d.ts +12 -0
  77. package/lib/types/src/client/panel/AssistantTab.d.ts +7 -0
  78. package/lib/types/src/client/panel/AuthorAssetsView.d.ts +4 -0
  79. package/lib/types/src/client/panel/AuthorHome.d.ts +32 -0
  80. package/lib/types/src/client/panel/BookshelfBar.d.ts +11 -0
  81. package/lib/types/src/client/panel/CreateBookView.d.ts +9 -0
  82. package/lib/types/src/client/panel/FlowGuide.d.ts +22 -0
  83. package/lib/types/src/client/panel/GlobalAssetLibraryView.d.ts +4 -0
  84. package/lib/types/src/client/panel/ImportModal.d.ts +7 -0
  85. package/lib/types/src/client/panel/MangaRoleLibrary.d.ts +20 -0
  86. package/lib/types/src/client/panel/MangaWorkspace.d.ts +12 -0
  87. package/lib/types/src/client/panel/ModelManager.d.ts +14 -0
  88. package/lib/types/src/client/panel/NovelPanel.d.ts +13 -0
  89. package/lib/types/src/client/panel/ProgressConsole.d.ts +17 -0
  90. package/lib/types/src/client/panel/ProgressHomeView.d.ts +5 -0
  91. package/lib/types/src/client/panel/PropLibrary.d.ts +8 -0
  92. package/lib/types/src/client/panel/ReaderView.d.ts +8 -0
  93. package/lib/types/src/client/panel/ReasoningSection.d.ts +10 -0
  94. package/lib/types/src/client/panel/RunPanel.d.ts +5 -0
  95. package/lib/types/src/client/panel/SceneLibrary.d.ts +16 -0
  96. package/lib/types/src/client/panel/SettingsView.d.ts +16 -0
  97. package/lib/types/src/client/panel/ShelfView.d.ts +15 -0
  98. package/lib/types/src/client/panel/StoryboardTab.d.ts +19 -0
  99. package/lib/types/src/client/panel/StyleCard.d.ts +8 -0
  100. package/lib/types/src/client/panel/StyleGallery.d.ts +1 -0
  101. package/lib/types/src/client/panel/WorldTab.d.ts +10 -0
  102. package/lib/types/src/client/panel/controller.d.ts +19 -0
  103. package/lib/types/src/client/panel/helpers.d.ts +18 -0
  104. package/lib/types/src/client/panel/views.d.ts +77 -0
  105. package/lib/types/src/client/sidebar-entry.d.ts +13 -0
  106. package/lib/types/src/client/text.d.ts +9 -0
  107. package/lib/types/src/comic-presets.d.ts +12 -0
  108. package/lib/types/src/docx.d.ts +19 -0
  109. package/lib/types/src/engine.d.ts +402 -0
  110. package/lib/types/src/index.d.ts +79 -0
  111. package/lib/types/src/manga-genre-rules.d.ts +22 -0
  112. package/lib/types/src/novel-context.d.ts +80 -0
  113. package/lib/types/src/prompt-utils.d.ts +35 -0
  114. package/lib/types/src/protocol.d.ts +2176 -0
  115. package/lib/types/src/review-policy.d.ts +97 -0
  116. package/lib/types/src/routes.d.ts +29 -0
  117. package/lib/types/src/run.d.ts +32 -0
  118. package/lib/types/src/shot-language.d.ts +46 -0
  119. package/lib/types/src/story-beat-language.d.ts +35 -0
  120. package/lib/types/src/style-library.d.ts +33 -0
  121. package/lib/types/story-beat-language.d.ts +35 -0
  122. package/lib/types/style-library.d.ts +33 -0
  123. package/package.json +57 -42
  124. package/scripts/patch-schemastery.mjs +47 -0
  125. package/src/ai-scan.ts +179 -0
  126. package/src/assets.ts +1774 -518
  127. package/src/assistant.ts +915 -547
  128. package/src/author-assets.ts +147 -0
  129. package/src/bookshelf.ts +32 -2
  130. package/src/client/api.ts +666 -254
  131. package/src/client/index.ts +3 -1
  132. package/src/client/llmLive.ts +101 -0
  133. package/src/client/locales.ts +178 -31
  134. package/src/client/mount.tsx +1 -1
  135. package/src/client/panel/AdaptModeView.tsx +535 -0
  136. package/src/client/panel/AssetsTab.tsx +168 -51
  137. package/src/client/panel/AssistantTab.tsx +339 -188
  138. package/src/client/panel/AuthorAssetsView.tsx +208 -0
  139. package/src/client/panel/AuthorHome.tsx +234 -0
  140. package/src/client/panel/BookAnalysisView.tsx +60 -0
  141. package/src/client/panel/BookshelfBar.tsx +63 -9
  142. package/src/client/panel/CreateBookView.tsx +371 -0
  143. package/src/client/panel/DirectorView.tsx +200 -0
  144. package/src/client/panel/GlobalAssetLibraryView.tsx +172 -0
  145. package/src/client/panel/IdeaInspirationView.tsx +71 -0
  146. package/src/client/panel/ImportModal.tsx +303 -0
  147. package/src/client/panel/KnowledgeBaseView.tsx +73 -0
  148. package/src/client/panel/LiveFeedLog.tsx +67 -0
  149. package/src/client/panel/MarketRadarView.tsx +374 -0
  150. package/src/client/panel/ModelManager.tsx +205 -0
  151. package/src/client/panel/NovelPanel.tsx +4713 -990
  152. package/src/client/panel/ProgressConsole.tsx +63 -0
  153. package/src/client/panel/ProgressHomeView.tsx +87 -0
  154. package/src/client/panel/ReaderView.tsx +306 -0
  155. package/src/client/panel/ReasoningSection.tsx +31 -0
  156. package/src/client/panel/RunPanel.tsx +204 -0
  157. package/src/client/panel/SettingsView.tsx +265 -0
  158. package/src/client/panel/ShelfView.tsx +242 -0
  159. package/src/client/panel/WorldTab.tsx +202 -0
  160. package/src/client/panel/helpers.ts +50 -0
  161. package/src/client/panel/panel.module.css +4325 -526
  162. package/src/client/panel/views.tsx +346 -0
  163. package/src/client/run-status.ts +100 -0
  164. package/src/client/sidebar-entry.ts +1 -1
  165. package/src/client/text.ts +28 -0
  166. package/src/engine.ts +4230 -1019
  167. package/src/global-assets.ts +59 -0
  168. package/src/index.ts +123 -9
  169. package/src/llm-live.ts +68 -0
  170. package/src/market-radar-scan.ts +41 -0
  171. package/src/market-radar-sources.ts +173 -0
  172. package/src/novel-context.ts +287 -0
  173. package/src/prompt-utils.ts +73 -0
  174. package/src/prompting.ts +159 -0
  175. package/src/protocol.ts +1998 -539
  176. package/src/review-policy.ts +161 -0
  177. package/src/routes.ts +3277 -955
  178. package/src/run.ts +338 -0
  179. package/lib/client.js.map +0 -1
  180. package/lib/index.js.map +0 -1
@@ -0,0 +1,242 @@
1
+ /**
2
+ * 书架首页视图:书卡网格(封面/书名/简介/进度)+ 搜索筛选 + 开书入口。
3
+ * 进入小说工坊默认展示;点击书卡进入该书工作台,+ 进入开书向导页。
4
+ */
5
+ import { useEffect, useMemo, useState } from 'react'
6
+ import { BookOpen, Download, Library, PenLine, Plus, Search } from 'lucide-react'
7
+ import type { NovelApi } from '../api.ts'
8
+ import type { BookshelfSnapshot } from '../../protocol.ts'
9
+ import css from './panel.module.css'
10
+
11
+ /** 相对时间(人性化:刚刚 / N 分钟前 / N 小时前 / N 天前 / 日期)。 */
12
+ function relativeTime(iso: string): string {
13
+ const t = new Date(iso).getTime()
14
+ if (!Number.isFinite(t)) return '—'
15
+ const diff = Date.now() - t
16
+ if (diff < 60_000) return '刚刚'
17
+ if (diff < 3_600_000) return `${Math.floor(diff / 60_000)} 分钟前`
18
+ if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)} 小时前`
19
+ if (diff < 7 * 86_400_000) return `${Math.floor(diff / 86_400_000)} 天前`
20
+ return new Date(t).toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric' })
21
+ }
22
+
23
+ /** 一本书的状态标签。 */
24
+ function bookStatus(book: BookshelfSnapshot['books'][number]): 'none' | 'done' | 'active' {
25
+ if (!book.hasProject) return 'none'
26
+ return book.total > 0 && book.done >= book.total ? 'done' : 'active'
27
+ }
28
+
29
+ /** 一张书卡(封面懒加载)。 */
30
+ function BookCard({
31
+ api,
32
+ book,
33
+ active,
34
+ onOpen,
35
+ onRead,
36
+ }: {
37
+ api: NovelApi
38
+ book: BookshelfSnapshot['books'][number]
39
+ active: boolean
40
+ onOpen: () => void
41
+ onRead: () => void
42
+ }) {
43
+ const [cover, setCover] = useState<string | null>(null)
44
+
45
+ useEffect(() => {
46
+ if (!book.hasCover) return
47
+ let cancelled = false
48
+ void api.coverGet(book.outputDir).then(result => {
49
+ if (!cancelled) setCover(result.dataUrl)
50
+ }).catch(() => { /* best-effort */ })
51
+ return () => { cancelled = true }
52
+ }, [api, book.hasCover, book.outputDir])
53
+
54
+ const ratio = book.total > 0 ? Math.min(book.done / book.total, 1) : 0
55
+ const status = bookStatus(book)
56
+ const statusLabel = status === 'none' ? '未开书' : status === 'done' ? '已完结' : '进行中'
57
+
58
+ return (
59
+ <div
60
+ className={`${css.bookCard} ${active ? css.bookCardActive : ''}`}
61
+ onClick={onOpen}
62
+ title={`打开《${book.bookName}》`}
63
+ >
64
+ <div className={css.bookCardCover}>
65
+ {cover !== null ? (
66
+ <img src={cover} alt={`《${book.bookName}》封面`} />
67
+ ) : (
68
+ <div className={css.bookCardCoverFallback}>
69
+ <span className={css.bookCardCoverTitle}>{book.bookName.slice(0, 4)}</span>
70
+ <span className={css.meta}>暂无封面</span>
71
+ </div>
72
+ )}
73
+ </div>
74
+ <div className={css.bookCardBody}>
75
+ <div className={css.bookCardTitleRow}>
76
+ <span className={css.bookCardName}>{book.bookName}</span>
77
+ <span className={`${css.badge} ${status === 'none' ? css.badgePending : status === 'done' ? css.badgeDone : css.badgeWritten}`}>
78
+ {statusLabel}
79
+ </span>
80
+ </div>
81
+ <span className={`${css.meta} ${css.bookCardBlurb}`} title={book.blurb ?? ''}>
82
+ {book.blurb !== undefined && book.blurb !== '' ? book.blurb : '暂无简介'}
83
+ </span>
84
+ <div className={css.bookCardProgressBar}>
85
+ <div className={css.bookCardProgressFill} style={{ width: `${Math.round(ratio * 100)}%` }} />
86
+ </div>
87
+ <div className={css.bookCardMetaRow}>
88
+ <span className={css.meta}>{book.total > 0 ? `已完成 ${book.done} / ${book.total} 章` : '尚未规划章节'}</span>
89
+ {book.hasProject && <span className={css.meta} title="最近活动时间">更新于 {relativeTime(book.updatedAt)}</span>}
90
+ </div>
91
+ <div className={css.bookCardActions} onClick={e => { e.stopPropagation() }}>
92
+ <button
93
+ type="button"
94
+ className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`}
95
+ onClick={onOpen}
96
+ title="进入创作工作台(大纲/章节/审稿/设定)"
97
+ >
98
+ <PenLine size={13} style={{ verticalAlign: -2 }} /> 进入工作台
99
+ </button>
100
+ <button
101
+ type="button"
102
+ className={`${css.button} ${css.buttonSmall}`}
103
+ disabled={!book.hasProject || book.done === 0}
104
+ onClick={onRead}
105
+ title={book.hasProject && book.done > 0 ? '沉浸式阅读已写章节' : '尚无已写章节,先去工作台创作'}
106
+ >
107
+ <BookOpen size={13} style={{ verticalAlign: -2 }} /> 阅读
108
+ </button>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ )
113
+ }
114
+
115
+ /** 书架首页。 */
116
+ export function ShelfView({
117
+ api,
118
+ shelf,
119
+ onOpenBook,
120
+ onReadBook,
121
+ onAddBook,
122
+ onImportBook,
123
+ }: {
124
+ api: NovelApi
125
+ shelf: BookshelfSnapshot
126
+ /** 点击书卡:激活该书并进入工作台。 */
127
+ onOpenBook: (id: string) => void
128
+ /** 点击「阅读」:激活该书并进入沉浸式阅读页。 */
129
+ onReadBook: (id: string) => void
130
+ /** 点击+:进入开书向导页。 */
131
+ onAddBook: () => void
132
+ /** 点击「导入」:打开导入弹窗(已有项目目录 / txt/md 全本)。 */
133
+ onImportBook: () => void
134
+ }) {
135
+ const [query, setQuery] = useState('')
136
+ /** 筛选:all=全部 / active=进行中 / done=已完结 / none=未开书。 */
137
+ const [filter, setFilter] = useState<'all' | 'active' | 'done' | 'none'>('all')
138
+
139
+ const stats = useMemo(() => {
140
+ let active = 0, done = 0, none = 0
141
+ for (const b of shelf.books) {
142
+ const s = bookStatus(b)
143
+ if (s === 'none') none++
144
+ else if (s === 'done') done++
145
+ else active++
146
+ }
147
+ return { active, done, none }
148
+ }, [shelf.books])
149
+
150
+ const visible = useMemo(() => {
151
+ const q = query.trim().toLowerCase()
152
+ return shelf.books.filter(b => {
153
+ if (filter !== 'all' && bookStatus(b) !== filter) return false
154
+ if (q === '') return true
155
+ return b.bookName.toLowerCase().includes(q) || (b.blurb ?? '').toLowerCase().includes(q)
156
+ })
157
+ }, [shelf.books, query, filter])
158
+
159
+ return (
160
+ <div className={css.shelfView}>
161
+ <div className={css.shelfHero}>
162
+ <div className={css.shelfTitleRow}>
163
+ <h2 className={css.panelTitle} style={{ margin: 0 }}><Library size={20} style={{ verticalAlign: -3 }} /> AI小说工坊 · 书架</h2>
164
+ <input
165
+ className={`${css.input} ${css.shelfSearch}`}
166
+ type="search"
167
+ placeholder="🔍 搜索书名 / 简介…"
168
+ value={query}
169
+ onChange={e => { setQuery(e.target.value) }}
170
+ />
171
+ </div>
172
+ {shelf.books.length > 0 && (
173
+ <div className={css.shelfStats}>
174
+ {[
175
+ { id: 'all' as const, label: '总书数', value: shelf.books.length },
176
+ { id: 'active' as const, label: '进行中', value: stats.active },
177
+ { id: 'done' as const, label: '已完结', value: stats.done },
178
+ { id: 'none' as const, label: '未开书', value: stats.none },
179
+ ].map(s => (
180
+ <div
181
+ key={s.id}
182
+ className={`${css.shelfStat} ${filter === s.id ? css.shelfStatActive : ''}`}
183
+ title={s.id === 'all' ? '显示全部书籍' : `只看「${s.label}」的书(再点一次取消筛选)`}
184
+ onClick={() => { setFilter(prev => prev === s.id ? 'all' : s.id) }}
185
+ >
186
+ <b>{s.value}</b><span>{s.label}</span>
187
+ </div>
188
+ ))}
189
+ </div>
190
+ )}
191
+ </div>
192
+
193
+ {shelf.books.length === 0 ? (
194
+ /* 空书架引导卡 */
195
+ <div className={css.shelfEmpty}>
196
+ <span className={css.shelfEmptyIcon}><BookOpen size={34} /></span>
197
+ <span className={css.shelfEmptyTitle}>你的创作从这里开始</span>
198
+ <span className={css.meta}>开一本书:粘贴大纲或导入 docx,书名自动识别,开书即建项目</span>
199
+ <button type="button" className={`${css.button} ${css.buttonPrimary}`} onClick={onAddBook}>
200
+ + 开第一本书
201
+ </button>
202
+ <button type="button" className={`${css.button}`} onClick={onImportBook}>
203
+ 📥 导入已有小说
204
+ </button>
205
+ </div>
206
+ ) : visible.length === 0 ? (
207
+ <div className={css.shelfEmpty} style={{ minHeight: 160 }}>
208
+ <span className={css.shelfEmptyIcon}><Search size={34} /></span>
209
+ <span className={css.shelfEmptyTitle}>没有符合条件的书</span>
210
+ <span className={css.meta}>换个关键词或筛选条件试试</span>
211
+ </div>
212
+ ) : (
213
+ <div className={css.shelfGrid}>
214
+ {visible.map(book => (
215
+ <BookCard
216
+ key={book.id}
217
+ api={api}
218
+ book={book}
219
+ active={book.id === shelf.activeBookId}
220
+ onOpen={() => { onOpenBook(book.id) }}
221
+ onRead={() => { onReadBook(book.id) }}
222
+ />
223
+ ))}
224
+
225
+ {/* 开书入口:进入独立向导页 */}
226
+ <div className={`${css.bookCard} ${css.bookAddCard}`} onClick={onAddBook} role="button" tabIndex={0} aria-label="开一本新书" onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onAddBook() } }}>
227
+ <div className={css.bookAddIcon}><Plus size={28} /></div>
228
+ <span>开一本新书</span>
229
+ <span className={css.meta}>书名 + 大纲,开书即建项目</span>
230
+ </div>
231
+
232
+ {/* 导入入口:已有项目目录 / txt/md 全本 */}
233
+ <div className={`${css.bookCard} ${css.bookAddCard}`} onClick={onImportBook} role="button" tabIndex={0} aria-label="导入已有小说" onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onImportBook() } }}>
234
+ <div className={css.bookAddIcon}><Download size={28} /></div>
235
+ <span>导入已有小说</span>
236
+ <span className={css.meta}>项目目录或 txt/md 全本</span>
237
+ </div>
238
+ </div>
239
+ )}
240
+ </div>
241
+ )
242
+ }
@@ -0,0 +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
+ }
@@ -15,3 +15,53 @@ export function tt(key: NovelKey, params?: Record<string, string | number>): str
15
15
  }
16
16
  return text
17
17
  }
18
+
19
+ // ------------------------------------------------------------ shared constants
20
+
21
+ /** 角色定位中文名(角色库/提炼候选共用,收敛自多份复制)。 */
22
+ export const ROLE_LABELS: Record<string, string> = {
23
+ protagonist: '主角',
24
+ female_lead: '女主',
25
+ female_support: '女配',
26
+ support: '配角',
27
+ antagonist: '反派',
28
+ extra: '路人',
29
+ }
30
+
31
+ /** 角色定位徽章颜色。 */
32
+ export function roleColor(label: string): string {
33
+ if (label === 'protagonist') return 'var(--nf-success)'
34
+ if (label === 'female_lead') return 'var(--nf-accent)'
35
+ if (label === 'antagonist') return 'var(--nf-error)'
36
+ return 'var(--nf-text-3)'
37
+ }
38
+
39
+ /** 剧情线类型中文名(与 locale 对齐)。 */
40
+ export function kindLabel(kind: string): string {
41
+ switch (kind) {
42
+ case 'main': return tt('plotlines.kindMain')
43
+ case 'branch': return tt('plotlines.kindBranch')
44
+ case 'character': return tt('plotlines.kindCharacter')
45
+ case 'mystery': return tt('plotlines.kindMystery')
46
+ default: return kind
47
+ }
48
+ }
49
+
50
+ /** 剧情线状态中文名。 */
51
+ export function plotlineStatusLabel(status: string): string {
52
+ switch (status) {
53
+ case 'active': return tt('plotlines.statusActive')
54
+ case 'paused': return tt('plotlines.statusPaused')
55
+ case 'resolved': return tt('plotlines.statusResolved')
56
+ case 'abandoned': return tt('plotlines.statusAbandoned')
57
+ default: return status
58
+ }
59
+ }
60
+
61
+ /** 剧情线状态颜色。 */
62
+ export function plotlineStatusColor(status: string): string {
63
+ if (status === 'resolved') return 'var(--nf-success)'
64
+ if (status === 'abandoned') return 'var(--nf-text-3)'
65
+ if (status === 'paused') return 'var(--nf-warn)'
66
+ return 'var(--nf-accent)'
67
+ }