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

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 (253) hide show
  1. package/README.md +278 -368
  2. package/lib/client.js +19568 -21381
  3. package/lib/index.js +4850 -4002
  4. package/lib/types/action-guard.d.ts +61 -0
  5. package/lib/types/ai-scan.d.ts +66 -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/tension.d.ts +53 -0
  78. package/lib/types/timeline.d.ts +37 -0
  79. package/package.json +41 -28
  80. package/assets/styles/arcane-thick-3d.webp +0 -0
  81. package/assets/styles/chinese-ink.webp +0 -0
  82. package/assets/styles/clay-stopmotion-3d.webp +0 -0
  83. package/assets/styles/dark-ghibli.webp +0 -0
  84. package/assets/styles/dark-gothic-3d.webp +0 -0
  85. package/assets/styles/film-photo.webp +0 -0
  86. package/assets/styles/guofeng-heavy.webp +0 -0
  87. package/assets/styles/guoman-2d-gufeng.webp +0 -0
  88. package/assets/styles/guoman-3d-tianyuan.webp +0 -0
  89. package/assets/styles/guoman-3d-xuanhuan.webp +0 -0
  90. package/assets/styles/horror-game-3d.webp +0 -0
  91. package/assets/styles/hyperreal-3d.webp +0 -0
  92. package/assets/styles/japan-cel-dark.webp +0 -0
  93. package/assets/styles/korean-webtoon.webp +0 -0
  94. package/assets/styles/low-cyberpunk-3d.webp +0 -0
  95. package/assets/styles/nextgen-cg.webp +0 -0
  96. package/assets/styles/noir-film.webp +0 -0
  97. package/assets/styles/openworld-survival.webp +0 -0
  98. package/assets/styles/paper-cut-3d.webp +0 -0
  99. package/assets/styles/pixar-adult-3d.webp +0 -0
  100. package/assets/styles/pop-marvel-2d.webp +0 -0
  101. package/assets/styles/sandiao-qban.webp +0 -0
  102. package/assets/styles/shinkai-anime.webp +0 -0
  103. package/assets/styles/tactical-stealth.webp +0 -0
  104. package/assets/styles/urban-lightreal-3d.webp +0 -0
  105. package/assets/styles/vaporwave-3d.webp +0 -0
  106. package/assets/styles/vhs90.webp +0 -0
  107. package/lib/types/client/panel/BookshelfBar.d.ts +0 -11
  108. package/lib/types/client/panel/FlowGuide.d.ts +0 -22
  109. package/lib/types/client/panel/MangaRoleLibrary.d.ts +0 -20
  110. package/lib/types/client/panel/MangaWorkspace.d.ts +0 -12
  111. package/lib/types/client/panel/ModelPicker.d.ts +0 -11
  112. package/lib/types/client/panel/ProgressHomeView.d.ts +0 -5
  113. package/lib/types/client/panel/PropLibrary.d.ts +0 -8
  114. package/lib/types/client/panel/SceneLibrary.d.ts +0 -16
  115. package/lib/types/client/panel/StoryboardTab.d.ts +0 -19
  116. package/lib/types/client/panel/StyleCard.d.ts +0 -8
  117. package/lib/types/client/panel/StyleGallery.d.ts +0 -1
  118. package/lib/types/comic-presets.d.ts +0 -12
  119. package/lib/types/manga-genre-rules.d.ts +0 -22
  120. package/lib/types/prompt-utils.d.ts +0 -35
  121. package/lib/types/review-policy.d.ts +0 -97
  122. package/lib/types/shot-language.d.ts +0 -46
  123. package/lib/types/src/ai-scan.d.ts +0 -36
  124. package/lib/types/src/assets.d.ts +0 -37
  125. package/lib/types/src/assistant.d.ts +0 -45
  126. package/lib/types/src/author-assets.d.ts +0 -25
  127. package/lib/types/src/bookshelf.d.ts +0 -42
  128. package/lib/types/src/client/api.d.ts +0 -234
  129. package/lib/types/src/client/docx.d.ts +0 -15
  130. package/lib/types/src/client/index.d.ts +0 -14
  131. package/lib/types/src/client/locales.d.ts +0 -212
  132. package/lib/types/src/client/mount.d.ts +0 -9
  133. package/lib/types/src/client/panel/AdaptModeView.d.ts +0 -5
  134. package/lib/types/src/client/panel/AssetsTab.d.ts +0 -12
  135. package/lib/types/src/client/panel/AssistantTab.d.ts +0 -7
  136. package/lib/types/src/client/panel/AuthorAssetsView.d.ts +0 -4
  137. package/lib/types/src/client/panel/AuthorHome.d.ts +0 -32
  138. package/lib/types/src/client/panel/BookshelfBar.d.ts +0 -11
  139. package/lib/types/src/client/panel/CreateBookView.d.ts +0 -9
  140. package/lib/types/src/client/panel/FlowGuide.d.ts +0 -22
  141. package/lib/types/src/client/panel/GlobalAssetLibraryView.d.ts +0 -4
  142. package/lib/types/src/client/panel/ImportModal.d.ts +0 -7
  143. package/lib/types/src/client/panel/MangaRoleLibrary.d.ts +0 -20
  144. package/lib/types/src/client/panel/MangaWorkspace.d.ts +0 -12
  145. package/lib/types/src/client/panel/ModelManager.d.ts +0 -14
  146. package/lib/types/src/client/panel/NovelPanel.d.ts +0 -13
  147. package/lib/types/src/client/panel/ProgressConsole.d.ts +0 -17
  148. package/lib/types/src/client/panel/ProgressHomeView.d.ts +0 -5
  149. package/lib/types/src/client/panel/PropLibrary.d.ts +0 -8
  150. package/lib/types/src/client/panel/ReaderView.d.ts +0 -8
  151. package/lib/types/src/client/panel/ReasoningSection.d.ts +0 -10
  152. package/lib/types/src/client/panel/RunPanel.d.ts +0 -5
  153. package/lib/types/src/client/panel/SceneLibrary.d.ts +0 -16
  154. package/lib/types/src/client/panel/SettingsView.d.ts +0 -16
  155. package/lib/types/src/client/panel/ShelfView.d.ts +0 -15
  156. package/lib/types/src/client/panel/StoryboardTab.d.ts +0 -19
  157. package/lib/types/src/client/panel/StyleCard.d.ts +0 -8
  158. package/lib/types/src/client/panel/StyleGallery.d.ts +0 -1
  159. package/lib/types/src/client/panel/WorldTab.d.ts +0 -10
  160. package/lib/types/src/client/panel/controller.d.ts +0 -19
  161. package/lib/types/src/client/panel/helpers.d.ts +0 -18
  162. package/lib/types/src/client/panel/views.d.ts +0 -77
  163. package/lib/types/src/client/sidebar-entry.d.ts +0 -13
  164. package/lib/types/src/client/text.d.ts +0 -9
  165. package/lib/types/src/comic-presets.d.ts +0 -12
  166. package/lib/types/src/docx.d.ts +0 -19
  167. package/lib/types/src/engine.d.ts +0 -402
  168. package/lib/types/src/index.d.ts +0 -79
  169. package/lib/types/src/manga-genre-rules.d.ts +0 -22
  170. package/lib/types/src/novel-context.d.ts +0 -80
  171. package/lib/types/src/prompt-utils.d.ts +0 -35
  172. package/lib/types/src/protocol.d.ts +0 -2176
  173. package/lib/types/src/review-policy.d.ts +0 -97
  174. package/lib/types/src/routes.d.ts +0 -29
  175. package/lib/types/src/run.d.ts +0 -32
  176. package/lib/types/src/shot-language.d.ts +0 -46
  177. package/lib/types/src/story-beat-language.d.ts +0 -35
  178. package/lib/types/src/style-library.d.ts +0 -33
  179. package/lib/types/story-beat-language.d.ts +0 -35
  180. package/lib/types/style-library.d.ts +0 -33
  181. package/src/ai-scan.ts +0 -179
  182. package/src/assets.ts +0 -1774
  183. package/src/assistant.ts +0 -915
  184. package/src/author-assets.ts +0 -147
  185. package/src/bookshelf.ts +0 -167
  186. package/src/client/api.ts +0 -716
  187. package/src/client/css-modules.d.ts +0 -8
  188. package/src/client/docx.ts +0 -69
  189. package/src/client/index.ts +0 -34
  190. package/src/client/llmLive.ts +0 -101
  191. package/src/client/locales.ts +0 -418
  192. package/src/client/mount.tsx +0 -97
  193. package/src/client/mount.tsx._new +0 -97
  194. package/src/client/mount.tsx._nf +0 -97
  195. package/src/client/mount.tsx._tmp +0 -97
  196. package/src/client/panel/AdaptModeView.tsx +0 -535
  197. package/src/client/panel/AssetsTab.tsx +0 -458
  198. package/src/client/panel/AssistantTab.tsx +0 -339
  199. package/src/client/panel/AuthorAssetsView.tsx +0 -208
  200. package/src/client/panel/AuthorHome.tsx +0 -234
  201. package/src/client/panel/BookAnalysisView.tsx +0 -60
  202. package/src/client/panel/BookshelfBar.tsx +0 -170
  203. package/src/client/panel/CreateBookView.tsx +0 -371
  204. package/src/client/panel/DirectorView.tsx +0 -200
  205. package/src/client/panel/FlowGuide.tsx +0 -84
  206. package/src/client/panel/GlobalAssetLibraryView.tsx +0 -172
  207. package/src/client/panel/IdeaInspirationView.tsx +0 -71
  208. package/src/client/panel/ImportModal.tsx +0 -303
  209. package/src/client/panel/KnowledgeBaseView.tsx +0 -73
  210. package/src/client/panel/LiveFeedLog.tsx +0 -76
  211. package/src/client/panel/MangaRoleLibrary.tsx +0 -1002
  212. package/src/client/panel/MangaWorkspace.tsx +0 -715
  213. package/src/client/panel/MarketRadarView.tsx +0 -374
  214. package/src/client/panel/ModelManager.tsx +0 -205
  215. package/src/client/panel/NovelPanel.tsx +0 -4721
  216. package/src/client/panel/ProgressConsole.tsx +0 -63
  217. package/src/client/panel/ProgressHomeView.tsx +0 -87
  218. package/src/client/panel/PropLibrary.tsx +0 -175
  219. package/src/client/panel/ReaderView.tsx +0 -306
  220. package/src/client/panel/ReasoningSection.tsx +0 -31
  221. package/src/client/panel/RunPanel.tsx +0 -204
  222. package/src/client/panel/SceneLibrary.tsx +0 -453
  223. package/src/client/panel/SettingsView.tsx +0 -289
  224. package/src/client/panel/ShelfView.tsx +0 -242
  225. package/src/client/panel/StoryboardTab.tsx +0 -472
  226. package/src/client/panel/StyleCard.tsx +0 -90
  227. package/src/client/panel/StyleGallery.tsx +0 -58
  228. package/src/client/panel/WorldTab.tsx +0 -202
  229. package/src/client/panel/controller.ts +0 -45
  230. package/src/client/panel/helpers.ts +0 -67
  231. package/src/client/panel/panel.module.css +0 -4359
  232. package/src/client/panel/views.tsx +0 -314
  233. package/src/client/sidebar-entry.ts +0 -122
  234. package/src/client/text.ts +0 -28
  235. package/src/comic-presets.ts +0 -67
  236. package/src/docx.ts +0 -83
  237. package/src/engine.ts +0 -5636
  238. package/src/global-assets.ts +0 -59
  239. package/src/index.ts +0 -346
  240. package/src/llm-live.ts +0 -68
  241. package/src/manga-genre-rules.ts +0 -87
  242. package/src/market-radar-scan.ts +0 -41
  243. package/src/market-radar-sources.ts +0 -173
  244. package/src/novel-context.ts +0 -287
  245. package/src/prompt-utils.ts +0 -73
  246. package/src/prompting.ts +0 -159
  247. package/src/protocol.ts +0 -2565
  248. package/src/review-policy.ts +0 -161
  249. package/src/routes.ts +0 -3939
  250. package/src/run.ts +0 -338
  251. package/src/shot-language.ts +0 -181
  252. package/src/story-beat-language.ts +0 -130
  253. package/src/style-library.ts +0 -84
