@waterwx/dsh-novel-forge 1.6.0 → 1.7.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.
- package/lib/client.js +37690 -2269
- package/lib/client.js.map +1 -1
- package/lib/index.js +767 -56
- package/lib/index.js.map +1 -1
- package/lib/types/client/api.d.ts +3 -0
- package/lib/types/client/locales.d.ts +0 -1
- package/lib/types/client/panel/FlowGuide.d.ts +21 -0
- package/lib/types/client/panel/MangaRoleLibrary.d.ts +18 -0
- package/lib/types/client/panel/MangaWorkspace.d.ts +6 -4
- package/lib/types/client/panel/RoleVisualPanel.d.ts +5 -3
- package/lib/types/client/panel/SceneLibrary.d.ts +1 -1
- package/lib/types/client/panel/StoryboardTab.d.ts +6 -2
- package/lib/types/engine.d.ts +23 -9
- package/lib/types/index.d.ts +6 -4
- package/lib/types/protocol.d.ts +171 -3
- package/package.json +100 -98
- package/src/client/api.ts +9 -0
- package/src/client/locales.ts +0 -1
- package/src/client/panel/AssetsTab.tsx +22 -22
- package/src/client/panel/AssistantTab.tsx +3 -3
- package/src/client/panel/BookshelfBar.tsx +1 -1
- package/src/client/panel/CreateBookView.tsx +10 -10
- package/src/client/panel/FlowGuide.tsx +83 -0
- package/src/client/panel/MangaRoleLibrary.tsx +810 -0
- package/src/client/panel/MangaWorkspace.tsx +359 -293
- package/src/client/panel/NovelPanel.tsx +465 -307
- package/src/client/panel/ReaderView.tsx +4 -4
- package/src/client/panel/RunPanel.tsx +9 -9
- package/src/client/panel/SceneLibrary.tsx +69 -68
- package/src/client/panel/ShelfView.tsx +28 -35
- package/src/client/panel/StoryboardTab.tsx +106 -42
- package/src/client/panel/StyleCard.tsx +13 -13
- package/src/client/panel/StyleGallery.tsx +2 -2
- package/src/client/panel/WorldTab.tsx +5 -5
- package/src/client/panel/panel.module.css +1189 -454
- package/src/client/panel/views.tsx +24 -24
- package/src/engine.ts +530 -59
- package/src/index.ts +37 -6
- package/src/protocol.ts +175 -3
- package/src/routes.ts +229 -1
- package/src/client/panel/RoleVisualPanel.tsx +0 -468
package/package.json
CHANGED
|
@@ -1,98 +1,100 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@waterwx/dsh-novel-forge",
|
|
3
|
-
"description": "AI 编译小说工作台 for the dsh web GUI: import a novel outline (docx/text), plan chapters with LLM, generate chapter by chapter (3000-4000 chars each), and save every chapter as Markdown into your chosen output folder.",
|
|
4
|
-
"version": "1.
|
|
5
|
-
"type": "module",
|
|
6
|
-
"engines": {
|
|
7
|
-
"node": "^22.19.0 || >=24.0.0"
|
|
8
|
-
},
|
|
9
|
-
"main": "lib/index.js",
|
|
10
|
-
"types": "lib/types/index.d.ts",
|
|
11
|
-
"exports": {
|
|
12
|
-
".": {
|
|
13
|
-
"types": "./lib/types/index.d.ts",
|
|
14
|
-
"default": "./lib/index.js"
|
|
15
|
-
},
|
|
16
|
-
"./client": {
|
|
17
|
-
"types": "./lib/types/client/index.d.ts",
|
|
18
|
-
"default": "./lib/client.js"
|
|
19
|
-
},
|
|
20
|
-
"./src/*": "./src/*",
|
|
21
|
-
"./package.json": "./package.json"
|
|
22
|
-
},
|
|
23
|
-
"dsh": {
|
|
24
|
-
"bundle": {
|
|
25
|
-
"patch": "./cordis.patch.yml"
|
|
26
|
-
},
|
|
27
|
-
"client": {
|
|
28
|
-
"inject": [
|
|
29
|
-
"@deepseek-ai/dsh-client-runtime",
|
|
30
|
-
"@deepseek-ai/dsh-client-connection",
|
|
31
|
-
"@deepseek-ai/dsh-client-ui-settings"
|
|
32
|
-
],
|
|
33
|
-
"platform": "web"
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
"dependencies": {
|
|
37
|
-
"fflate": "^0.8.2",
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"@deepseek-ai/dsh-client-
|
|
43
|
-
"@deepseek-ai/dsh-client-
|
|
44
|
-
"@deepseek-ai/dsh-client-
|
|
45
|
-
"@deepseek-ai/dsh-client-ui-
|
|
46
|
-
"@deepseek-ai/dsh-
|
|
47
|
-
"@deepseek-ai/dsh-
|
|
48
|
-
"@deepseek-ai/dsh-
|
|
49
|
-
"@deepseek-ai/dsh-
|
|
50
|
-
"@deepseek-ai/dsh-
|
|
51
|
-
"@deepseek-ai/dsh-
|
|
52
|
-
"
|
|
53
|
-
"react
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"@deepseek-ai/
|
|
58
|
-
"@deepseek-ai/dsh-client-
|
|
59
|
-
"@deepseek-ai/dsh-client-
|
|
60
|
-
"@deepseek-ai/dsh-client-
|
|
61
|
-
"@deepseek-ai/dsh-client-ui-
|
|
62
|
-
"@deepseek-ai/dsh-
|
|
63
|
-
"@deepseek-ai/dsh-
|
|
64
|
-
"@deepseek-ai/dsh-
|
|
65
|
-
"@deepseek-ai/dsh-
|
|
66
|
-
"@deepseek-ai/dsh-
|
|
67
|
-
"@deepseek-ai/dsh-
|
|
68
|
-
"@
|
|
69
|
-
"@types/
|
|
70
|
-
"@types/react
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"react
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
"lib/**/*.js
|
|
81
|
-
"lib/**/*.
|
|
82
|
-
"lib/**/*.d.ts
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
"
|
|
91
|
-
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
|
|
98
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@waterwx/dsh-novel-forge",
|
|
3
|
+
"description": "AI 编译小说工作台 for the dsh web GUI: import a novel outline (docx/text), plan chapters with LLM, generate chapter by chapter (3000-4000 chars each), and save every chapter as Markdown into your chosen output folder.",
|
|
4
|
+
"version": "1.7.2",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
8
|
+
},
|
|
9
|
+
"main": "lib/index.js",
|
|
10
|
+
"types": "lib/types/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./lib/types/index.d.ts",
|
|
14
|
+
"default": "./lib/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./client": {
|
|
17
|
+
"types": "./lib/types/client/index.d.ts",
|
|
18
|
+
"default": "./lib/client.js"
|
|
19
|
+
},
|
|
20
|
+
"./src/*": "./src/*",
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"dsh": {
|
|
24
|
+
"bundle": {
|
|
25
|
+
"patch": "./cordis.patch.yml"
|
|
26
|
+
},
|
|
27
|
+
"client": {
|
|
28
|
+
"inject": [
|
|
29
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
30
|
+
"@deepseek-ai/dsh-client-connection",
|
|
31
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
32
|
+
],
|
|
33
|
+
"platform": "web"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"fflate": "^0.8.2",
|
|
38
|
+
"lucide-react": "^1.33.0",
|
|
39
|
+
"schemastery": "^3.18.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.5",
|
|
43
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.5",
|
|
44
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.5",
|
|
45
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.5",
|
|
46
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.5",
|
|
47
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.5",
|
|
48
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.5",
|
|
49
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.5",
|
|
50
|
+
"@deepseek-ai/dsh-skill": "^0.1.0-rc.5",
|
|
51
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.5",
|
|
52
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.5",
|
|
53
|
+
"react": "^18.2.0",
|
|
54
|
+
"react-dom": "^18.2.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
58
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.5",
|
|
59
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.5",
|
|
60
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.5",
|
|
61
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.5",
|
|
62
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.5",
|
|
63
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.5",
|
|
64
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.5",
|
|
65
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.5",
|
|
66
|
+
"@deepseek-ai/dsh-skill": "^0.1.0-rc.5",
|
|
67
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.5",
|
|
68
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.5",
|
|
69
|
+
"@types/node": "^22.20.0",
|
|
70
|
+
"@types/react": "~18.3.1",
|
|
71
|
+
"@types/react-dom": "^18.3.5",
|
|
72
|
+
"lightningcss": "1.32.0",
|
|
73
|
+
"react": "^18.3.1",
|
|
74
|
+
"react-dom": "^18.3.1",
|
|
75
|
+
"tsdown": "0.22.2",
|
|
76
|
+
"typescript": "~5.7.2",
|
|
77
|
+
"vitest": "^3.0.0"
|
|
78
|
+
},
|
|
79
|
+
"files": [
|
|
80
|
+
"lib/**/*.js",
|
|
81
|
+
"lib/**/*.js.map",
|
|
82
|
+
"lib/**/*.d.ts",
|
|
83
|
+
"lib/**/*.d.ts.map",
|
|
84
|
+
"src",
|
|
85
|
+
"assets/styles",
|
|
86
|
+
"scripts/patch-schemastery.mjs",
|
|
87
|
+
"cordis.patch.yml",
|
|
88
|
+
"README.md"
|
|
89
|
+
],
|
|
90
|
+
"license": "Apache-2.0",
|
|
91
|
+
"scripts": {
|
|
92
|
+
"postinstall": "node scripts/patch-schemastery.mjs",
|
|
93
|
+
"build": "tsc -p tsconfig.build.json && tsdown",
|
|
94
|
+
"bundle": "tsdown",
|
|
95
|
+
"watch": "tsdown --watch",
|
|
96
|
+
"typecheck": "tsc --noEmit",
|
|
97
|
+
"check:styles": "node scripts/check-theme-sizes.mjs",
|
|
98
|
+
"test": "vitest run"
|
|
99
|
+
}
|
|
100
|
+
}
|
package/src/client/api.ts
CHANGED
|
@@ -228,10 +228,19 @@ export class NovelApi {
|
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
/** 角色库:AI 提炼 / 采纳 / 更新 / 删除。 */
|
|
231
|
+
async imageTest(req: import('../protocol.ts').ImageTestRequest): Promise<import('../protocol.ts').ImageTestResponse> {
|
|
232
|
+
return postJson<import('../protocol.ts').ImageTestResponse>(NOVEL_API.imageTest, req)
|
|
233
|
+
}
|
|
234
|
+
|
|
231
235
|
async roles(req: import('../protocol.ts').RolesRequest): Promise<import('../protocol.ts').RolesResponse> {
|
|
232
236
|
return postJson<import('../protocol.ts').RolesResponse>(NOVEL_API.roles, req)
|
|
233
237
|
}
|
|
234
238
|
|
|
239
|
+
/** 漫剧角色库:从分镜提名 / 建卡 / 更新 / 删除 / 形象锚点 / 精修提示词。 */
|
|
240
|
+
async mangaRoles(req: import('../protocol.ts').MangaRolesRequest): Promise<import('../protocol.ts').MangaRolesResponse> {
|
|
241
|
+
return postJson<import('../protocol.ts').MangaRolesResponse>(NOVEL_API.mangaRoles, req)
|
|
242
|
+
}
|
|
243
|
+
|
|
235
244
|
/** 场景库:AI 提炼 / 采纳 / 更新 / 删除 / 图集。 */
|
|
236
245
|
async scenes(req: import('../protocol.ts').ScenesRequest): Promise<import('../protocol.ts').ScenesResponse> {
|
|
237
246
|
return postJson<import('../protocol.ts').ScenesResponse>(NOVEL_API.scenes, req)
|
package/src/client/locales.ts
CHANGED
|
@@ -154,7 +154,6 @@ export const zh = {
|
|
|
154
154
|
'settings.reasoning.max': 'max · 最高思考(质检/推演)',
|
|
155
155
|
'settings.theme': '主题',
|
|
156
156
|
'settings.themeLiquid': 'iOS 液态玻璃(绿)',
|
|
157
|
-
'settings.themeClassic': '经典毛玻璃(蓝)',
|
|
158
157
|
'settings.themeNeumorph': '新拟物(双阴影)',
|
|
159
158
|
'settings.themeHint': '存于浏览器本地,即时生效',
|
|
160
159
|
'settings.chapterChars': '每章目标字数',
|
|
@@ -19,7 +19,7 @@ export interface AssetsTabProps {
|
|
|
19
19
|
function GenreTree({ node, selected, onSelect }: { node: GenreNode; selected: string; onSelect: (node: GenreNode) => void }) {
|
|
20
20
|
return (
|
|
21
21
|
<div>
|
|
22
|
-
<label style={{ display: 'flex', alignItems: 'flex-start', gap: 6, cursor: 'pointer' }}>
|
|
22
|
+
<label style={{ display: 'flex', alignItems: 'flex-start', gap: 'var(--nf-space-6)', cursor: 'pointer' }}>
|
|
23
23
|
<input type="radio" name="genre" checked={selected === node.name} onChange={() => { onSelect(node) }} />
|
|
24
24
|
<span>
|
|
25
25
|
<b>{node.name}</b>
|
|
@@ -34,7 +34,7 @@ function GenreTree({ node, selected, onSelect }: { node: GenreNode; selected: st
|
|
|
34
34
|
</span>
|
|
35
35
|
</label>
|
|
36
36
|
{node.children.length > 0 && (
|
|
37
|
-
<div style={{ marginLeft:
|
|
37
|
+
<div style={{ marginLeft: 'var(--nf-space-24)', display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-4)' }}>
|
|
38
38
|
{node.children.map(child => (
|
|
39
39
|
<GenreTree key={child.name} node={child} selected={selected} onSelect={onSelect} />
|
|
40
40
|
))}
|
|
@@ -205,9 +205,9 @@ export function AssetsTab({ api, initialTab = 'genre' }: AssetsTabProps) {
|
|
|
205
205
|
const genreLibrary = data.genreLibrary
|
|
206
206
|
|
|
207
207
|
return (
|
|
208
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, flex: 1, minHeight: 0 }}>
|
|
208
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-12)', flex: 1, minHeight: 0 }}>
|
|
209
209
|
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
210
|
-
<span className={css.cardTitle} style={{ fontSize:
|
|
210
|
+
<span className={css.cardTitle} style={{ fontSize: 'var(--nf-fs-16)', fontWeight: 700 }}>🧰 创作资产</span>
|
|
211
211
|
<span className={css.meta}>题材基底 / 推进模式 / 笔法帖 / 文戒 / 心法</span>
|
|
212
212
|
</div>
|
|
213
213
|
{error !== '' && <div className={css.card} style={{ borderColor: 'var(--nf-error)' }}><span style={{ color: 'var(--nf-error)' }}>{tt('common.error')}: {error}</span></div>}
|
|
@@ -240,13 +240,13 @@ export function AssetsTab({ api, initialTab = 'genre' }: AssetsTabProps) {
|
|
|
240
240
|
</div>
|
|
241
241
|
|
|
242
242
|
{/* 子页签:题材基底 / 推进模式 / 笔法帖 / 文戒 / 心法 */}
|
|
243
|
-
<div className={css.row} style={{ flexWrap: 'wrap', gap: 6 }}>
|
|
243
|
+
<div className={css.row} style={{ flexWrap: 'wrap', gap: 'var(--nf-space-6)' }}>
|
|
244
244
|
{SUB_TABS.map(t => (
|
|
245
245
|
<button
|
|
246
246
|
key={t.id}
|
|
247
247
|
type="button"
|
|
248
248
|
className={`${css.button} ${assetTab === t.id ? css.buttonPrimary : ''}`}
|
|
249
|
-
style={{ fontSize: 14, flex: 1 }}
|
|
249
|
+
style={{ fontSize: 'var(--nf-fs-14)', flex: 1 }}
|
|
250
250
|
onClick={() => { setAssetTab(t.id) }}
|
|
251
251
|
>
|
|
252
252
|
{t.label}
|
|
@@ -260,12 +260,12 @@ export function AssetsTab({ api, initialTab = 'genre' }: AssetsTabProps) {
|
|
|
260
260
|
<span className={css.cardTitle}>题材基底库</span>
|
|
261
261
|
<span className={css.meta}>这本书属于哪个阅读市场?题材定位会注入章节生成与审稿提示词。</span>
|
|
262
262
|
{assets.genre !== undefined && (
|
|
263
|
-
<div style={{ border: '1px solid var(--nf-border)', borderRadius: 6, padding: '
|
|
263
|
+
<div style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-6)', padding: 'var(--nf-space-6) var(--nf-space-10)', fontSize: 'var(--nf-fs-12)' }}>
|
|
264
264
|
<b>当前题材:{assets.genre.name}</b> — {assets.genre.description}
|
|
265
265
|
</div>
|
|
266
266
|
)}
|
|
267
267
|
{/* 题材基底库:自然展开,不套内部滚动条(描述两行截断,悬停看全文) */}
|
|
268
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
268
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-8)' }}>
|
|
269
269
|
{genreLibrary.map(root => <GenreTree key={root.name} node={root} selected={assets.genre?.name ?? ''} onSelect={handleSelectGenre} />)}
|
|
270
270
|
</div>
|
|
271
271
|
</div>
|
|
@@ -277,17 +277,17 @@ export function AssetsTab({ api, initialTab = 'genre' }: AssetsTabProps) {
|
|
|
277
277
|
<span className={css.cardTitle}>推进模式库</span>
|
|
278
278
|
<span className={css.meta}>读者为什么继续看下一章?主模式 + 辅助模式注入卷规划与章节生成。</span>
|
|
279
279
|
{assets.primaryProgression !== undefined && (
|
|
280
|
-
<div style={{ border: '1px solid var(--nf-accent)', borderRadius: 6, padding: '
|
|
280
|
+
<div style={{ border: '1px solid var(--nf-accent)', borderRadius: 'var(--nf-radius-6)', padding: 'var(--nf-space-6) var(--nf-space-10)', fontSize: 'var(--nf-fs-12)', color: 'var(--nf-accent)' }}>
|
|
281
281
|
<b>主推进:{assets.primaryProgression.name}</b> — {assets.primaryProgression.driver}
|
|
282
282
|
</div>
|
|
283
283
|
)}
|
|
284
284
|
{assets.auxiliaryProgressions.map(mode => (
|
|
285
|
-
<div key={mode.name} style={{ border: '1px solid var(--nf-border)', borderRadius: 6, padding: '
|
|
285
|
+
<div key={mode.name} style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-6)', padding: 'var(--nf-space-6) var(--nf-space-10)', fontSize: 'var(--nf-fs-12)' }}>
|
|
286
286
|
<b>{mode.name}</b> — {mode.driver}
|
|
287
287
|
</div>
|
|
288
288
|
))}
|
|
289
289
|
<span className={css.meta}>从内置推进模式库选择添加(第一个设为主推进):</span>
|
|
290
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, flex: 1, minHeight: 0, overflowY: 'auto' }}>
|
|
290
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)', flex: 1, minHeight: 0, overflowY: 'auto' }}>
|
|
291
291
|
{data.progressionLibrary.map(mode => {
|
|
292
292
|
const alreadyPrimary = assets.primaryProgression?.name === mode.name
|
|
293
293
|
const alreadyAux = assets.auxiliaryProgressions.some(m => m.name === mode.name)
|
|
@@ -307,12 +307,12 @@ export function AssetsTab({ api, initialTab = 'genre' }: AssetsTabProps) {
|
|
|
307
307
|
<div className={css.card} style={{ flex: 1, minHeight: 0 }}>
|
|
308
308
|
<span className={css.cardTitle}>笔法帖</span>
|
|
309
309
|
<span className={css.meta}>从内置 8 套叙事风格模板中一键选用(来自 AI-Novel-Writing-Assistant 写法引擎),无需样本文本;绑定后生成与润色都遵循该风格。</span>
|
|
310
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, flex: 1, minHeight: 0, overflowY: 'auto' }}>
|
|
310
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-8)', flex: 1, minHeight: 0, overflowY: 'auto' }}>
|
|
311
311
|
{data.styleTemplates.map(template => {
|
|
312
312
|
const bound = assets.styleAssets.some(s => s.name === template.name)
|
|
313
313
|
return (
|
|
314
|
-
<div key={template.key} style={{ border: `1px solid ${bound ? 'var(--nf-accent)' : 'var(--nf-border)'}`, borderRadius: 6, padding: '
|
|
315
|
-
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
|
314
|
+
<div key={template.key} style={{ border: `1px solid ${bound ? 'var(--nf-accent)' : 'var(--nf-border)'}`, borderRadius: 'var(--nf-radius-6)', padding: 'var(--nf-space-8) var(--nf-space-10)', fontSize: 'var(--nf-fs-12)' }}>
|
|
315
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--nf-space-8)' }}>
|
|
316
316
|
<span><b>{template.name}</b> <span className={css.badge} style={{ borderColor: 'var(--nf-text-3)', color: 'var(--nf-text-3)' }}>{template.category}</span></span>
|
|
317
317
|
<button
|
|
318
318
|
className={`${css.button} ${css.buttonSmall} ${bound ? '' : css.buttonPrimary}`}
|
|
@@ -347,12 +347,12 @@ export function AssetsTab({ api, initialTab = 'genre' }: AssetsTabProps) {
|
|
|
347
347
|
<div className={css.card} style={{ flex: 1, minHeight: 0 }}>
|
|
348
348
|
<span className={css.cardTitle}>文戒</span>
|
|
349
349
|
<span className={css.meta}>写作时必须遵守的表达边界(内置全局 + 项目自定义),生成与审稿都会检查。内置规则可用「覆盖编辑」复制为自定义版本调整。</span>
|
|
350
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, flex: 1, minHeight: 0, overflowY: 'auto' }}>
|
|
350
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)', flex: 1, minHeight: 0, overflowY: 'auto' }}>
|
|
351
351
|
{builtinRules.map(rule => {
|
|
352
352
|
const overridden = customRules.some(r => r.name === rule.name)
|
|
353
353
|
return (
|
|
354
|
-
<div key={rule.name} style={{ border: '1px solid var(--nf-border)', borderRadius: 6, padding: '
|
|
355
|
-
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
|
354
|
+
<div key={rule.name} style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-6)', padding: 'var(--nf-space-6) var(--nf-space-10)', fontSize: 'var(--nf-fs-12)' }}>
|
|
355
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--nf-space-8)' }}>
|
|
356
356
|
<span>
|
|
357
357
|
<b>{rule.name}</b>{' '}
|
|
358
358
|
<span className={css.badge} style={{ borderColor: overridden ? 'var(--nf-accent)' : 'var(--nf-text-3)', color: overridden ? 'var(--nf-accent)' : 'var(--nf-text-3)' }}>
|
|
@@ -369,13 +369,13 @@ export function AssetsTab({ api, initialTab = 'genre' }: AssetsTabProps) {
|
|
|
369
369
|
)
|
|
370
370
|
})}
|
|
371
371
|
{customRules.map((rule, index) => (
|
|
372
|
-
<div key={`${rule.name}-${index}`} style={{ border: '1px solid var(--nf-accent)', borderRadius: 6, padding: '
|
|
373
|
-
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
|
372
|
+
<div key={`${rule.name}-${index}`} style={{ border: '1px solid var(--nf-accent)', borderRadius: 'var(--nf-radius-6)', padding: 'var(--nf-space-6) var(--nf-space-10)', fontSize: 'var(--nf-fs-12)' }}>
|
|
373
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--nf-space-8)' }}>
|
|
374
374
|
<span>
|
|
375
375
|
<b>{rule.name}</b>{' '}
|
|
376
376
|
<span className={css.badge} style={{ borderColor: 'var(--nf-accent)', color: 'var(--nf-accent)' }}>自定义</span>
|
|
377
377
|
</span>
|
|
378
|
-
<span style={{ display: 'flex', gap: 6 }}>
|
|
378
|
+
<span style={{ display: 'flex', gap: 'var(--nf-space-6)' }}>
|
|
379
379
|
<button className={`${css.button} ${css.buttonSmall}`} disabled={busy} onClick={() => { setEditingRule({ index, name: rule.name, avoid: rule.avoid, fix: rule.fix ?? '' }) }}>
|
|
380
380
|
编辑
|
|
381
381
|
</button>
|
|
@@ -385,7 +385,7 @@ export function AssetsTab({ api, initialTab = 'genre' }: AssetsTabProps) {
|
|
|
385
385
|
</span>
|
|
386
386
|
</div>
|
|
387
387
|
{editingRule !== null && editingRule.index === index ? (
|
|
388
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 6 }}>
|
|
388
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)', marginTop: 'var(--nf-space-6)' }}>
|
|
389
389
|
<input className={css.input} placeholder="规则名" value={editingRule.name} onChange={e => { setEditingRule({ ...editingRule, name: e.target.value }) }} />
|
|
390
390
|
<input className={css.input} placeholder="避免(要杜绝的表达)" value={editingRule.avoid} onChange={e => { setEditingRule({ ...editingRule, avoid: e.target.value }) }} />
|
|
391
391
|
<input className={css.input} placeholder="修正(改写方向,可留空)" value={editingRule.fix} onChange={e => { setEditingRule({ ...editingRule, fix: e.target.value }) }} />
|
|
@@ -426,7 +426,7 @@ export function AssetsTab({ api, initialTab = 'genre' }: AssetsTabProps) {
|
|
|
426
426
|
<span className={css.cardTitle}>心法</span>
|
|
427
427
|
<span className={css.meta}>粘贴一段你喜欢的样本文本,AI 提取叙事风格规则并绑定到本书,后续章节保持同一味道。</span>
|
|
428
428
|
{assets.styleAssets.map(style => (
|
|
429
|
-
<div key={style.name} style={{ border: '1px solid var(--nf-border)', borderRadius: 6, padding: '
|
|
429
|
+
<div key={style.name} style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-6)', padding: 'var(--nf-space-6) var(--nf-space-10)', fontSize: 'var(--nf-fs-12)' }}>
|
|
430
430
|
<b>{style.name}</b>
|
|
431
431
|
<div className={css.meta}>叙述:{style.proseRules.slice(0, 3).join(';')}</div>
|
|
432
432
|
{style.dialogueRules.length > 0 && <div className={css.meta}>台词:{style.dialogueRules.slice(0, 2).join(';')}</div>}
|
|
@@ -227,8 +227,8 @@ export function AssistantTab({ api }: AssistantTabProps) {
|
|
|
227
227
|
</button>
|
|
228
228
|
</div>
|
|
229
229
|
<span className={css.meta}>{tt('assistant.hint')}</span>
|
|
230
|
-
{notice !== '' && <span style={{ color: 'var(--nf-success)', fontSize: 12 }}>{notice}</span>}
|
|
231
|
-
{error !== '' && <span style={{ color: 'var(--nf-error)', fontSize: 12 }}>{tt('common.error')}: {error}</span>}
|
|
230
|
+
{notice !== '' && <span style={{ color: 'var(--nf-success)', fontSize: 'var(--nf-fs-12)' }}>{notice}</span>}
|
|
231
|
+
{error !== '' && <span style={{ color: 'var(--nf-error)', fontSize: 'var(--nf-fs-12)' }}>{tt('common.error')}: {error}</span>}
|
|
232
232
|
{/* 状态条:编辑老师正在做什么 */}
|
|
233
233
|
<div className={`${css.assistantStatus} ${busy ? css.assistantStatusBusy : ''}`}>
|
|
234
234
|
{busy
|
|
@@ -278,7 +278,7 @@ export function AssistantTab({ api }: AssistantTabProps) {
|
|
|
278
278
|
))}
|
|
279
279
|
{busy && <span className={css.meta} style={{ color: 'var(--nf-accent)' }}>…</span>}
|
|
280
280
|
</div>
|
|
281
|
-
<div className={css.row} style={{ marginTop: 8 }}>
|
|
281
|
+
<div className={css.row} style={{ marginTop: 'var(--nf-space-8)' }}>
|
|
282
282
|
<textarea
|
|
283
283
|
className={css.textarea}
|
|
284
284
|
style={{ minHeight: 64, flex: 1 }}
|
|
@@ -164,7 +164,7 @@ export function BookshelfBar({ api, shelf, onSwitch }: BookshelfBarProps) {
|
|
|
164
164
|
</div>
|
|
165
165
|
)}
|
|
166
166
|
</div>
|
|
167
|
-
{error !== '' && <span style={{ color: 'var(--nf-error)', fontSize: 12 }}>{error}</span>}
|
|
167
|
+
{error !== '' && <span style={{ color: 'var(--nf-error)', fontSize: 'var(--nf-fs-12)' }}>{error}</span>}
|
|
168
168
|
</div>
|
|
169
169
|
)
|
|
170
170
|
}
|
|
@@ -161,14 +161,14 @@ export function CreateBookView({
|
|
|
161
161
|
<span className={css.meta}>把一份大纲「编译」成一本完整的小说</span>
|
|
162
162
|
|
|
163
163
|
{error !== '' && (
|
|
164
|
-
<div className={css.card} style={{ borderColor: 'var(--nf-error)', padding: '
|
|
165
|
-
<span style={{ color: 'var(--nf-error)', fontSize: 12 }}>{error}</span>
|
|
164
|
+
<div className={css.card} style={{ borderColor: 'var(--nf-error)', padding: 'var(--nf-space-8) var(--nf-space-12)' }}>
|
|
165
|
+
<span style={{ color: 'var(--nf-error)', fontSize: 'var(--nf-fs-12)' }}>{error}</span>
|
|
166
166
|
</div>
|
|
167
167
|
)}
|
|
168
168
|
|
|
169
169
|
{fillNotice !== '' && (
|
|
170
|
-
<div className={css.card} style={{ borderColor: 'var(--nf-success)', padding: '
|
|
171
|
-
<span style={{ color: 'var(--nf-success)', fontSize: 12 }}>✅ {fillNotice}</span>
|
|
170
|
+
<div className={css.card} style={{ borderColor: 'var(--nf-success)', padding: 'var(--nf-space-8) var(--nf-space-12)' }}>
|
|
171
|
+
<span style={{ color: 'var(--nf-success)', fontSize: 'var(--nf-fs-12)' }}>✅ {fillNotice}</span>
|
|
172
172
|
</div>
|
|
173
173
|
)}
|
|
174
174
|
|
|
@@ -240,7 +240,7 @@ export function CreateBookView({
|
|
|
240
240
|
✨ {ideaOpen ? '▾' : '▸'} 没有大纲?用一句话想法让 AI 生成
|
|
241
241
|
</button>
|
|
242
242
|
{ideaOpen && (
|
|
243
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
243
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-8)' }}>
|
|
244
244
|
<textarea
|
|
245
245
|
className={css.textarea}
|
|
246
246
|
style={{ minHeight: 60 }}
|
|
@@ -268,7 +268,7 @@ export function CreateBookView({
|
|
|
268
268
|
|
|
269
269
|
{/* 方案卡 */}
|
|
270
270
|
{candidates.length > 0 && (
|
|
271
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
271
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-8)' }}>
|
|
272
272
|
{candidates.map(candidate => {
|
|
273
273
|
const isPinned = pinned.includes(candidate.id)
|
|
274
274
|
return (
|
|
@@ -277,13 +277,13 @@ export function CreateBookView({
|
|
|
277
277
|
className={css.ideaCandidate}
|
|
278
278
|
style={isPinned ? { borderColor: 'var(--nf-accent)', boxShadow: '0 0 0 2px var(--nf-accent-soft)' } : undefined}
|
|
279
279
|
>
|
|
280
|
-
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap', gap: 6 }}>
|
|
281
|
-
<span style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
|
280
|
+
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap', gap: 'var(--nf-space-6)' }}>
|
|
281
|
+
<span style={{ display: 'flex', alignItems: 'center', gap: 'var(--nf-space-8)', flexWrap: 'wrap' }}>
|
|
282
282
|
<b>《{candidate.bookName}》</b>
|
|
283
283
|
{candidate.genre !== '' && <span className={css.badge} style={{ borderColor: 'var(--nf-accent)', color: 'var(--nf-accent)' }}>{candidate.genre}</span>}
|
|
284
284
|
{isPinned && <span className={css.badge} style={{ borderColor: 'var(--nf-warn)', color: 'var(--nf-warn)' }}>★ 已暂留</span>}
|
|
285
285
|
</span>
|
|
286
|
-
<span style={{ display: 'flex', gap: 6 }}>
|
|
286
|
+
<span style={{ display: 'flex', gap: 'var(--nf-space-6)' }}>
|
|
287
287
|
<button
|
|
288
288
|
type="button"
|
|
289
289
|
className={`${css.button} ${css.buttonSmall} ${isPinned ? '' : css.buttonPrimary}`}
|
|
@@ -333,7 +333,7 @@ export function CreateBookView({
|
|
|
333
333
|
<button
|
|
334
334
|
type="button"
|
|
335
335
|
className={`${css.button} ${css.buttonPrimary}`}
|
|
336
|
-
style={{ width: '100%', padding: '
|
|
336
|
+
style={{ width: '100%', padding: 'var(--nf-space-10) 0', fontSize: 'var(--nf-fs-14)' }}
|
|
337
337
|
disabled={busy || effectiveName === ''}
|
|
338
338
|
onClick={() => { void handleCreate() }}
|
|
339
339
|
>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 漫剧工作台·全流程步骤条(唯一导航,方案X):
|
|
3
|
+
* ①创建方案 → ②视觉规则 → ③剧情骨架 → ④分镜表 → ⑤导入角色 → ⑥角色定妆 → ⑦视频提示词 → ⑧场景库 → ⑨导出使用。
|
|
4
|
+
* 每个步骤对应一个独立页面主体;完成度自动判定(读 project 现有字段)。
|
|
5
|
+
*/
|
|
6
|
+
import type { ProjectState } from '../../protocol.ts'
|
|
7
|
+
import css from './panel.module.css'
|
|
8
|
+
|
|
9
|
+
/** 步骤 → 页面主体的一一对应目标。 */
|
|
10
|
+
export type FlowTarget =
|
|
11
|
+
| 'plan' // ① 创建方案页
|
|
12
|
+
| 'rules' // ② 视觉规则页
|
|
13
|
+
| 'skeleton' // ③ 剧情骨架页(分镜·骨架)
|
|
14
|
+
| 'table' // ④ 分镜表页(分镜·分镜表)
|
|
15
|
+
| 'import' // ⑤ 导入角色页(角色·导入)
|
|
16
|
+
| 'makeup' // ⑥ 角色定妆页(角色·卡片)
|
|
17
|
+
| 'prompts' // ⑦ 视频提示词页(分镜·提示词)
|
|
18
|
+
| 'scenes' // ⑧ 场景库页
|
|
19
|
+
| 'export' // ⑨ 导出使用页
|
|
20
|
+
|
|
21
|
+
export interface FlowStep {
|
|
22
|
+
no: number
|
|
23
|
+
label: string
|
|
24
|
+
hint: string
|
|
25
|
+
done: boolean
|
|
26
|
+
target: FlowTarget
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 依据 project 数据计算 9 步完成度(与存储顺序无关,按生产顺序排列)。 */
|
|
30
|
+
export function computeFlowSteps(project: ProjectState | null): FlowStep[] {
|
|
31
|
+
const plans = project?.mangaPlans ?? []
|
|
32
|
+
const rules = project?.visualRules ?? []
|
|
33
|
+
const sbs = project?.storyboards ?? []
|
|
34
|
+
const manga = project?.mangaRoles ?? []
|
|
35
|
+
const scenes = project?.scenes ?? []
|
|
36
|
+
const hasSkeleton = sbs.some(e => e.skeleton !== undefined)
|
|
37
|
+
const hasTable = sbs.some(e => e.table !== undefined)
|
|
38
|
+
const hasPrompts = sbs.some(e => (e.prompts ?? []).length > 0)
|
|
39
|
+
return [
|
|
40
|
+
{ no: 1, label: '创建方案', hint: '选基底风格+可选滤镜并命名;此后所有提示词按此风格生成', done: plans.length > 0, target: 'plan' },
|
|
41
|
+
{ no: 2, label: '视觉规则', hint: '从道藏提炼 3-6 条视觉世界观规则,自动注入所有生图/生视频提示词', done: rules.length > 0, target: 'rules' },
|
|
42
|
+
{ no: 3, label: '剧情骨架', hint: '分镜①:本章弧线+节拍链+出场角色(characters 提名地基)', done: hasSkeleton, target: 'skeleton' },
|
|
43
|
+
{ no: 4, label: '分镜表', hint: '分镜②:骨架展开为镜头级画面(景别/机位/台词/每镜头角色)', done: hasTable, target: 'table' },
|
|
44
|
+
{ no: 5, label: '导入角色', hint: '从本集分镜提名→小说库匹配(规则+LLM 两段式)→导入为漫剧卡', done: manga.length > 0, target: 'import' },
|
|
45
|
+
{ no: 6, label: '角色定妆', hint: '生成形象锚点→精修提示词→上传/生成定妆图(status=已定妆)', done: manga.some(c => c.status === 'anchored'), target: 'makeup' },
|
|
46
|
+
{ no: 7, label: '视频提示词', hint: '分镜③:每镜头一段即梦可粘贴提示词,带风格词块与定妆绑定', done: hasPrompts, target: 'prompts' },
|
|
47
|
+
{ no: 8, label: '场景库', hint: '(选做)从正文提炼场景卡并采纳,分镜表会标注使用场景', done: scenes.length > 0, target: 'scenes' },
|
|
48
|
+
{ no: 9, label: '导出使用', hint: '复制提示词到即梦/豆包/ComfyUI 出图生视频;定妆卡按 mangaRoleIds 绑定参考图', done: hasPrompts, target: 'export' },
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function FlowGuide({
|
|
53
|
+
project,
|
|
54
|
+
onNavigate,
|
|
55
|
+
}: {
|
|
56
|
+
project: ProjectState | null
|
|
57
|
+
onNavigate: (target: FlowTarget) => void
|
|
58
|
+
}) {
|
|
59
|
+
const steps = computeFlowSteps(project)
|
|
60
|
+
const doneCount = steps.filter(s => s.done).length
|
|
61
|
+
return (
|
|
62
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)', marginTop: 'var(--nf-space-8)' }}>
|
|
63
|
+
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
64
|
+
<span className={css.meta}>🧭 全流程({doneCount}/{steps.length}):每步一个页面,完成自动打勾</span>
|
|
65
|
+
<span className={css.meta}>点击步骤打开对应页面</span>
|
|
66
|
+
</div>
|
|
67
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--nf-space-6)' }}>
|
|
68
|
+
{steps.map(s => (
|
|
69
|
+
<button
|
|
70
|
+
key={s.no}
|
|
71
|
+
type="button"
|
|
72
|
+
title={s.hint}
|
|
73
|
+
className={css.button + ' ' + css.buttonSmall + (s.done ? ' ' + css.buttonPrimary : '')}
|
|
74
|
+
style={{ minWidth: 0 }}
|
|
75
|
+
onClick={() => { onNavigate(s.target) }}
|
|
76
|
+
>
|
|
77
|
+
{s.done ? '✓ ' : s.no + '. '}{s.label}
|
|
78
|
+
</button>
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|