@waterwx/dsh-novel-forge 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +35487 -745
- package/lib/client.js.map +1 -1
- package/lib/index.js +111 -12
- package/lib/index.js.map +1 -1
- package/lib/types/client/api.d.ts +1 -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 +1 -1
- package/lib/types/engine.d.ts +8 -1
- package/lib/types/index.d.ts +6 -4
- package/lib/types/protocol.d.ts +47 -3
- package/package.json +99 -98
- package/src/client/api.ts +4 -0
- package/src/client/panel/MangaWorkspace.tsx +7 -3
- package/src/client/panel/NovelPanel.tsx +389 -231
- package/src/client/panel/RoleVisualPanel.tsx +23 -6
- package/src/client/panel/RunPanel.tsx +1 -1
- package/src/client/panel/SceneLibrary.tsx +1 -1
- package/src/client/panel/ShelfView.tsx +28 -35
- package/src/client/panel/StoryboardTab.tsx +1 -1
- package/src/client/panel/panel.module.css +407 -23
- package/src/engine.ts +48 -8
- package/src/index.ts +37 -6
- package/src/protocol.ts +50 -3
- package/src/routes.ts +26 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useEffect, useRef, useState } from 'react'
|
|
4
4
|
import { createPortal } from 'react-dom'
|
|
5
5
|
import type { NovelApi } from '../api.ts'
|
|
6
|
-
import type { ProjectState } from '../../protocol.ts'
|
|
6
|
+
import type { ImageModelConfig, ProjectState } from '../../protocol.ts'
|
|
7
7
|
import css from './panel.module.css'
|
|
8
8
|
|
|
9
9
|
export function RoleVisualPanel({
|
|
@@ -13,6 +13,7 @@ export function RoleVisualPanel({
|
|
|
13
13
|
styleId,
|
|
14
14
|
filterId,
|
|
15
15
|
imageApiEnabled,
|
|
16
|
+
imageModels,
|
|
16
17
|
onGotoRoles,
|
|
17
18
|
onProgress,
|
|
18
19
|
}: {
|
|
@@ -22,8 +23,10 @@ export function RoleVisualPanel({
|
|
|
22
23
|
styleId?: string
|
|
23
24
|
filterId?: string
|
|
24
25
|
imageApiEnabled?: boolean
|
|
26
|
+
/** 生图模型库(AI 生图时可选择用哪条)。 */
|
|
27
|
+
imageModels?: ImageModelConfig[]
|
|
25
28
|
onGotoRoles?: () => void
|
|
26
|
-
/**
|
|
29
|
+
/** 上报到「AI进度」控制台(角色形象页 / 漫剧工作台共用)。 */
|
|
27
30
|
onProgress?: (text: string, kind?: 'info' | 'done' | 'error') => void
|
|
28
31
|
}) {
|
|
29
32
|
const [busy, setBusy] = useState(false)
|
|
@@ -33,6 +36,8 @@ export function RoleVisualPanel({
|
|
|
33
36
|
const [roleImageLabel, setRoleImageLabel] = useState('立绘')
|
|
34
37
|
const [detailRoleName, setDetailRoleName] = useState<string | null>(null)
|
|
35
38
|
const [detailUploadLabel, setDetailUploadLabel] = useState('立绘')
|
|
39
|
+
/** AI 生图时选择的模型条目 id('' = 用设置中启用的模型)。 */
|
|
40
|
+
const [imageModelId, setImageModelId] = useState('')
|
|
36
41
|
/** 提示词包全局中英切换(浮窗内所有提示词块共用)。 */
|
|
37
42
|
const [kitLang, setKitLang] = useState<'zh' | 'en'>('zh')
|
|
38
43
|
const inputRef = useRef<HTMLInputElement | null>(null)
|
|
@@ -113,16 +118,16 @@ export function RoleVisualPanel({
|
|
|
113
118
|
|
|
114
119
|
const handleImageGenerate = async (name: string): Promise<void> => {
|
|
115
120
|
setBusy(true); setError('')
|
|
116
|
-
report('
|
|
121
|
+
report('AI 生图「' + name + '」参考图…')
|
|
117
122
|
try {
|
|
118
|
-
await api.roles({ op: 'imageGenerate', name })
|
|
123
|
+
await api.roles({ op: 'imageGenerate', name, modelId: imageModelId !== '' ? imageModelId : undefined })
|
|
119
124
|
await refresh()
|
|
120
125
|
notify('已生成「' + name + '」参考图')
|
|
121
126
|
report('已生成「' + name + '」参考图', 'done')
|
|
122
127
|
} catch (err) {
|
|
123
128
|
const m = (err as Error).message
|
|
124
129
|
setError(m)
|
|
125
|
-
report('
|
|
130
|
+
report('AI 生图「' + name + '」参考图失败:' + m, 'error')
|
|
126
131
|
} finally { setBusy(false) }
|
|
127
132
|
}
|
|
128
133
|
|
|
@@ -434,7 +439,19 @@ export function RoleVisualPanel({
|
|
|
434
439
|
<button type='button' className={css.button + ' ' + css.buttonSmall} disabled={busy} onClick={() => { void handleVisual(detailRole.name) }}>✨ 生成锚点</button>
|
|
435
440
|
)}
|
|
436
441
|
{anchor !== undefined && imageApiEnabled === true && (
|
|
437
|
-
<
|
|
442
|
+
<span className={css.row} style={{ gap: 4 }}>
|
|
443
|
+
<select
|
|
444
|
+
className={css.input}
|
|
445
|
+
style={{ width: 150, fontSize: 11, padding: '3px 6px' }}
|
|
446
|
+
value={imageModelId}
|
|
447
|
+
onChange={e => { setImageModelId(e.target.value) }}
|
|
448
|
+
title="选择生图模型(缺省用设置中启用的模型)"
|
|
449
|
+
>
|
|
450
|
+
<option value="">默认(启用模型)</option>
|
|
451
|
+
{(imageModels ?? []).map(m => <option key={m.id} value={m.id}>{m.name !== '' ? m.name : (m.model !== '' ? m.model : m.id)}{m.enabled ? ' ✓' : ''}</option>)}
|
|
452
|
+
</select>
|
|
453
|
+
<button type='button' className={css.button + ' ' + css.buttonSmall} disabled={busy} onClick={() => { void handleImageGenerate(detailRole.name) }}>AI 生图</button>
|
|
454
|
+
</span>
|
|
438
455
|
)}
|
|
439
456
|
{anchor !== undefined && (
|
|
440
457
|
<button type='button' className={css.button + ' ' + css.buttonSmall} onClick={() => {
|
|
@@ -26,7 +26,7 @@ export function SceneLibrary({
|
|
|
26
26
|
styleId?: string
|
|
27
27
|
/** 可选滤镜风格 id。 */
|
|
28
28
|
filterId?: string
|
|
29
|
-
/**
|
|
29
|
+
/** 上报到「AI进度」控制台。 */
|
|
30
30
|
onProgress?: (text: string, kind?: 'info' | 'done' | 'error') => void
|
|
31
31
|
}) {
|
|
32
32
|
const [busy, setBusy] = useState(false)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* 进入小说工坊默认展示;点击书卡进入该书工作台,+ 进入开书向导页。
|
|
4
4
|
*/
|
|
5
5
|
import { useEffect, useMemo, useState } from 'react'
|
|
6
|
+
import { BookOpen, Download, Library, PenLine, Plus, Search } from 'lucide-react'
|
|
6
7
|
import type { NovelApi } from '../api.ts'
|
|
7
8
|
import type { BookshelfSnapshot } from '../../protocol.ts'
|
|
8
9
|
import css from './panel.module.css'
|
|
@@ -94,7 +95,7 @@ function BookCard({
|
|
|
94
95
|
onClick={onOpen}
|
|
95
96
|
title="进入创作工作台(大纲/章节/审稿/设定)"
|
|
96
97
|
>
|
|
97
|
-
|
|
98
|
+
<PenLine size={13} style={{ verticalAlign: -2 }} /> 进入工作台
|
|
98
99
|
</button>
|
|
99
100
|
<button
|
|
100
101
|
type="button"
|
|
@@ -103,7 +104,7 @@ function BookCard({
|
|
|
103
104
|
onClick={onRead}
|
|
104
105
|
title={book.hasProject && book.done > 0 ? '沉浸式阅读已写章节' : '尚无已写章节,先去工作台创作'}
|
|
105
106
|
>
|
|
106
|
-
|
|
107
|
+
<BookOpen size={13} style={{ verticalAlign: -2 }} /> 阅读
|
|
107
108
|
</button>
|
|
108
109
|
</div>
|
|
109
110
|
</div>
|
|
@@ -155,25 +156,11 @@ export function ShelfView({
|
|
|
155
156
|
})
|
|
156
157
|
}, [shelf.books, query, filter])
|
|
157
158
|
|
|
158
|
-
const filters: Array<{ id: 'all' | 'active' | 'done' | 'none'; label: string; count: number }> = [
|
|
159
|
-
{ id: 'all', label: '全部', count: shelf.books.length },
|
|
160
|
-
{ id: 'active', label: '进行中', count: stats.active },
|
|
161
|
-
{ id: 'done', label: '已完结', count: stats.done },
|
|
162
|
-
{ id: 'none', label: '未开书', count: stats.none },
|
|
163
|
-
]
|
|
164
|
-
|
|
165
159
|
return (
|
|
166
160
|
<div className={css.shelfView}>
|
|
167
|
-
<div className={css.
|
|
161
|
+
<div className={css.shelfHero}>
|
|
168
162
|
<div className={css.shelfTitleRow}>
|
|
169
|
-
<h2 className={css.panelTitle} style={{ margin: 0 }}
|
|
170
|
-
{shelf.books.length > 0 && (
|
|
171
|
-
<span className={css.meta}>
|
|
172
|
-
{shelf.books.length} 本书 · {stats.active} 进行中 · {stats.done} 已完结 · {stats.none} 未开书
|
|
173
|
-
</span>
|
|
174
|
-
)}
|
|
175
|
-
</div>
|
|
176
|
-
<div className={css.shelfToolbar}>
|
|
163
|
+
<h2 className={css.panelTitle} style={{ margin: 0 }}><Library size={20} style={{ verticalAlign: -3 }} /> AI小说工坊 · 书架</h2>
|
|
177
164
|
<input
|
|
178
165
|
className={`${css.input} ${css.shelfSearch}`}
|
|
179
166
|
type="search"
|
|
@@ -182,25 +169,31 @@ export function ShelfView({
|
|
|
182
169
|
onChange={e => { setQuery(e.target.value) }}
|
|
183
170
|
/>
|
|
184
171
|
</div>
|
|
185
|
-
|
|
186
|
-
{
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
+
)}
|
|
198
191
|
</div>
|
|
199
192
|
|
|
200
193
|
{shelf.books.length === 0 ? (
|
|
201
194
|
/* 空书架引导卡 */
|
|
202
195
|
<div className={css.shelfEmpty}>
|
|
203
|
-
<span className={css.shelfEmptyIcon}
|
|
196
|
+
<span className={css.shelfEmptyIcon}><BookOpen size={34} /></span>
|
|
204
197
|
<span className={css.shelfEmptyTitle}>你的创作从这里开始</span>
|
|
205
198
|
<span className={css.meta}>开一本书:粘贴大纲或导入 docx,书名自动识别,开书即建项目</span>
|
|
206
199
|
<button type="button" className={`${css.button} ${css.buttonPrimary}`} onClick={onAddBook}>
|
|
@@ -212,7 +205,7 @@ export function ShelfView({
|
|
|
212
205
|
</div>
|
|
213
206
|
) : visible.length === 0 ? (
|
|
214
207
|
<div className={css.shelfEmpty} style={{ minHeight: 160 }}>
|
|
215
|
-
<span className={css.shelfEmptyIcon}
|
|
208
|
+
<span className={css.shelfEmptyIcon}><Search size={34} /></span>
|
|
216
209
|
<span className={css.shelfEmptyTitle}>没有符合条件的书</span>
|
|
217
210
|
<span className={css.meta}>换个关键词或筛选条件试试</span>
|
|
218
211
|
</div>
|
|
@@ -231,14 +224,14 @@ export function ShelfView({
|
|
|
231
224
|
|
|
232
225
|
{/* 开书入口:进入独立向导页 */}
|
|
233
226
|
<div className={`${css.bookCard} ${css.bookAddCard}`} onClick={onAddBook}>
|
|
234
|
-
<div className={css.bookAddIcon}
|
|
227
|
+
<div className={css.bookAddIcon}><Plus size={28} /></div>
|
|
235
228
|
<span>开一本新书</span>
|
|
236
229
|
<span className={css.meta}>书名 + 大纲,开书即建项目</span>
|
|
237
230
|
</div>
|
|
238
231
|
|
|
239
232
|
{/* 导入入口:已有项目目录 / txt/md 全本 */}
|
|
240
233
|
<div className={`${css.bookCard} ${css.bookAddCard}`} onClick={onImportBook}>
|
|
241
|
-
<div className={css.bookAddIcon}
|
|
234
|
+
<div className={css.bookAddIcon}><Download size={28} /></div>
|
|
242
235
|
<span>导入已有小说</span>
|
|
243
236
|
<span className={css.meta}>项目目录或 txt/md 全本</span>
|
|
244
237
|
</div>
|
|
@@ -25,7 +25,7 @@ export function StoryboardTab({
|
|
|
25
25
|
styleId?: string
|
|
26
26
|
/** 可选滤镜风格 id。 */
|
|
27
27
|
filterId?: string
|
|
28
|
-
/**
|
|
28
|
+
/** 上报到「AI进度」控制台(分镜三步生成)。 */
|
|
29
29
|
onProgress?: (text: string, kind?: 'info' | 'done' | 'error') => void
|
|
30
30
|
}) {
|
|
31
31
|
const written = useMemo(() => chapters.filter(c => c.status !== 'pending' && c.status !== 'generating' && c.status !== 'error').sort((a, b) => a.no - b.no), [chapters])
|