@waterwx/dsh-novel-forge 1.3.1 → 1.3.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.
- package/README.md +288 -279
- package/lib/client.js +56309 -12221
- package/lib/index.js +14239 -5595
- package/lib/types/action-guard.d.ts +61 -0
- package/lib/types/ai-scan.d.ts +36 -0
- package/lib/types/assets.d.ts +24 -9
- package/lib/types/assistant.d.ts +29 -0
- package/lib/types/author-assets.d.ts +25 -0
- package/lib/types/bookshelf.d.ts +11 -1
- package/lib/types/client/api.d.ts +186 -10
- package/lib/types/client/chip-prefs.d.ts +36 -0
- package/lib/types/client/docx.d.ts +2 -0
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/client/llmLive.d.ts +31 -0
- package/lib/types/client/locales.d.ts +28 -67
- package/lib/types/client/panel/AdaptModeView.d.ts +5 -0
- package/lib/types/client/panel/AiTag.d.ts +1 -0
- package/lib/types/client/panel/AssistantTab.d.ts +4 -1
- package/lib/types/client/panel/AuthorAssetsView.d.ts +4 -0
- package/lib/types/client/panel/AuthorHome.d.ts +34 -0
- package/lib/types/client/panel/BookAnalysisView.d.ts +4 -0
- package/lib/types/client/panel/CmdPalette.d.ts +12 -0
- package/lib/types/client/panel/CreateBookView.d.ts +5 -1
- package/lib/types/client/panel/DirectorView.d.ts +7 -0
- package/lib/types/client/panel/GlobalAssetLibraryView.d.ts +4 -0
- package/lib/types/client/panel/IdeaInspirationView.d.ts +6 -0
- package/lib/types/client/panel/ImportModal.d.ts +7 -0
- package/lib/types/client/panel/KnowledgeBaseView.d.ts +4 -0
- package/lib/types/client/panel/LiveFeedLog.d.ts +13 -0
- package/lib/types/client/panel/MarketRadarView.d.ts +5 -0
- package/lib/types/client/panel/ModelManager.d.ts +14 -0
- package/lib/types/client/panel/NovelPanel.d.ts +6 -1
- package/lib/types/client/panel/PageHeader.d.ts +18 -0
- package/lib/types/client/panel/ProgressConsole.d.ts +17 -0
- package/lib/types/client/panel/ProgressHomeView.d.ts +5 -0
- package/lib/types/client/panel/PromptSlotsView.d.ts +11 -0
- package/lib/types/client/panel/ReasoningSection.d.ts +10 -0
- package/lib/types/client/panel/SettingsView.d.ts +19 -0
- package/lib/types/client/panel/ShelfView.d.ts +12 -1
- package/lib/types/client/panel/SubPage.d.ts +27 -0
- package/lib/types/client/panel/TensionView.d.ts +18 -0
- package/lib/types/client/panel/TimelineView.d.ts +18 -0
- package/lib/types/client/panel/helpers.d.ts +5 -3
- package/lib/types/client/panel/toast.d.ts +2 -0
- package/lib/types/client/panel/views.d.ts +25 -7
- package/lib/types/client/run-status.d.ts +21 -0
- package/lib/types/client/text.d.ts +9 -0
- package/lib/types/docx-core.d.ts +16 -0
- package/lib/types/docx.d.ts +2 -3
- package/lib/types/engine.d.ts +186 -45
- package/lib/types/global-assets.d.ts +6 -0
- package/lib/types/index.d.ts +22 -8
- package/lib/types/llm-live.d.ts +75 -0
- package/lib/types/llm-retry.d.ts +38 -0
- package/lib/types/market-radar-scan.d.ts +19 -0
- package/lib/types/market-radar-sources.d.ts +31 -0
- package/lib/types/migrate-output.d.ts +25 -0
- package/lib/types/novel-context.d.ts +80 -0
- package/lib/types/prompt-slots.d.ts +57 -0
- package/lib/types/prompt-utils.d.ts +35 -0
- package/lib/types/prompting.d.ts +10 -0
- package/lib/types/protocol.d.ts +1362 -166
- package/lib/types/review-policy.d.ts +97 -0
- package/lib/types/revision.d.ts +76 -0
- package/lib/types/routes.d.ts +4 -0
- package/lib/types/run.d.ts +23 -1
- package/lib/types/snapshots.d.ts +46 -0
- package/lib/types/stage-contract.d.ts +60 -0
- package/lib/types/tension.d.ts +53 -0
- package/lib/types/timeline.d.ts +37 -0
- package/package.json +52 -28
- package/src/action-guard.ts +127 -0
- package/src/ai-scan.ts +179 -0
- package/src/assets.ts +1806 -520
- package/src/assistant.ts +1098 -770
- package/src/author-assets.ts +149 -0
- package/src/bookshelf.ts +187 -151
- package/src/client/api.ts +378 -21
- package/src/client/chip-prefs.ts +121 -0
- package/src/client/docx.ts +37 -69
- package/src/client/index.ts +3 -1
- package/src/client/llmLive.ts +111 -0
- package/src/client/locales.ts +28 -253
- package/src/client/mount.tsx +1 -1
- package/src/client/panel/AdaptModeView.tsx +535 -0
- package/src/client/panel/AiTag.tsx +7 -0
- package/src/client/panel/AssetsTab.tsx +448 -458
- package/src/client/panel/AssistantTab.tsx +355 -301
- package/src/client/panel/AuthorAssetsView.tsx +206 -0
- package/src/client/panel/AuthorHome.tsx +309 -0
- package/src/client/panel/BookAnalysisView.tsx +63 -0
- package/src/client/panel/CmdPalette.tsx +102 -0
- package/src/client/panel/CreateBookView.tsx +366 -348
- package/src/client/panel/DirectorView.tsx +200 -0
- package/src/client/panel/GlobalAssetLibraryView.tsx +168 -0
- package/src/client/panel/IdeaInspirationView.tsx +76 -0
- package/src/client/panel/ImportModal.tsx +303 -0
- package/src/client/panel/KnowledgeBaseView.tsx +125 -0
- package/src/client/panel/LiveFeedLog.tsx +204 -0
- package/src/client/panel/MarketRadarView.tsx +377 -0
- package/src/client/panel/ModelManager.tsx +205 -0
- package/src/client/panel/NovelPanel.tsx +3135 -2651
- package/src/client/panel/PageHeader.tsx +37 -0
- package/src/client/panel/ProgressConsole.tsx +63 -0
- package/src/client/panel/PromptSlotsView.tsx +97 -0
- package/src/client/panel/ReaderView.tsx +306 -306
- package/src/client/panel/ReasoningSection.tsx +31 -0
- package/src/client/panel/RunPanel.tsx +64 -32
- package/src/client/panel/SettingsView.tsx +354 -0
- package/src/client/panel/ShelfView.tsx +266 -236
- package/src/client/panel/SubPage.tsx +42 -0
- package/src/client/panel/TensionView.tsx +212 -0
- package/src/client/panel/TimelineView.tsx +208 -0
- package/src/client/panel/WorldTab.tsx +202 -202
- package/src/client/panel/helpers.ts +7 -5
- package/src/client/panel/panel.module.css +4481 -1655
- package/src/client/panel/toast.ts +51 -0
- package/src/client/panel/views.tsx +401 -314
- package/src/client/run-status.ts +171 -0
- package/src/client/sidebar-entry.ts +1 -1
- package/src/client/text.ts +28 -0
- package/src/docx-core.ts +53 -0
- package/src/docx.ts +44 -83
- package/src/engine.ts +4672 -2943
- package/src/global-assets.ts +59 -0
- package/src/index.ts +89 -33
- package/src/llm-live.ts +260 -0
- package/src/llm-retry.ts +77 -0
- package/src/market-radar-scan.ts +41 -0
- package/src/market-radar-sources.ts +173 -0
- package/src/migrate-output.ts +76 -0
- package/src/novel-context.ts +286 -0
- package/src/prompt-slots.ts +125 -0
- package/src/prompting.ts +26 -0
- package/src/protocol.ts +1388 -158
- package/src/review-policy.ts +161 -0
- package/src/revision.ts +283 -0
- package/src/routes.ts +1934 -434
- package/src/run.ts +118 -21
- package/src/snapshots.ts +174 -0
- package/src/stage-contract.ts +189 -0
- package/src/tension.ts +251 -0
- package/src/timeline.ts +159 -0
- package/lib/client.js.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/types/comic-presets.d.ts +0 -12
- package/scripts/assistant-frames.txt +0 -8
- package/scripts/diagnose-adapter.mts +0 -81
- package/scripts/diagnose-assistant.mjs +0 -58
- package/scripts/diagnose-bible.mjs +0 -72
- package/scripts/diagnose-harness.mts +0 -75
- package/scripts/novel-forge-restart.log +0 -10
- package/scripts/novel-forge-web.stderr.log +0 -2
- package/scripts/novel-forge-web.stdout.log +0 -1
- package/scripts/outline-sample.txt +0 -449
- package/scripts/probe.mjs +0 -6
- package/scripts/restart-web.ps1 +0 -88
- package/scripts/smoke.mts +0 -72
- package/scripts/upload-github.ps1 +0 -51
- package/src/client/panel/BookshelfBar.tsx +0 -170
- package/src/comic-presets.ts +0 -67
|
@@ -0,0 +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.authorPageBodyFlow}>
|
|
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: 'var(--nf-space-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: 'var(--nf-fs-14)' }}> {error}</div>}
|
|
108
|
+
|
|
109
|
+
{/* 傻瓜式指引:生成后置顶 */}
|
|
110
|
+
{result !== null && (
|
|
111
|
+
<div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-6)' }}>
|
|
112
|
+
<div style={{ fontSize: 'var(--nf-fs-14)', fontWeight: 600 }}> 这份建议怎么用(三步)</div>
|
|
113
|
+
<ol style={{ margin: 0, paddingLeft: 'var(--nf-space-18)', display: 'grid', gap: 'var(--nf-space-4)', fontSize: 'var(--nf-fs-14)' }}>
|
|
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: 'var(--nf-fs-16)', fontWeight: 600 }}>总体判断</div>
|
|
125
|
+
<div style={{ fontSize: 'var(--nf-fs-14)' }}>{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: 'var(--nf-fs-16)', fontWeight: 600 }}>下一阶段剧情节点</div>
|
|
131
|
+
<ol style={{ margin: 0, paddingLeft: 'var(--nf-space-18)', display: 'grid', gap: 'var(--nf-space-6)', fontSize: 'var(--nf-fs-14)' }}>
|
|
132
|
+
{(result.nextArc ?? []).map((s, i) => (
|
|
133
|
+
<li key={i} style={{ display: 'flex', gap: 'var(--nf-space-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: 'var(--nf-fs-16)', fontWeight: 600 }}>节奏板</div>
|
|
144
|
+
<div style={{ fontSize: 'var(--nf-fs-14)' }}>{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: 'var(--nf-fs-16)', fontWeight: 600 }}>风险提示</div>
|
|
151
|
+
<ul style={{ margin: 0, paddingLeft: 'var(--nf-space-18)', display: 'grid', gap: 'var(--nf-space-6)', fontSize: 'var(--nf-fs-14)' }}>
|
|
152
|
+
{(result.risks ?? []).map((s, i) => (
|
|
153
|
+
<li key={i} style={{ display: 'flex', gap: 'var(--nf-space-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: 'var(--nf-fs-16)', fontWeight: 600 }}>需要修复/再平衡</div>
|
|
164
|
+
<ul style={{ margin: 0, paddingLeft: 'var(--nf-space-18)', display: 'grid', gap: 'var(--nf-space-6)', fontSize: 'var(--nf-fs-14)' }}>
|
|
165
|
+
{(result.fixes ?? []).map((s, i) => (
|
|
166
|
+
<li key={i} style={{ display: 'flex', gap: 'var(--nf-space-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: 'var(--nf-fs-16)', 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: 'var(--nf-space-6)', fontSize: 'var(--nf-fs-14)' }}>
|
|
184
|
+
{todos.map(t => (
|
|
185
|
+
<li key={t.id} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
186
|
+
<label style={{ display: 'flex', alignItems: 'center', gap: 'var(--nf-space-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
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 全局写作资产库:只读浏览插件内置的公共资产(题材/反AI规则/风格模板/推进模式)。
|
|
3
|
+
* 内置库为固定内容;实际绑定到某本书在「创作资产」页。
|
|
4
|
+
*/
|
|
5
|
+
import { useEffect, useState } from 'react'
|
|
6
|
+
import { Info } from 'lucide-react'
|
|
7
|
+
import type { NovelApi } from '../api.ts'
|
|
8
|
+
import type { AssetsResponse, GenreNode, AntiAiRule, StyleTemplate, ProgressionMode, PlotBeatTemplate } from '../../protocol.ts'
|
|
9
|
+
import css from './panel.module.css'
|
|
10
|
+
import { SubPage } from './SubPage.tsx'
|
|
11
|
+
|
|
12
|
+
type Tab = 'genre' | 'antiAi' | 'style' | 'progression' | 'plotBeat'
|
|
13
|
+
|
|
14
|
+
export function GlobalAssetLibraryView({ api }: { api: NovelApi }) {
|
|
15
|
+
const [data, setData] = useState<AssetsResponse | null>(null);
|
|
16
|
+
const [error, setError] = useState('');
|
|
17
|
+
const [tab, setTab] = useState<Tab>('genre');
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
let cancelled = false
|
|
21
|
+
void (async () => {
|
|
22
|
+
try {
|
|
23
|
+
const res = await api.assets()
|
|
24
|
+
if (!cancelled) setData(res)
|
|
25
|
+
} catch (err) {
|
|
26
|
+
if (!cancelled) setError((err as Error).message)
|
|
27
|
+
}
|
|
28
|
+
})()
|
|
29
|
+
return () => { cancelled = true }
|
|
30
|
+
}, [api]);
|
|
31
|
+
|
|
32
|
+
const renderGenreDescendants = (children: GenreNode[], depth: number): JSX.Element[] =>
|
|
33
|
+
children.map(c => (
|
|
34
|
+
<div key={c.name} style={{ paddingLeft: depth * 12, marginTop: 2 }}>
|
|
35
|
+
<span className={css.meta}><b>{c.name}</b>:{c.description}</span>
|
|
36
|
+
{(c.children ?? []).length > 0 && renderGenreDescendants(c.children ?? [], depth + 1)}
|
|
37
|
+
</div>
|
|
38
|
+
));
|
|
39
|
+
|
|
40
|
+
const renderGenre = (nodes: GenreNode[]): JSX.Element[] =>
|
|
41
|
+
nodes.map(n => (
|
|
42
|
+
<div key={n.name} className={css.assetCard}>
|
|
43
|
+
<div className={css.assetCardName}>{n.name}</div>
|
|
44
|
+
<div className={css.meta}>{n.description}</div>
|
|
45
|
+
{(n.children ?? []).length > 0 && (
|
|
46
|
+
<div style={{ marginTop: 6 }}>{renderGenreDescendants(n.children ?? [], 0)}</div>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
));
|
|
50
|
+
|
|
51
|
+
const renderAntiAi = (rules: AntiAiRule[]): JSX.Element[] =>
|
|
52
|
+
rules.map(r => (
|
|
53
|
+
<div key={r.name} className={css.assetCard}>
|
|
54
|
+
<div className={css.assetCardTop}><span className={css.assetKind} style={{ color: 'var(--nf-warn)' }}>反AI</span></div>
|
|
55
|
+
<div className={css.assetCardName}>{r.name}</div>
|
|
56
|
+
<div className={css.assetCardContent}>避免:{r.avoid}{r.fix !== '' ? '\n修正:' + r.fix : ''}</div>
|
|
57
|
+
{(r.detectPatterns ?? []).length > 0 && <div className={css.meta}>命中模式:{(r.detectPatterns ?? []).join('、')}</div>}
|
|
58
|
+
</div>
|
|
59
|
+
));
|
|
60
|
+
|
|
61
|
+
const renderStyle = (templates: StyleTemplate[]): JSX.Element[] =>
|
|
62
|
+
templates.map(t => (
|
|
63
|
+
<div key={t.key} className={css.assetCard}>
|
|
64
|
+
<div className={css.assetCardTop}><span className={css.assetKind} style={{ color: 'var(--nf-info)' }}>{t.category}</span></div>
|
|
65
|
+
<div className={css.assetCardName}>{t.name}</div>
|
|
66
|
+
<div className={css.meta}>{t.description}</div>
|
|
67
|
+
<div className={css.assetCardContent}>
|
|
68
|
+
适用:{(t.applicableGenres ?? []).join('、')}
|
|
69
|
+
{t.proseRules.length > 0 ? '\n叙述:' + t.proseRules.join(';') : ''}
|
|
70
|
+
{t.dialogueRules.length > 0 ? '\n台词:' + t.dialogueRules.join(';') : ''}
|
|
71
|
+
{t.languageRules.length > 0 ? '\n语言:' + t.languageRules.join(';') : ''}
|
|
72
|
+
{t.rhythmRules.length > 0 ? '\n节奏:' + t.rhythmRules.join(';') : ''}
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
));
|
|
76
|
+
|
|
77
|
+
const renderProgression = (modes: ProgressionMode[]): JSX.Element[] =>
|
|
78
|
+
modes.map(m => (
|
|
79
|
+
<div key={m.name} className={css.assetCard}>
|
|
80
|
+
<div className={css.assetCardTop}><span className={css.assetKind} style={{ color: 'var(--nf-accent)' }}>{m.primary ? '主模式' : '辅助'}</span></div>
|
|
81
|
+
<div className={css.assetCardName}>{m.name}</div>
|
|
82
|
+
<div className={css.meta}>驱动:{m.driver}</div>
|
|
83
|
+
<div className={css.assetCardContent}>
|
|
84
|
+
期待:{m.readerExpectation}
|
|
85
|
+
{m.payoffs.length > 0 ? '\n兑现:' + m.payoffs.join(';') : ''}
|
|
86
|
+
{m.risks.length > 0 ? '\n风险:' + m.risks.join(';') : ''}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
));
|
|
90
|
+
|
|
91
|
+
const renderPlotBeat = (beats: PlotBeatTemplate[]): JSX.Element[] =>
|
|
92
|
+
beats.map(b => (
|
|
93
|
+
<div key={b.key} className={css.assetCard}>
|
|
94
|
+
<div className={css.assetCardTop}><span className={css.assetKind} style={{ color: 'var(--nf-accent)' }}>{b.category}</span></div>
|
|
95
|
+
<div className={css.assetCardName}>{b.name}</div>
|
|
96
|
+
<div className={css.meta}>{b.summary}</div>
|
|
97
|
+
<div className={css.assetCardContent}>
|
|
98
|
+
位置:{b.position}
|
|
99
|
+
{(b.preconditions ?? []).length > 0 ? '\n前置:' + b.preconditions.join(';') : ''}
|
|
100
|
+
{(b.payoffSource ?? []).length > 0 ? '\n爽点:' + b.payoffSource.join(';') : ''}
|
|
101
|
+
{(b.combos ?? []).length > 0 ? '\n组合:' + b.combos.join(';') : ''}
|
|
102
|
+
{(b.taboos ?? []).length > 0 ? '\n禁忌:' + b.taboos.join(';') : ''}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
));
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<SubPage
|
|
109
|
+
title="全局写作资产库"
|
|
110
|
+
meta="内置题材基底库 / 反AI规则库 / 风格模板 / 推进模式 / 剧情桥段库(跨书)——插件自带、只读。"
|
|
111
|
+
>
|
|
112
|
+
<div className={css.shelfToolbar}>
|
|
113
|
+
<button type="button" className={css.assetFilterChip + (tab === 'genre' ? ' ' + css.assetFilterChipActive : '')} onClick={() => setTab('genre')}>题材 {(data?.genreLibrary ?? []).length}</button>
|
|
114
|
+
<button type="button" className={css.assetFilterChip + (tab === 'antiAi' ? ' ' + css.assetFilterChipActive : '')} onClick={() => setTab('antiAi')}>反AI规则 {(data?.antiAiLibrary ?? []).length}</button>
|
|
115
|
+
<button type="button" className={css.assetFilterChip + (tab === 'style' ? ' ' + css.assetFilterChipActive : '')} onClick={() => setTab('style')}>风格模板 {(data?.styleTemplates ?? []).length}</button>
|
|
116
|
+
<button type="button" className={css.assetFilterChip + (tab === 'progression' ? ' ' + css.assetFilterChipActive : '')} onClick={() => setTab('progression')}>推进模式 {(data?.progressionLibrary ?? []).length}</button>
|
|
117
|
+
<button type="button" className={css.assetFilterChip + (tab === 'plotBeat' ? ' ' + css.assetFilterChipActive : '')} onClick={() => setTab('plotBeat')}>剧情桥段 {(data?.plotBeatLibrary ?? []).length}</button>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{error !== '' && <div className={css.noticeError}>{error}</div>}
|
|
121
|
+
|
|
122
|
+
{data === null ? (
|
|
123
|
+
<div className={css.shelfEmpty} style={{ minHeight: 160 }}>
|
|
124
|
+
<span className={css.shelfEmptyIcon}><Info size={30} /></span>
|
|
125
|
+
<span className={css.shelfEmptyTitle}>加载中…</span>
|
|
126
|
+
<span className={css.meta}>正在读取全局内置资产库。</span>
|
|
127
|
+
</div>
|
|
128
|
+
) : (
|
|
129
|
+
<div className={css.assetGroups}>
|
|
130
|
+
{tab === 'genre' && (
|
|
131
|
+
<div className={css.assetGroup}>
|
|
132
|
+
<div className={css.assetGroupHeader}>题材基底库 <span className={css.meta}>树形 / 只读</span></div>
|
|
133
|
+
<div className={css.assetGrid}>{renderGenre(data.genreLibrary)}</div>
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
{tab === 'antiAi' && (
|
|
137
|
+
<div className={css.assetGroup}>
|
|
138
|
+
<div className={css.assetGroupHeader}>反AI规则库 <span className={css.meta}>只读 / 可在书内复制为自定义</span></div>
|
|
139
|
+
<div className={css.assetGrid}>{renderAntiAi(data.antiAiLibrary)}</div>
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
{tab === 'style' && (
|
|
143
|
+
<div className={css.assetGroup}>
|
|
144
|
+
<div className={css.assetGroupHeader}>风格模板 <span className={css.meta}>只读 / 可在书内一键绑定</span></div>
|
|
145
|
+
<div className={css.assetGrid}>{renderStyle(data.styleTemplates)}</div>
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
{tab === 'progression' && (
|
|
149
|
+
<div className={css.assetGroup}>
|
|
150
|
+
<div className={css.assetGroupHeader}>推进模式 <span className={css.meta}>只读</span></div>
|
|
151
|
+
<div className={css.assetGrid}>{renderProgression(data.progressionLibrary)}</div>
|
|
152
|
+
</div>
|
|
153
|
+
)}
|
|
154
|
+
{tab === 'plotBeat' && (
|
|
155
|
+
<div className={css.assetGroup}>
|
|
156
|
+
<div className={css.assetGroupHeader}>剧情桥段库 <span className={css.meta}>只读 / 可收藏或导入作者库</span></div>
|
|
157
|
+
<div className={css.assetGrid}>{renderPlotBeat(data.plotBeatLibrary)}</div>
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
)}
|
|
162
|
+
|
|
163
|
+
<div className={css.meta} style={{ marginTop: 10 }}>
|
|
164
|
+
提示:内置库为插件自带、只读。想要在书里使用,请进入某本书的「创作资产」页绑定为项目资产,或在「作者资产库」里「导入默认资产」沉淀成可复用的个人条目。
|
|
165
|
+
</div>
|
|
166
|
+
</SubPage>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 创意灵感:一句话/题材 → 多方向开书灵感。
|
|
3
|
+
*/
|
|
4
|
+
import { useState } from 'react'
|
|
5
|
+
import type { NovelApi } from '../api.ts'
|
|
6
|
+
import type { IdeaInspirationResult } from '../../protocol.ts'
|
|
7
|
+
import css from './panel.module.css'
|
|
8
|
+
import { SubPage } from './SubPage.tsx'
|
|
9
|
+
|
|
10
|
+
export default function IdeaInspirationView({ api, onUseIdea }: { api: NovelApi; onUseIdea?: (idea: IdeaInspirationResult['ideas'][number]) => void }): JSX.Element {
|
|
11
|
+
const [idea, setIdea] = useState('')
|
|
12
|
+
const [count, setCount] = useState(5)
|
|
13
|
+
const [busy, setBusy] = useState(false)
|
|
14
|
+
const [error, setError] = useState('')
|
|
15
|
+
const [result, setResult] = useState<IdeaInspirationResult | null>(null)
|
|
16
|
+
|
|
17
|
+
const copyText = (it: IdeaInspirationResult['ideas'][number]): string =>
|
|
18
|
+
`《${it.title}》\n题材:${it.genre}\n视角:${it.pov}\n钩子:${it.hook}\n长期兑现:${it.payoff}`
|
|
19
|
+
|
|
20
|
+
const copy = async (it: IdeaInspirationResult['ideas'][number]): Promise<void> => {
|
|
21
|
+
try { await navigator.clipboard.writeText(copyText(it)); setError('') } catch (e) { setError('复制失败:' + (e as Error).message) }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const run = async (): Promise<void> => {
|
|
25
|
+
if (idea.trim().length < 2) { setError('请填写你的方向/一句话想法'); return }
|
|
26
|
+
setBusy(true); setError('')
|
|
27
|
+
try {
|
|
28
|
+
const r = await api.ideaInspiration(idea.trim(), count)
|
|
29
|
+
setResult(r.result)
|
|
30
|
+
} catch (e) { setError((e as Error).message) } finally { setBusy(false) }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<SubPage
|
|
35
|
+
title="创意灵感"
|
|
36
|
+
meta="输入一句话/题材方向 → 给出多个可开书的差异化创意(不照搬具体作品)。"
|
|
37
|
+
actions={(
|
|
38
|
+
<button type="button" className={`${css.button} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void run() }}>
|
|
39
|
+
{busy ? '生成中…' : '生成灵感'}
|
|
40
|
+
</button>
|
|
41
|
+
)}
|
|
42
|
+
>
|
|
43
|
+
<div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-10)' }}>
|
|
44
|
+
<span className={css.meta}>填一句话方向,指定数量,点生成灵感即可。</span>
|
|
45
|
+
<textarea className={css.input} style={{ minHeight: 120, resize: 'vertical' }} value={idea} onChange={e => setIdea(e.target.value)} placeholder="例:重生后我在城隍庙摆摊;反派每天都被打脸却越来越强…" />
|
|
46
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
|
47
|
+
<label style={{ fontSize: 12 }}>数量</label>
|
|
48
|
+
<select className={css.input} style={{ width: 'auto', padding: '4px 8px' }} value={count} onChange={e => setCount(Number(e.target.value))}>
|
|
49
|
+
{[3, 5, 8].map(n => <option key={n} value={n}>{n}</option>)}
|
|
50
|
+
</select>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
{error !== '' && <div style={{ color: 'var(--nf-error)', fontSize: 13 }}> {error}</div>}
|
|
55
|
+
|
|
56
|
+
{result !== null && (
|
|
57
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))', gap: 'var(--nf-space-10)' }}>
|
|
58
|
+
{(result.ideas ?? []).map((it, i) => (
|
|
59
|
+
<div key={i} className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-6)' }}>
|
|
60
|
+
<div style={{ fontSize: 15, fontWeight: 600 }}>{it.title}</div>
|
|
61
|
+
<div style={{ fontSize: 12, color: 'var(--nf-success)' }}>{it.genre} · {it.pov}</div>
|
|
62
|
+
<div style={{ fontSize: 13 }}>钩子:{it.hook}</div>
|
|
63
|
+
<div style={{ fontSize: 12, color: 'var(--nf-text-2)' }}>兑现:{it.payoff}</div>
|
|
64
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
|
65
|
+
<button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { void copy(it) }} title="复制给开书向导/其他用到">复制</button>
|
|
66
|
+
{onUseIdea !== undefined && (
|
|
67
|
+
<button type="button" className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`} onClick={() => onUseIdea(it)} title="带着这个灵感去开书(预填书名与想法)">以此方向开书</button>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
))}
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
</SubPage>
|
|
75
|
+
)
|
|
76
|
+
}
|