@@ -1,453 +0,0 @@
1
- /**
2
- * 场景库(完整版):提炼 / 采纳 / 编辑 / 图集上传 / 详情浮窗。
3
- * 从 NovelPanel 左导航「场景库」搬入漫剧工作台场景区共用;
4
- * 浮窗用 createPortal 挂 body(避开 .view backdrop-filter 的 fixed 包含块问题),
5
- * 提示词全局中英切换 + 复制全部,视觉规则只在主页面显示一次。
6
- */
7
- import { useEffect, useRef, useState } from 'react'
8
- import { createPortal } from 'react-dom'
9
- import type { NovelApi } from '../api.ts'
10
- import type { ProjectState, SceneCard } from '../../protocol.ts'
11
- import css from './panel.module.css'
12
-
13
- export function SceneLibrary({
14
- api,
15
- project,
16
- refresh,
17
- styleId,
18
- filterId,
19
- chapterNo: externalChapter,
20
- onProgress,
21
- }: {
22
- api: NovelApi
23
- project: ProjectState | null
24
- /** 场景库变更已持久化后触发(刷新项目)。 */
25
- refresh: () => void | Promise<void>
26
- /** 提炼场景时的漫剧基底风格 id(提示词按方案风格措辞)。 */
27
- styleId?: string
28
- /** 可选滤镜风格 id。 */
29
- filterId?: string
30
- /** 全局当前章节(从工作台顶部导航条传入)。 */
31
- chapterNo?: number | null
32
- /** 上报到「AI进度」控制台。 */
33
- onProgress?: (text: string, kind?: 'info' | 'done' | 'error') => void
34
- }) {
35
- const [busy, setBusy] = useState(false)
36
- const [error, setError] = useState('')
37
- const [notice, setNotice] = useState('')
38
- const [candidates, setCandidates] = useState<SceneCard[] | null>(null)
39
- const [detailName, setDetailName] = useState<string | null>(null)
40
- const [uploadLabel, setUploadLabel] = useState('全景')
41
- const [uploadTarget, setUploadTarget] = useState<string | null>(null)
42
- const [draft, setDraft] = useState<SceneCard | null>(null)
43
- /** 场景分级筛选。 */
44
- const [tierFilter, setTierFilter] = useState<'all' | 'core' | 'secondary' | 'passing'>('all')
45
- /** 按章提取场景的章节号;undefined=全书提取(前4章)。 */
46
- const [chapterNo, setChapterNo] = useState<number | undefined>(externalChapter ?? undefined)
47
-
48
- // 外部章节变化时同步内部状态
49
- useEffect(() => {
50
- if (externalChapter !== undefined && externalChapter !== null) {
51
- setChapterNo(externalChapter)
52
- }
53
- }, [externalChapter])
54
- const inputRef = useRef<HTMLInputElement | null>(null)
55
- const rootRef = useRef<HTMLDivElement | null>(null)
56
- /** 浮窗挂 body 时复制的面板主题变量(--nf-*)。 */
57
- const [portalVars, setPortalVars] = useState<Record<string, string>>({})
58
-
59
- const notify = (msg: string): void => { setNotice(msg); setTimeout(() => { setNotice('') }, 3000) }
60
- const report = (text: string, kind: 'info' | 'done' | 'error' = 'info'): void => { onProgress?.(text, kind) }
61
-
62
- // Esc 关闭详情浮窗。
63
- useEffect(() => {
64
- if (detailName === null) return
65
- const onKey = (e: KeyboardEvent): void => {
66
- if (e.key === 'Escape') setDetailName(null)
67
- }
68
- window.addEventListener('keydown', onKey)
69
- return () => { window.removeEventListener('keydown', onKey) }
70
- }, [detailName])
71
-
72
- /** 打开场景详情:记录目标并复制面板主题变量(浮窗在 body 下渲染需要)。 */
73
- const openDetail = (name: string): void => {
74
- setDetailName(name)
75
- setUploadLabel('全景')
76
- if (rootRef.current !== null) {
77
- const cs = getComputedStyle(rootRef.current)
78
- const vars: Record<string, string> = {}
79
- for (let i = 0; i < cs.length; i++) {
80
- const key = cs[i]
81
- if (key.startsWith('--nf-')) {
82
- const v = cs.getPropertyValue(key).trim()
83
- if (v !== '') vars[key] = v
84
- }
85
- }
86
- setPortalVars(vars)
87
- }
88
- }
89
-
90
- const extract = async (): Promise<void> => {
91
- setBusy(true); setError('')
92
- report(chapterNo !== undefined ? '按第' + chapterNo + '章提炼场景…' : '按当前方案提炼场景…')
93
- try {
94
- const result = await api.scenes({ op: 'extract', chapterNo, styleId, filterId })
95
- const list = result.candidates ?? []
96
- // 自动采纳全部场景(傻瓜式操作,无需手动一个个点采纳)
97
- for (const s of list) {
98
- try { await api.scenes({ op: 'adopt', scene: s }) } catch { /* 单个采纳失败不阻塞 */ }
99
- }
100
- setCandidates(null)
101
- await refresh()
102
- report('场景提炼完成:已自动采纳 ' + list.length + ' 个场景', 'done')
103
- } catch (err) {
104
- const m = (err as Error).message
105
- setError(m)
106
- report('场景提炼失败:' + m, 'error')
107
- } finally { setBusy(false) }
108
- }
109
-
110
- const adopt = async (s: SceneCard): Promise<void> => {
111
- setBusy(true); setError('')
112
- report('采纳场景「' + s.name + '」…')
113
- try {
114
- await api.scenes({ op: 'adopt', scene: s })
115
- setCandidates(prev => (prev ?? []).filter(x => x.name !== s.name))
116
- await refresh()
117
- report('已采纳场景「' + s.name + '」', 'done')
118
- } catch (err) {
119
- const m = (err as Error).message
120
- setError(m)
121
- report('采纳场景「' + s.name + '」失败:' + m, 'error')
122
- } finally { setBusy(false) }
123
- }
124
-
125
- const saveDraft = async (): Promise<void> => {
126
- if (draft === null) return
127
- const name = draft.name.trim()
128
- if (name === '') return
129
- setBusy(true); setError('')
130
- report('保存场景「' + name + '」…')
131
- try {
132
- await api.scenes({ op: 'update', scene: { ...draft, name } })
133
- await refresh()
134
- setDraft(null)
135
- report('已保存场景「' + name + '」', 'done')
136
- } catch (err) {
137
- const m = (err as Error).message
138
- setError(m)
139
- report('保存场景「' + name + '」失败:' + m, 'error')
140
- } finally { setBusy(false) }
141
- }
142
-
143
- const removeScene = async (name: string): Promise<void> => {
144
- if (!window.confirm('确定删除场景「' + name + '」?')) return
145
- setBusy(true); setError('')
146
- report('删除场景「' + name + '」…')
147
- try {
148
- await api.scenes({ op: 'remove', name })
149
- await refresh()
150
- setDetailName(null)
151
- report('已删除场景「' + name + '」', 'done')
152
- } catch (err) {
153
- const m = (err as Error).message
154
- setError(m)
155
- report('删除场景「' + name + '」失败:' + m, 'error')
156
- } finally { setBusy(false) }
157
- }
158
-
159
- const uploadImage = async (file: File): Promise<void> => {
160
- const target = uploadTarget
161
- if (target === null) return
162
- const dataUrl = await new Promise<string>((resolve, reject) => {
163
- const reader = new FileReader()
164
- reader.onload = () => resolve(String(reader.result))
165
- reader.onerror = () => reject(reader.error ?? new Error('读取图片失败'))
166
- reader.readAsDataURL(file)
167
- })
168
- const label = uploadLabel.trim() !== '' ? uploadLabel.trim() : '全景'
169
- setBusy(true); setError('')
170
- report('上传场景「' + target + '」' + label + '…')
171
- try {
172
- await api.scenes({ op: 'image', name: target, dataUrl, label })
173
- await refresh()
174
- report('已上传场景「' + target + '」' + label, 'done')
175
- } catch (err) {
176
- const m = (err as Error).message
177
- setError(m)
178
- report('场景图上传失败:' + m, 'error')
179
- } finally {
180
- setBusy(false)
181
- setUploadTarget(null)
182
- if (inputRef.current !== null) inputRef.current.value = ''
183
- }
184
- }
185
-
186
- const removeImage = async (name: string, label: string): Promise<void> => {
187
- setBusy(true); setError('')
188
- try {
189
- await api.scenes({ op: 'removeImage', name, label })
190
- await refresh()
191
- report('已删除「' + name + '」' + label, 'done')
192
- } catch (err) {
193
- const m = (err as Error).message
194
- setError(m)
195
- report('删除「' + name + '」' + label + ' 失败:' + m, 'error')
196
- } finally { setBusy(false) }
197
- }
198
-
199
- if (project === null) {
200
- return <div className={css.card}><span className={css.meta}>请先开书或选择一本书,再进入场景库。</span></div>
201
- }
202
-
203
- const scenes = project.scenes ?? []
204
- const filteredScenes = tierFilter === 'all' ? scenes : scenes.filter(s => (s.tier ?? 'secondary') === tierFilter)
205
- const detail = detailName !== null ? scenes.find(s => s.name === detailName) : undefined
206
-
207
- return (
208
- <div ref={rootRef} className={css.card} style={{ flex: 1, minHeight: 0 }}>
209
- <div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
210
- <span className={css.cardTitle} style={{ fontSize: 'var(--nf-fs-16)', fontWeight: 700 }}>🏞️ 场景库</span>
211
- <span className={css.meta}>场景视觉锚点:提炼自正文,供漫剧分镜/生图锁定「在哪、什么氛围」</span>
212
- <span className={css.meta} style={{ display: 'block', marginTop: 'var(--nf-space-2)' }}>流程位置:第④步场景底图 · 前置:一键生成已完成 · 下一步:第⑤步导出即梦脚本</span>
213
- </div>
214
-
215
- <div className={css.row} style={{ flexWrap: 'wrap', gap: 'var(--nf-space-6)', margin: '8px 0' }}>
216
- <span style={{ display: 'inline-flex', gap: 'var(--nf-space-2)', alignItems: 'center' }}>
217
- <span className={css.meta} style={{ fontSize: 'var(--nf-fs-12)' }}>提取章节:</span>
218
- <select className={css.input} style={{ fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-2) var(--nf-space-6)', width: 'auto' }} value={chapterNo ?? ''} onChange={e => { setChapterNo(e.target.value === '' ? undefined : Number(e.target.value)) }}>
219
- <option value="">全书(前4章)</option>
220
- {(project?.chapters ?? []).filter(c => c.status !== 'pending' && c.status !== 'generating').map(c => (
221
- <option key={c.no} value={c.no}>第{c.no}章 {c.title ?? ''}</option>
222
- ))}
223
- </select>
224
- </span>
225
- <button type="button" className={`${css.button} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void extract() }}>
226
- {busy ? '⏳ 提炼中…' : '✨ 提炼场景'}
227
- </button>
228
- <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { void refresh(); report('场景库已刷新', 'done') }}>🔄 刷新</button>
229
-
230
- <span style={{ display: 'inline-flex', gap: 'var(--nf-space-2)', alignItems: 'center' }}>
231
- <span className={css.meta} style={{ fontSize: 'var(--nf-fs-12)' }}>分级:</span>
232
- {(['all', 'core', 'secondary', 'passing'] as const).map(t => (
233
- <button key={t} type="button" className={`${css.button} ${css.buttonSmall} ${tierFilter === t ? css.buttonPrimary : ''}`} style={{ fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-2) var(--nf-space-6)' }} onClick={() => { setTierFilter(t) }}>
234
- {t === 'all' ? '全部' : t === 'core' ? '核心' : t === 'secondary' ? '次要' : '路过'}
235
- </button>
236
- ))}
237
- </span>
238
- <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => {
239
- const text = scenes.map(s => '【' + s.name + '】\n' + s.zh + '\n负面词:' + (s.negativePrompt ?? 'no people, no characters, no text, no watermark, no logo')).join('\n\n')
240
- void navigator.clipboard?.writeText(text).then(() => { notify('已复制全部场景提示词') }).catch(() => { /* ignore */ })
241
- }}>📋 复制全部提示词</button> {candidates !== null && (
242
- <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setCandidates(null) }}>收起候选</button>
243
- )}
244
- </div>
245
-
246
- {error !== '' && <div className={css.importError}>{error}</div>}
247
- {notice !== '' && <div className={css.importResult} style={{ padding: 'var(--nf-space-6) var(--nf-space-10)' }}>{notice}</div>}
248
-
249
- {/* 编辑场景 */}
250
- {draft !== null && (
251
- <div style={{ border: '1px solid var(--nf-accent)', borderRadius: 'var(--nf-radius-10)', padding: 'var(--nf-space-10)', marginBottom: 'var(--nf-space-10)', display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)' }}>
252
- <b style={{ fontSize: 'var(--nf-fs-14)' }}>编辑场景</b>
253
- <input className={css.input} style={{ fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-4) var(--nf-space-8)' }} placeholder="场景名" value={draft.name} onChange={e => { setDraft({ ...draft, name: e.target.value }) }} />
254
- <input className={css.input} style={{ fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-4) var(--nf-space-8)' }} placeholder="一句话定位" value={draft.summary} onChange={e => { setDraft({ ...draft, summary: e.target.value }) }} />
255
- <div className={css.row} style={{ gap: 'var(--nf-space-6)' }}>
256
- <input className={css.input} style={{ fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-4) var(--nf-space-8)', flex: 1 }} placeholder="幕归属(第一幕后场…)" value={draft.act ?? ''} onChange={e => { setDraft({ ...draft, act: e.target.value }) }} />
257
- <input className={css.input} style={{ fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-4) var(--nf-space-8)', flex: 1 }} placeholder="时间光态(雨夜/闭店后…)" value={draft.moment ?? ''} onChange={e => { setDraft({ ...draft, moment: e.target.value }) }} />
258
- </div>
259
- <textarea className={css.input} style={{ fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-4) var(--nf-space-8)', minHeight: 48 }} placeholder="关键镜头(每行一条:人物动作+情绪+镜头)" value={(draft.beats ?? []).join('\n')} onChange={e => { setDraft({ ...draft, beats: e.target.value.split(/\n+/).map(x => x.trim()).filter(Boolean) }) }} />
260
- <input className={css.input} style={{ fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-4) var(--nf-space-8)' }} placeholder="人物在场状态(含标志物细节)" value={draft.characterState ?? ''} onChange={e => { setDraft({ ...draft, characterState: e.target.value }) }} />
261
- <textarea className={css.input} style={{ fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-4) var(--nf-space-8)', minHeight: 60 }} placeholder="环境构成(每行一项)" value={(draft.elements ?? []).join('\n')} onChange={e => { setDraft({ ...draft, elements: e.target.value.split(/\n+/).map(x => x.trim()).filter(Boolean) }) }} />
262
- <textarea className={css.input} style={{ fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-4) var(--nf-space-8)', minHeight: 60 }} placeholder="中文生图提示词" value={draft.zh} onChange={e => { setDraft({ ...draft, zh: e.target.value }) }} />
263
- <textarea className={css.input} style={{ fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-4) var(--nf-space-8)', minHeight: 60 }} placeholder="英文生图提示词" value={draft.en} onChange={e => { setDraft({ ...draft, en: e.target.value }) }} />
264
- <div className={css.row} style={{ gap: 'var(--nf-space-6)' }}>
265
- <button type="button" className={`${css.button} ${css.buttonPrimary}`} disabled={busy || draft.name.trim() === ''} onClick={() => { void saveDraft() }}>保存</button>
266
- <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setDraft(null) }}>取消</button>
267
- </div>
268
- </div>
269
- )}
270
-
271
- {/* 候选场景 */}
272
- {candidates !== null && candidates.length > 0 && (
273
- <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)', marginBottom: 'var(--nf-space-10)' }}>
274
- {candidates.map(s => (
275
- <div key={s.name} style={{ border: '1px solid var(--nf-accent)', borderRadius: 'var(--nf-radius-10)', padding: 'var(--nf-space-8) var(--nf-space-10)', fontSize: 'var(--nf-fs-12)' }}>
276
- <div className={css.row} style={{ flexWrap: 'wrap', gap: 'var(--nf-space-6)', justifyContent: 'space-between' }}>
277
- <span><b>{s.name}</b> <span className={css.meta}>· {(s.moods ?? []).join('、')}</span>{s.styleId !== undefined && <span className={css.badge}>🎨 {s.styleId}</span>}</span>
278
- <span className={css.row} style={{ gap: 'var(--nf-space-4)' }}>
279
- <button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={busy} onClick={() => { void adopt(s) }}>采纳</button>
280
- <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setDraft(s); setCandidates(null) }}>修改后采纳</button>
281
- </span>
282
- </div>
283
- <div className={css.meta} style={{ marginTop: 'var(--nf-space-2)' }}>{s.summary}</div>
284
- <div className={css.meta} style={{ marginTop: 'var(--nf-space-2)', fontSize: 'var(--nf-fs-12)' }}>要素:{(s.elements ?? []).join(';')}</div>
285
- </div>
286
- ))}
287
- </div>
288
- )}
289
-
290
- {filteredScenes.length === 0 && candidates === null ? (
291
- <div className={css.shelfEmpty} style={{ minHeight: 140, flex: 1 }}>
292
- <span className={css.shelfEmptyIcon}>🏞️</span>
293
- <span className={css.shelfEmptyTitle}>场景库为空</span>
294
- <span className={css.meta}>点「✨ 按当前方案提炼场景」自动建立,或手动新增</span>
295
- </div>
296
- ) : (
297
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))', gap: 'var(--nf-space-10)' }}>
298
- {filteredScenes.map(s => {
299
- const cover = (s.gallery ?? [])[0]
300
- return (
301
- <div key={s.name} onClick={() => { openDetail(s.name) }} role="button" tabIndex={0} aria-label={`打开场景 ${s.name}`} onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openDetail(s.name) } }} style={{ cursor: 'pointer', border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-12)', overflow: 'hidden', background: 'var(--nf-bg-inset)', display: 'flex', flexDirection: 'column' }}>
302
- <div style={{ height: 100, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--nf-bg-inset)', borderBottom: '1px solid var(--nf-border)', position: 'relative' }}>
303
- {cover !== undefined
304
- ? <img src={cover.dataUrl} alt={s.name} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
305
- : <span className={css.meta}>暂无场景图</span>}
306
- <span style={{ position: 'absolute', bottom: 4, right: 4, background: 'rgba(0,0,0,0.6)', color: '#fff', fontSize: 'var(--nf-fs-12)', borderRadius: 'var(--nf-radius-6)', padding: 'var(--nf-space-2) var(--nf-space-6)' }}>🖼 {(s.gallery ?? []).length}</span>
307
- </div>
308
- <div style={{ padding: 'var(--nf-space-6)', display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-2)' }}>
309
- <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--nf-space-4)', flexWrap: 'wrap' }}>
310
- <b style={{ fontSize: 'var(--nf-fs-14)' }}>{s.name}</b>
311
- <span style={{ fontSize: 'var(--nf-fs-11)', padding: '1px 6px', borderRadius: 'var(--nf-radius-999)', background: (s.tier ?? 'secondary') === 'core' ? 'rgba(224,85,85,0.15)' : (s.tier ?? 'secondary') === 'passing' ? 'rgba(128,128,128,0.15)' : 'rgba(85,136,224,0.15)', color: (s.tier ?? 'secondary') === 'core' ? '#e05555' : (s.tier ?? 'secondary') === 'passing' ? '#888' : '#5588e0' }}>{(s.tier ?? 'secondary') === 'core' ? '核心' : (s.tier ?? 'secondary') === 'passing' ? '路过' : '次要'}</span>
312
- {s.styleId !== undefined && <span className={css.badge} style={{ fontSize: 'var(--nf-fs-12)' }}>🎨 {s.styleId}</span>}
313
- </div>
314
- <div className={css.meta} style={{ fontSize: 'var(--nf-fs-12)' }}>{(s.moods ?? []).join('、') || s.summary.slice(0, 16)}</div>
315
- </div>
316
- </div>
317
- )
318
- })}
319
- </div>
320
- )}
321
-
322
- {/* 场景详情浮窗(portal 到 body,避免 .view 的 backdrop-filter 破坏 fixed 定位) */}
323
- {detail !== undefined && createPortal(
324
- <div style={{ ...portalVars, position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.55)', zIndex: 2147483000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 'var(--nf-space-24)' }} onClick={e => { if (e.target === e.currentTarget) setDetailName(null) }}>
325
- <div style={{ position: 'relative', background: 'var(--nf-bg)', border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-16)', padding: 'var(--nf-space-16)', width: 'min(860px, 100%)', maxHeight: '88vh', overflow: 'auto', marginTop: 'var(--nf-space-24)' }}>
326
- <button type="button" className={css.iconButton} style={{ position: 'absolute', top: 10, right: 10 }} title="关闭" aria-label="关闭" onClick={() => { setDetailName(null) }}>✕</button>
327
- <div style={{ marginBottom: 'var(--nf-space-12)' }}>
328
- <div style={{ display: 'flex', alignItems: 'baseline', gap: 'var(--nf-space-8)', flexWrap: 'wrap' }}>
329
- <b style={{ fontSize: 'var(--nf-fs-20)' }}>{detail.name}</b>
330
- <span style={{ fontSize: 'var(--nf-fs-12)', padding: '2px 8px', borderRadius: 'var(--nf-radius-999)', background: (detail.tier ?? 'secondary') === 'core' ? 'rgba(224,85,85,0.15)' : (detail.tier ?? 'secondary') === 'passing' ? 'rgba(128,128,128,0.15)' : 'rgba(85,136,224,0.15)', color: (detail.tier ?? 'secondary') === 'core' ? '#e05555' : (detail.tier ?? 'secondary') === 'passing' ? '#888' : '#5588e0' }}>{(detail.tier ?? 'secondary') === 'core' ? '核心场景' : (detail.tier ?? 'secondary') === 'passing' ? '路过场景' : '次要场景'}</span>
331
- {detail.styleId !== undefined && <span className={css.badge}>🎨 {detail.styleId}</span>}
332
- {(detail.act !== undefined && detail.act !== '') && <span className={css.badge} style={{ borderColor: 'var(--nf-accent)', color: 'var(--nf-accent)' }}>{detail.act}</span>}
333
- {(detail.moment !== undefined && detail.moment !== '') && <span className={css.meta}>{detail.moment}</span>}
334
- </div>
335
- <div className={css.meta} style={{ marginTop: 'var(--nf-space-4)' }}>{detail.summary}</div>
336
- </div>
337
-
338
- {/* 图集 */}
339
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))', gap: 'var(--nf-space-8)', marginBottom: 'var(--nf-space-12)' }}>
340
- {(detail.gallery ?? []).map(img => (
341
- <div key={img.label} style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-10)', overflow: 'hidden', position: 'relative' }}>
342
- <img src={img.dataUrl} alt={img.label} style={{ width: '100%', height: 100, objectFit: 'cover', display: 'block' }} />
343
- <div style={{ background: 'rgba(0,0,0,0.55)', color: '#fff', fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-2) var(--nf-space-6)', textAlign: 'center' }}>{img.label}</div>
344
- <button type="button" className={css.iconButton} style={{ position: 'absolute', top: 2, right: 2, fontSize: 'var(--nf-fs-12)' }} title="删除" aria-label="删除" onClick={() => { void removeImage(detail.name, img.label) }}>×</button>
345
- </div>
346
- ))}
347
- {(detail.gallery ?? []).length === 0 && <span className={css.meta}>暂无场景图</span>}
348
- </div>
349
-
350
- {/* 场景信息(紧凑卡片) */}
351
- <div style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-10)', padding: 'var(--nf-space-8) var(--nf-space-10)', marginBottom: 'var(--nf-space-10)', fontSize: 'var(--nf-fs-12)' }}>
352
- <b style={{ fontSize: 'var(--nf-fs-12)' }}>场景信息</b>
353
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: '6px 14px', marginTop: 'var(--nf-space-4)', lineHeight: 1.7 }}>
354
- <div>
355
- <b style={{ fontSize: 'var(--nf-fs-12)' }}>关键镜头</b>
356
- <div style={{ marginTop: 'var(--nf-space-2)' }}>{(detail.beats ?? []).map((b, i) => <div key={i} style={{ fontSize: 'var(--nf-fs-12)', color: 'var(--nf-text-2)', marginTop: 'var(--nf-space-2)' }}>🎬 {b}</div>)}</div>
357
- </div>
358
- <div>
359
- {(detail.characterState !== undefined && detail.characterState !== '') && (
360
- <div style={{ marginBottom: 'var(--nf-space-6)' }}>
361
- <b style={{ fontSize: 'var(--nf-fs-12)' }}>人物状态</b>
362
- <div className={css.meta} style={{ marginTop: 'var(--nf-space-2)' }}>{detail.characterState}</div>
363
- </div>
364
- )}
365
- <b style={{ fontSize: 'var(--nf-fs-12)' }}>环境构成</b>
366
- <div style={{ marginTop: 'var(--nf-space-2)' }}>{(detail.elements ?? []).map(e => <span key={e} style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-999)', padding: 'var(--nf-space-2) var(--nf-space-8)', fontSize: 'var(--nf-fs-12)', margin: '0 4px 4px 0', display: 'inline-block' }}>{e}</span>)}</div>
367
- <div style={{ marginTop: 'var(--nf-space-6)' }}><b style={{ fontSize: 'var(--nf-fs-12)' }}>色调光影</b> <span className={css.meta}>{(detail.palette ?? []).join(';') || '—'}</span></div>
368
- <div style={{ marginTop: 'var(--nf-space-4)' }}><b style={{ fontSize: 'var(--nf-fs-12)' }}>氛围</b> <span className={css.meta}>{(detail.moods ?? []).join('、') || '—'}</span></div>
369
- <div style={{ marginTop: 'var(--nf-space-4)' }}><b style={{ fontSize: 'var(--nf-fs-12)' }}>依据</b> <span className={css.meta}>{detail.source || '—'}</span></div>
370
- </div>
371
- </div>
372
- </div>
373
-
374
- {/* 生图提示词:全局中英 + 复制全部 */}
375
- <div style={{ marginBottom: 'var(--nf-space-12)' }}>
376
- <div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap', marginBottom: 'var(--nf-space-6)' }}>
377
- <b style={{ fontSize: 'var(--nf-fs-14)' }}>🎨 生图提示词</b>
378
- <span className={css.row} style={{ gap: 'var(--nf-space-6)', flexWrap: 'wrap' }}>
379
- <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => {
380
- const text = '【中文】\n' + detail.zh
381
- void navigator.clipboard?.writeText(text).then(() => { notify('已复制「' + detail.name + '」生图提示词') }).catch(() => { /* ignore */ })
382
- }}>📋 复制全部</button>
383
- </span>
384
- </div>
385
- <div style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-8)', padding: 'var(--nf-space-8)' }}>
386
- <div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
387
- <b style={{ fontSize: 'var(--nf-fs-12)' }}>🇨🇳 中文</b>
388
- <button type="button" className={`${css.button} ${css.buttonSmall}`} style={{ padding: 'var(--nf-space-2) var(--nf-space-6)', fontSize: 'var(--nf-fs-12)' }} onClick={() => {
389
- void navigator.clipboard?.writeText(detail.zh).then(() => { notify('已复制「' + detail.name + '」生图提示词') }).catch(() => { /* ignore */ })
390
- }}>复制</button>
391
- </div>
392
- <div className={css.meta} style={{ fontSize: 'var(--nf-fs-12)', lineHeight: 1.6, marginTop: 'var(--nf-space-4)', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{detail.zh}</div>
393
- <div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--nf-space-4)', marginTop: 'var(--nf-space-8)' }}>
394
- {(detail.tags ?? []).map(t => <span key={t} style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-999)', padding: 'var(--nf-space-2) var(--nf-space-8)', fontSize: 'var(--nf-fs-12)' }}>{t}</span>)}
395
- </div>
396
- </div>
397
- {/* 负面提示词 */}
398
- <div style={{ marginTop: 'var(--nf-space-8)', border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-8)', padding: 'var(--nf-space-8)' }}>
399
- <div className={css.row} style={{ justifyContent: 'space-between' }}>
400
- <b style={{ fontSize: 'var(--nf-fs-12)' }}>🚫 负面提示词(场景专用)</b>
401
- <button type="button" className={`${css.button} ${css.buttonSmall}`} style={{ padding: 'var(--nf-space-2) var(--nf-space-6)', fontSize: 'var(--nf-fs-12)' }} onClick={() => {
402
- void navigator.clipboard?.writeText(detail.negativePrompt ?? 'no people, no characters, no text, no watermark, no logo, no subtitles, blurry, low quality, distorted, deformed').then(() => { notify('已复制负面提示词') }).catch(() => { /* ignore */ })
403
- }}>复制</button>
404
- </div>
405
- <div className={css.meta} style={{ fontSize: 'var(--nf-fs-12)', lineHeight: 1.6, marginTop: 'var(--nf-space-4)', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{detail.negativePrompt ?? 'no people, no characters, no text, no watermark, no logo, no subtitles, blurry, low quality, distorted, deformed'}</div>
406
- </div>
407
- </div>
408
-
409
- {/* 即梦使用指引 */}
410
- <div style={{ border: '1px solid var(--nf-accent)', borderRadius: 'var(--nf-radius-10)', padding: 'var(--nf-space-8) var(--nf-space-10)', marginBottom: 'var(--nf-space-12)', fontSize: 'var(--nf-fs-12)', background: 'rgba(85,136,224,0.05)' }}>
411
- <b style={{ fontSize: 'var(--nf-fs-12)', color: 'var(--nf-accent)' }}>🎬 即梦使用指引</b>
412
- <div style={{ marginTop: 'var(--nf-space-4)', lineHeight: 1.7, color: 'var(--nf-text-2)' }}>
413
- <div>1. 上方场景图可作为即梦「首帧参考图」或「背景参考图」上传</div>
414
- <div>2. 分镜提示词中已自动标注使用的场景名,导出时会附带场景图路径</div>
415
- <div>3. 核心场景建议生成多角度/多氛围图(全景/局部/氛围),提升视频一致性</div>
416
- <div>4. 资产库路径:manga-assets/场景/场景名-标签.png</div>
417
- </div>
418
- </div>
419
-
420
- {/* 操作条 */}
421
- <div style={{ borderTop: '1px solid var(--nf-border)', paddingTop: 'var(--nf-space-10)', display: 'flex', flexWrap: 'wrap', gap: 'var(--nf-space-6)', alignItems: 'center' }}>
422
- <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => {
423
- const text = detail.zh + (detail.negativePrompt !== undefined && detail.negativePrompt !== '' ? '\n负面词:' + detail.negativePrompt : '')
424
- void navigator.clipboard?.writeText(text).then(() => { notify('已复制「' + detail.name + '」生图提示词') }).catch(() => { /* ignore */ })
425
- }}>复制提示词</button>
426
-
427
- <input className={css.input} style={{ width: 130, fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-4) var(--nf-space-6)' }} placeholder="图集标签" value={uploadLabel} onChange={e => { setUploadLabel(e.target.value) }} />
428
- <input
429
- ref={inputRef}
430
- type="file"
431
- accept="image/*"
432
- style={{ display: 'none' }}
433
- onChange={e => {
434
- const file = e.target.files?.[0]
435
- if (file === undefined || uploadTarget === null) return
436
- void uploadImage(file)
437
- }}
438
- />
439
- <button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={busy} onClick={() => {
440
- setUploadLabel(uploadLabel.trim() !== '' ? uploadLabel.trim() : '全景')
441
- setUploadTarget(detail.name)
442
- inputRef.current?.click()
443
- }}>📤 上传场景图</button>
444
- <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setDraft(detail); setDetailName(null) }}>✏️ 编辑</button>
445
- <button type="button" className={`${css.button} ${css.buttonSmall}`} style={{ color: 'var(--nf-danger, #e05)' }} onClick={() => { void removeScene(detail.name) }}>🗑 删除</button>
446
- </div>
447
- </div>
448
- </div>,
449
- document.body,
450
- )}
451
- </div>
452
- )
453
- }