@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.
@@ -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('豆包生成「' + name + '」参考图…')
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('豆包生成「' + name + '」参考图失败:' + m, 'error')
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
- <button type='button' className={css.button + ' ' + css.buttonSmall} disabled={busy} onClick={() => { void handleImageGenerate(detailRole.name) }}>豆包生成</button>
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={() => {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * 生产单面板:区间批量生产(计划补足 → 逐章生成 → 被拒分级处理 → 断点续跑)。
3
- * 与「工作进度」悬浮窗不同:这是标准流水线设备,一键下单、实时进度、日志可查。
3
+ * 与「AI进度」悬浮窗不同:这是标准流水线设备,一键下单、实时进度、日志可查。
4
4
  */
5
5
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
6
6
  import type { NovelApi } from '../api.ts'
@@ -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.shelfHeader}>
161
+ <div className={css.shelfHero}>
168
162
  <div className={css.shelfTitleRow}>
169
- <h2 className={css.panelTitle} style={{ margin: 0 }}>📚 书架</h2>
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
- <div className={css.shelfFilters} style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
186
- {filters.map(f => (
187
- <button
188
- key={f.id}
189
- type="button"
190
- className={`${css.button} ${filter === f.id ? css.buttonPrimary : ''}`}
191
- style={{ fontSize: 14, flex: 1 }}
192
- onClick={() => { setFilter(f.id) }}
193
- >
194
- {f.label}{f.count > 0 ? `(${f.count})` : ''}
195
- </button>
196
- ))}
197
- </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
+ )}
198
191
  </div>
199
192
 
200
193
  {shelf.books.length === 0 ? (
201
194
  /* 空书架引导卡 */
202
195
  <div className={css.shelfEmpty}>
203
- <span className={css.shelfEmptyIcon}>📖</span>
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}>🔍</span>
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}>+</div>
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}>📥</div>
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])