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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -161,10 +161,9 @@ export function AuthorHome({ api, shelf, onOpenBook, onReadBook, onAddBook, onIm
161
161
  <div className={css.navGroup} key={group.label}>
162
162
  <div className={css.navGroupLabel}>{group.label}</div>
163
163
  {group.items.filter(item => item.id !== 'adapt' || adaptEnabled).map(item => (
164
- <button key={item.id} type="button" role="tab" aria-selected={nav === item.id} data-active={nav === item.id ? '' : undefined} className={css.navTab} title={item.hint} onClick={() => { if (item.id === 'progress') { setProgressOpen(o => !o); return } setNav(item.id) }}>
164
+ <button key={item.id} type="button" role="tab" aria-selected={nav === item.id} data-active={nav === item.id ? '' : undefined} className={css.navTab} title={item.hint} onClick={() => { setNav(item.id) }}>
165
165
  <span className={css.navTabIcon}>{item.icon}</span>
166
166
  <span className={css.navTabLabel}>{item.label}</span>
167
- {item.id === 'progress' && busy === true && <span className={css.navTabDot} />}
168
167
  </button>
169
168
  ))}
170
169
  </div>
@@ -152,7 +152,7 @@ export function CreateBookView({
152
152
  if (detectedGenre !== '') {
153
153
  try {
154
154
  setCurrentBook(created.id)
155
- await api.patchAssets({ genre: { name: detectedGenre, description: '' } })
155
+ await api.patchAssets({ genre: { name: detectedGenre, description: '', children: [] } })
156
156
  } catch { /* ignore */ }
157
157
  }
158
158
  onCreated(created.id)
@@ -1,200 +1,200 @@
1
- /**
2
- * 自动导演:基于全书上下文,给出下一卷/阶段编排建议 + 修复再平衡。
3
- * 生成后提供「傻瓜式」后续:一条条采纳 → 加入剧情线 / 加入待办,一键复制。
4
- */
5
- import { useEffect, useState } from 'react'
6
- import type { NovelApi } from '../api.ts'
7
- import type { DirectorAdvice, DirectorTodo } from '../../protocol.ts'
8
- import css from './panel.module.css'
9
-
10
- export default function DirectorView({ api, todos, onTodosChange }: { api: NovelApi; todos: DirectorTodo[]; onTodosChange?: (todos: DirectorTodo[]) => void }): JSX.Element {
11
- const [focus, setFocus] = useState('')
12
- const [busy, setBusy] = useState(false)
13
- const [error, setError] = useState('')
14
- const [result, setResult] = useState<DirectorAdvice | null>(null)
15
- const [adopted, setAdopted] = useState<Set<string>>(new Set())
16
- const [todosBusy, setTodosBusy] = useState(false)
17
-
18
- const refreshTodos = async (): Promise<void> => {
19
- try {
20
- const r = await api.directorTodosList()
21
- onTodosChange?.(r.todos)
22
- } catch { /* ignore */ }
23
- }
24
-
25
- useEffect(() => { void refreshTodos() }, [])
26
-
27
- const markAdopted = (key: string): void => {
28
- setAdopted(prev => new Set(prev).add(key))
29
- }
30
-
31
- const run = async (): Promise<void> => {
32
- setBusy(true); setError('')
33
- try {
34
- const r = await api.director(focus.trim())
35
- setResult(r.result)
36
- void refreshTodos()
37
- } catch (e) { setError((e as Error).message) } finally { setBusy(false) }
38
- }
39
-
40
- const adoptArc = async (s: string): Promise<void> => {
41
- try {
42
- await api.plotlines({ op: 'add', line: { id: '', name: s.slice(0, 16), kind: 'branch', goal: s, progress: '', status: 'active', chapters: [], createdAt: '' } })
43
- markAdopted('arc:' + s)
44
- } catch (e) { setError('加入剧情线失败:' + ((e as Error).message)) }
45
- }
46
-
47
- const adoptTodo = async (s: string, source: 'risk' | 'fix'): Promise<void> => {
48
- try {
49
- const r = await api.directorTodosAdd(s, source)
50
- onTodosChange?.(r.todos)
51
- markAdopted('todo:' + source + ':' + s)
52
- } catch (e) { setError('加入待办失败:' + ((e as Error).message)) }
53
- }
54
-
55
- const toggleTodo = async (id: string): Promise<void> => {
56
- setTodosBusy(true)
57
- try {
58
- const r = await api.directorTodosToggle(id)
59
- onTodosChange?.(r.todos)
60
- } catch (e) { setError((e as Error).message) } finally { setTodosBusy(false) }
61
- }
62
-
63
- const removeTodo = async (id: string): Promise<void> => {
64
- setTodosBusy(true)
65
- try {
66
- const r = await api.directorTodosRemove(id)
67
- onTodosChange?.(r.todos)
68
- } catch (e) { setError((e as Error).message) } finally { setTodosBusy(false) }
69
- }
70
-
71
- const copyAll = async (): Promise<void> => {
72
- if (result === null) return
73
- const text = [
74
- '【自动导演】',
75
- `总体判断:${result.summary}`,
76
- '',
77
- '下一阶段节点:',
78
- ...(result.nextArc ?? []).map(s => `- ${s}`),
79
- '',
80
- `节奏板:${result.pacing}`,
81
- '',
82
- '风险提示:',
83
- ...(result.risks ?? []).map(s => `- ${s}`),
84
- '',
85
- '需要修复/再平衡:',
86
- ...(result.fixes ?? []).map(s => `- ${s}`),
87
- ].join('\n')
88
- try { await navigator.clipboard.writeText(text); setError('') } catch (e) { setError('复制失败:' + ((e as Error).message)) }
89
- }
90
-
91
- return (
92
- <div className={css.authorPageBody}>
93
- <div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-10)' }}>
94
- <span className={css.cardTitle}>🎬 自动导演</span>
95
- <span className={css.meta}>结合本书分卷/剧情线/伏笔/最近事实,给出下一阶段编排 + 修复再平衡。可什么都不填直接生成。</span>
96
- <textarea className={css.input} style={{ minHeight: 90, resize: 'vertical' }} value={focus} onChange={e => setFocus(e.target.value)} placeholder="可选:聚焦某个方向,如「主角成长节奏」「反派压迫感」「感情线处理」「下一卷转折」…" />
97
- <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
98
- <button type="button" className={`${css.button} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void run() }}>
99
- {busy ? '编排中…' : '生成编排建议'}
100
- </button>
101
- {result !== null && (
102
- <button type="button" className={css.button} onClick={() => { void copyAll() }}>📋 一键复制</button>
103
- )}
104
- </div>
105
- </div>
106
-
107
- {error !== '' && <div style={{ color: 'var(--nf-error)', fontSize: 13 }}>⚠ {error}</div>}
108
-
109
- {/* 傻瓜式指引:生成后置顶 */}
110
- {result !== null && (
111
- <div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-6)' }}>
112
- <div style={{ fontSize: 14, fontWeight: 600 }}>🧭 这份建议怎么用(三步)</div>
113
- <ol style={{ margin: 0, paddingLeft: 18, display: 'grid', gap: 4, fontSize: 13 }}>
114
- <li>先看「风险 / 修复」,点每条旁的 <b>加入待办</b>,记下来逐个处理。</li>
115
- <li>「下一阶段节点」每条可 <b>加入剧情线</b>,直接把下一步方向写进本书。</li>
116
- <li>「节奏板」对照你当前写到的地方,看该加快还是放慢;不确定就点 <b>一键复制</b> 保存。</li>
117
- </ol>
118
- </div>
119
- )}
120
-
121
- {result !== null && (
122
- <div style={{ display: 'grid', gap: 'var(--nf-space-10)' }}>
123
- <div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-6)' }}>
124
- <div style={{ fontSize: 15, fontWeight: 600 }}>总体判断</div>
125
- <div style={{ fontSize: 13 }}>{result.summary}</div>
126
- </div>
127
-
128
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: 'var(--nf-space-10)' }}>
129
- <div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-6)' }}>
130
- <div style={{ fontSize: 15, fontWeight: 600 }}>下一阶段剧情节点</div>
131
- <ol style={{ margin: 0, paddingLeft: 18, display: 'grid', gap: 6, fontSize: 13 }}>
132
- {(result.nextArc ?? []).map((s, i) => (
133
- <li key={i} style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
134
- <span style={{ flex: 1, minWidth: 160 }}>{s}</span>
135
- <button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={adopted.has('arc:' + s)} onClick={() => { void adoptArc(s) }}>
136
- {adopted.has('arc:' + s) ? '已加入' : '采纳 → 加入剧情线'}
137
- </button>
138
- </li>
139
- ))}
140
- </ol>
141
- </div>
142
- <div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-6)' }}>
143
- <div style={{ fontSize: 15, fontWeight: 600 }}>节奏板</div>
144
- <div style={{ fontSize: 13 }}>{result.pacing}</div>
145
- </div>
146
- </div>
147
-
148
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: 'var(--nf-space-10)' }}>
149
- <div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-6)' }}>
150
- <div style={{ fontSize: 15, fontWeight: 600 }}>风险提示</div>
151
- <ul style={{ margin: 0, paddingLeft: 18, display: 'grid', gap: 6, fontSize: 13 }}>
152
- {(result.risks ?? []).map((s, i) => (
153
- <li key={i} style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
154
- <span style={{ flex: 1, minWidth: 160 }}>{s}</span>
155
- <button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={adopted.has('todo:risk:' + s)} onClick={() => { void adoptTodo(s, 'risk') }}>
156
- {adopted.has('todo:risk:' + s) ? '已加入' : '采纳 → 加入待办'}
157
- </button>
158
- </li>
159
- ))}
160
- </ul>
161
- </div>
162
- <div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-6)' }}>
163
- <div style={{ fontSize: 15, fontWeight: 600 }}>需要修复/再平衡</div>
164
- <ul style={{ margin: 0, paddingLeft: 18, display: 'grid', gap: 6, fontSize: 13 }}>
165
- {(result.fixes ?? []).map((s, i) => (
166
- <li key={i} style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
167
- <span style={{ flex: 1, minWidth: 160 }}>{s}</span>
168
- <button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={adopted.has('todo:fix:' + s)} onClick={() => { void adoptTodo(s, 'fix') }}>
169
- {adopted.has('todo:fix:' + s) ? '已加入' : '采纳 → 加入待办'}
170
- </button>
171
- </li>
172
- ))}
173
- </ul>
174
- </div>
175
- </div>
176
-
177
- {/* 已采纳待办清单(傻瓜式:勾选处理,做完删掉) */}
178
- <div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-6)' }}>
179
- <div style={{ fontSize: 15, fontWeight: 600 }}>🎬 导演待办(已采纳)</div>
180
- {todos.length === 0 ? (
181
- <span className={css.meta}>还没有。点上面「加入待办」,把风险/修复记到这里,做完勾掉。这些也会出现在工作流主页的「待办队列」。</span>
182
- ) : (
183
- <ul style={{ margin: 0, paddingLeft: 0, listStyle: 'none', display: 'grid', gap: 6, fontSize: 13 }}>
184
- {todos.map(t => (
185
- <li key={t.id} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
186
- <label style={{ display: 'flex', alignItems: 'center', gap: 6, flex: 1, cursor: 'pointer' }}>
187
- <input type="checkbox" checked={t.done} disabled={todosBusy} onChange={() => { void toggleTodo(t.id) }} />
188
- <span style={{ textDecoration: t.done ? 'line-through' : 'none', color: t.done ? 'var(--nf-text-2)' : undefined }}>{t.text}</span>
189
- </label>
190
- <button type="button" className={`${css.button} ${css.buttonSmall}`} title="删除" onClick={() => { void removeTodo(t.id) }}>×</button>
191
- </li>
192
- ))}
193
- </ul>
194
- )}
195
- </div>
196
- </div>
197
- )}
198
- </div>
199
- )
200
- }
1
+ /**
2
+ * 自动编辑:基于全书上下文,给出下一卷/阶段编排建议 + 修复再平衡。
3
+ * 生成后提供「傻瓜式」后续:一条条采纳 → 加入剧情线 / 加入待办,一键复制。
4
+ */
5
+ import { useEffect, useState } from 'react'
6
+ import type { NovelApi } from '../api.ts'
7
+ import type { DirectorAdvice, DirectorTodo } from '../../protocol.ts'
8
+ import css from './panel.module.css'
9
+
10
+ export default function DirectorView({ api, todos, onTodosChange }: { api: NovelApi; todos: DirectorTodo[]; onTodosChange?: (todos: DirectorTodo[]) => void }): JSX.Element {
11
+ const [focus, setFocus] = useState('')
12
+ const [busy, setBusy] = useState(false)
13
+ const [error, setError] = useState('')
14
+ const [result, setResult] = useState<DirectorAdvice | null>(null)
15
+ const [adopted, setAdopted] = useState<Set<string>>(new Set())
16
+ const [todosBusy, setTodosBusy] = useState(false)
17
+
18
+ const refreshTodos = async (): Promise<void> => {
19
+ try {
20
+ const r = await api.directorTodosList()
21
+ onTodosChange?.(r.todos)
22
+ } catch { /* ignore */ }
23
+ }
24
+
25
+ useEffect(() => { void refreshTodos() }, [])
26
+
27
+ const markAdopted = (key: string): void => {
28
+ setAdopted(prev => new Set(prev).add(key))
29
+ }
30
+
31
+ const run = async (): Promise<void> => {
32
+ setBusy(true); setError('')
33
+ try {
34
+ const r = await api.director(focus.trim())
35
+ setResult(r.result)
36
+ void refreshTodos()
37
+ } catch (e) { setError((e as Error).message) } finally { setBusy(false) }
38
+ }
39
+
40
+ const adoptArc = async (s: string): Promise<void> => {
41
+ try {
42
+ await api.plotlines({ op: 'add', line: { id: '', name: s.slice(0, 16), kind: 'branch', goal: s, progress: '', status: 'active', chapters: [], createdAt: '' } })
43
+ markAdopted('arc:' + s)
44
+ } catch (e) { setError('加入剧情线失败:' + ((e as Error).message)) }
45
+ }
46
+
47
+ const adoptTodo = async (s: string, source: 'risk' | 'fix'): Promise<void> => {
48
+ try {
49
+ const r = await api.directorTodosAdd(s, source)
50
+ onTodosChange?.(r.todos)
51
+ markAdopted('todo:' + source + ':' + s)
52
+ } catch (e) { setError('加入待办失败:' + ((e as Error).message)) }
53
+ }
54
+
55
+ const toggleTodo = async (id: string): Promise<void> => {
56
+ setTodosBusy(true)
57
+ try {
58
+ const r = await api.directorTodosToggle(id)
59
+ onTodosChange?.(r.todos)
60
+ } catch (e) { setError((e as Error).message) } finally { setTodosBusy(false) }
61
+ }
62
+
63
+ const removeTodo = async (id: string): Promise<void> => {
64
+ setTodosBusy(true)
65
+ try {
66
+ const r = await api.directorTodosRemove(id)
67
+ onTodosChange?.(r.todos)
68
+ } catch (e) { setError((e as Error).message) } finally { setTodosBusy(false) }
69
+ }
70
+
71
+ const copyAll = async (): Promise<void> => {
72
+ if (result === null) return
73
+ const text = [
74
+ '【自动编辑】',
75
+ `总体判断:${result.summary}`,
76
+ '',
77
+ '下一阶段节点:',
78
+ ...(result.nextArc ?? []).map(s => `- ${s}`),
79
+ '',
80
+ `节奏板:${result.pacing}`,
81
+ '',
82
+ '风险提示:',
83
+ ...(result.risks ?? []).map(s => `- ${s}`),
84
+ '',
85
+ '需要修复/再平衡:',
86
+ ...(result.fixes ?? []).map(s => `- ${s}`),
87
+ ].join('\n')
88
+ try { await navigator.clipboard.writeText(text); setError('') } catch (e) { setError('复制失败:' + ((e as Error).message)) }
89
+ }
90
+
91
+ return (
92
+ <div className={css.authorPageBody}>
93
+ <div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-10)' }}>
94
+ <span className={css.cardTitle}>🎬 自动编辑</span>
95
+ <span className={css.meta}>结合本书分卷/剧情线/伏笔/最近事实,给出下一阶段编排 + 修复再平衡。可什么都不填直接生成。</span>
96
+ <textarea className={css.input} style={{ minHeight: 90, resize: 'vertical' }} value={focus} onChange={e => setFocus(e.target.value)} placeholder="可选:聚焦某个方向,如「主角成长节奏」「反派压迫感」「感情线处理」「下一卷转折」…" />
97
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
98
+ <button type="button" className={`${css.button} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void run() }}>
99
+ {busy ? '编排中…' : '生成编排建议'}
100
+ </button>
101
+ {result !== null && (
102
+ <button type="button" className={css.button} onClick={() => { void copyAll() }}>📋 一键复制</button>
103
+ )}
104
+ </div>
105
+ </div>
106
+
107
+ {error !== '' && <div style={{ color: 'var(--nf-error)', fontSize: 13 }}>⚠ {error}</div>}
108
+
109
+ {/* 傻瓜式指引:生成后置顶 */}
110
+ {result !== null && (
111
+ <div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-6)' }}>
112
+ <div style={{ fontSize: 14, fontWeight: 600 }}>🧭 这份建议怎么用(三步)</div>
113
+ <ol style={{ margin: 0, paddingLeft: 18, display: 'grid', gap: 4, fontSize: 13 }}>
114
+ <li>先看「风险 / 修复」,点每条旁的 <b>加入待办</b>,记下来逐个处理。</li>
115
+ <li>「下一阶段节点」每条可 <b>加入剧情线</b>,直接把下一步方向写进本书。</li>
116
+ <li>「节奏板」对照你当前写到的地方,看该加快还是放慢;不确定就点 <b>一键复制</b> 保存。</li>
117
+ </ol>
118
+ </div>
119
+ )}
120
+
121
+ {result !== null && (
122
+ <div style={{ display: 'grid', gap: 'var(--nf-space-10)' }}>
123
+ <div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-6)' }}>
124
+ <div style={{ fontSize: 15, fontWeight: 600 }}>总体判断</div>
125
+ <div style={{ fontSize: 13 }}>{result.summary}</div>
126
+ </div>
127
+
128
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: 'var(--nf-space-10)' }}>
129
+ <div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-6)' }}>
130
+ <div style={{ fontSize: 15, fontWeight: 600 }}>下一阶段剧情节点</div>
131
+ <ol style={{ margin: 0, paddingLeft: 18, display: 'grid', gap: 6, fontSize: 13 }}>
132
+ {(result.nextArc ?? []).map((s, i) => (
133
+ <li key={i} style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
134
+ <span style={{ flex: 1, minWidth: 160 }}>{s}</span>
135
+ <button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={adopted.has('arc:' + s)} onClick={() => { void adoptArc(s) }}>
136
+ {adopted.has('arc:' + s) ? '已加入' : '采纳 → 加入剧情线'}
137
+ </button>
138
+ </li>
139
+ ))}
140
+ </ol>
141
+ </div>
142
+ <div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-6)' }}>
143
+ <div style={{ fontSize: 15, fontWeight: 600 }}>节奏板</div>
144
+ <div style={{ fontSize: 13 }}>{result.pacing}</div>
145
+ </div>
146
+ </div>
147
+
148
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: 'var(--nf-space-10)' }}>
149
+ <div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-6)' }}>
150
+ <div style={{ fontSize: 15, fontWeight: 600 }}>风险提示</div>
151
+ <ul style={{ margin: 0, paddingLeft: 18, display: 'grid', gap: 6, fontSize: 13 }}>
152
+ {(result.risks ?? []).map((s, i) => (
153
+ <li key={i} style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
154
+ <span style={{ flex: 1, minWidth: 160 }}>{s}</span>
155
+ <button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={adopted.has('todo:risk:' + s)} onClick={() => { void adoptTodo(s, 'risk') }}>
156
+ {adopted.has('todo:risk:' + s) ? '已加入' : '采纳 → 加入待办'}
157
+ </button>
158
+ </li>
159
+ ))}
160
+ </ul>
161
+ </div>
162
+ <div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-6)' }}>
163
+ <div style={{ fontSize: 15, fontWeight: 600 }}>需要修复/再平衡</div>
164
+ <ul style={{ margin: 0, paddingLeft: 18, display: 'grid', gap: 6, fontSize: 13 }}>
165
+ {(result.fixes ?? []).map((s, i) => (
166
+ <li key={i} style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
167
+ <span style={{ flex: 1, minWidth: 160 }}>{s}</span>
168
+ <button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={adopted.has('todo:fix:' + s)} onClick={() => { void adoptTodo(s, 'fix') }}>
169
+ {adopted.has('todo:fix:' + s) ? '已加入' : '采纳 → 加入待办'}
170
+ </button>
171
+ </li>
172
+ ))}
173
+ </ul>
174
+ </div>
175
+ </div>
176
+
177
+ {/* 已采纳待办清单(傻瓜式:勾选处理,做完删掉) */}
178
+ <div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-6)' }}>
179
+ <div style={{ fontSize: 15, fontWeight: 600 }}>🎬 编辑待办(已采纳)</div>
180
+ {todos.length === 0 ? (
181
+ <span className={css.meta}>还没有。点上面「加入待办」,把风险/修复记到这里,做完勾掉。这些也会出现在工作流主页的「待办队列」。</span>
182
+ ) : (
183
+ <ul style={{ margin: 0, paddingLeft: 0, listStyle: 'none', display: 'grid', gap: 6, fontSize: 13 }}>
184
+ {todos.map(t => (
185
+ <li key={t.id} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
186
+ <label style={{ display: 'flex', alignItems: 'center', gap: 6, flex: 1, cursor: 'pointer' }}>
187
+ <input type="checkbox" checked={t.done} disabled={todosBusy} onChange={() => { void toggleTodo(t.id) }} />
188
+ <span style={{ textDecoration: t.done ? 'line-through' : 'none', color: t.done ? 'var(--nf-text-2)' : undefined }}>{t.text}</span>
189
+ </label>
190
+ <button type="button" className={`${css.button} ${css.buttonSmall}`} title="删除" onClick={() => { void removeTodo(t.id) }}>×</button>
191
+ </li>
192
+ ))}
193
+ </ul>
194
+ )}
195
+ </div>
196
+ </div>
197
+ )}
198
+ </div>
199
+ )
200
+ }
@@ -146,7 +146,7 @@ export default function MarketRadarView({ api, bookId }: { api: NovelApi; bookId
146
146
  setIdeaBusy(true)
147
147
  setError('')
148
148
  try {
149
- const r = await api.marketIdeaInspiration({ signals, foundation: result.productionFoundation, brief, count: 5 })
149
+ const r = await api.marketIdeaInspiration({ signals, foundation: result.productionFoundation, brief: brief ?? undefined, count: 5 })
150
150
  setIdeas(r.result.ideas)
151
151
  } catch (err) {
152
152
  setError((err as Error).message)