@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.
Files changed (161) hide show
  1. package/README.md +288 -279
  2. package/lib/client.js +56309 -12221
  3. package/lib/index.js +14239 -5595
  4. package/lib/types/action-guard.d.ts +61 -0
  5. package/lib/types/ai-scan.d.ts +36 -0
  6. package/lib/types/assets.d.ts +24 -9
  7. package/lib/types/assistant.d.ts +29 -0
  8. package/lib/types/author-assets.d.ts +25 -0
  9. package/lib/types/bookshelf.d.ts +11 -1
  10. package/lib/types/client/api.d.ts +186 -10
  11. package/lib/types/client/chip-prefs.d.ts +36 -0
  12. package/lib/types/client/docx.d.ts +2 -0
  13. package/lib/types/client/index.d.ts +1 -1
  14. package/lib/types/client/llmLive.d.ts +31 -0
  15. package/lib/types/client/locales.d.ts +28 -67
  16. package/lib/types/client/panel/AdaptModeView.d.ts +5 -0
  17. package/lib/types/client/panel/AiTag.d.ts +1 -0
  18. package/lib/types/client/panel/AssistantTab.d.ts +4 -1
  19. package/lib/types/client/panel/AuthorAssetsView.d.ts +4 -0
  20. package/lib/types/client/panel/AuthorHome.d.ts +34 -0
  21. package/lib/types/client/panel/BookAnalysisView.d.ts +4 -0
  22. package/lib/types/client/panel/CmdPalette.d.ts +12 -0
  23. package/lib/types/client/panel/CreateBookView.d.ts +5 -1
  24. package/lib/types/client/panel/DirectorView.d.ts +7 -0
  25. package/lib/types/client/panel/GlobalAssetLibraryView.d.ts +4 -0
  26. package/lib/types/client/panel/IdeaInspirationView.d.ts +6 -0
  27. package/lib/types/client/panel/ImportModal.d.ts +7 -0
  28. package/lib/types/client/panel/KnowledgeBaseView.d.ts +4 -0
  29. package/lib/types/client/panel/LiveFeedLog.d.ts +13 -0
  30. package/lib/types/client/panel/MarketRadarView.d.ts +5 -0
  31. package/lib/types/client/panel/ModelManager.d.ts +14 -0
  32. package/lib/types/client/panel/NovelPanel.d.ts +6 -1
  33. package/lib/types/client/panel/PageHeader.d.ts +18 -0
  34. package/lib/types/client/panel/ProgressConsole.d.ts +17 -0
  35. package/lib/types/client/panel/ProgressHomeView.d.ts +5 -0
  36. package/lib/types/client/panel/PromptSlotsView.d.ts +11 -0
  37. package/lib/types/client/panel/ReasoningSection.d.ts +10 -0
  38. package/lib/types/client/panel/SettingsView.d.ts +19 -0
  39. package/lib/types/client/panel/ShelfView.d.ts +12 -1
  40. package/lib/types/client/panel/SubPage.d.ts +27 -0
  41. package/lib/types/client/panel/TensionView.d.ts +18 -0
  42. package/lib/types/client/panel/TimelineView.d.ts +18 -0
  43. package/lib/types/client/panel/helpers.d.ts +5 -3
  44. package/lib/types/client/panel/toast.d.ts +2 -0
  45. package/lib/types/client/panel/views.d.ts +25 -7
  46. package/lib/types/client/run-status.d.ts +21 -0
  47. package/lib/types/client/text.d.ts +9 -0
  48. package/lib/types/docx-core.d.ts +16 -0
  49. package/lib/types/docx.d.ts +2 -3
  50. package/lib/types/engine.d.ts +186 -45
  51. package/lib/types/global-assets.d.ts +6 -0
  52. package/lib/types/index.d.ts +22 -8
  53. package/lib/types/llm-live.d.ts +75 -0
  54. package/lib/types/llm-retry.d.ts +38 -0
  55. package/lib/types/market-radar-scan.d.ts +19 -0
  56. package/lib/types/market-radar-sources.d.ts +31 -0
  57. package/lib/types/migrate-output.d.ts +25 -0
  58. package/lib/types/novel-context.d.ts +80 -0
  59. package/lib/types/prompt-slots.d.ts +57 -0
  60. package/lib/types/prompt-utils.d.ts +35 -0
  61. package/lib/types/prompting.d.ts +10 -0
  62. package/lib/types/protocol.d.ts +1362 -166
  63. package/lib/types/review-policy.d.ts +97 -0
  64. package/lib/types/revision.d.ts +76 -0
  65. package/lib/types/routes.d.ts +4 -0
  66. package/lib/types/run.d.ts +23 -1
  67. package/lib/types/snapshots.d.ts +46 -0
  68. package/lib/types/stage-contract.d.ts +60 -0
  69. package/lib/types/tension.d.ts +53 -0
  70. package/lib/types/timeline.d.ts +37 -0
  71. package/package.json +52 -28
  72. package/src/action-guard.ts +127 -0
  73. package/src/ai-scan.ts +179 -0
  74. package/src/assets.ts +1806 -520
  75. package/src/assistant.ts +1098 -770
  76. package/src/author-assets.ts +149 -0
  77. package/src/bookshelf.ts +187 -151
  78. package/src/client/api.ts +378 -21
  79. package/src/client/chip-prefs.ts +121 -0
  80. package/src/client/docx.ts +37 -69
  81. package/src/client/index.ts +3 -1
  82. package/src/client/llmLive.ts +111 -0
  83. package/src/client/locales.ts +28 -253
  84. package/src/client/mount.tsx +1 -1
  85. package/src/client/panel/AdaptModeView.tsx +535 -0
  86. package/src/client/panel/AiTag.tsx +7 -0
  87. package/src/client/panel/AssetsTab.tsx +448 -458
  88. package/src/client/panel/AssistantTab.tsx +355 -301
  89. package/src/client/panel/AuthorAssetsView.tsx +206 -0
  90. package/src/client/panel/AuthorHome.tsx +309 -0
  91. package/src/client/panel/BookAnalysisView.tsx +63 -0
  92. package/src/client/panel/CmdPalette.tsx +102 -0
  93. package/src/client/panel/CreateBookView.tsx +366 -348
  94. package/src/client/panel/DirectorView.tsx +200 -0
  95. package/src/client/panel/GlobalAssetLibraryView.tsx +168 -0
  96. package/src/client/panel/IdeaInspirationView.tsx +76 -0
  97. package/src/client/panel/ImportModal.tsx +303 -0
  98. package/src/client/panel/KnowledgeBaseView.tsx +125 -0
  99. package/src/client/panel/LiveFeedLog.tsx +204 -0
  100. package/src/client/panel/MarketRadarView.tsx +377 -0
  101. package/src/client/panel/ModelManager.tsx +205 -0
  102. package/src/client/panel/NovelPanel.tsx +3135 -2651
  103. package/src/client/panel/PageHeader.tsx +37 -0
  104. package/src/client/panel/ProgressConsole.tsx +63 -0
  105. package/src/client/panel/PromptSlotsView.tsx +97 -0
  106. package/src/client/panel/ReaderView.tsx +306 -306
  107. package/src/client/panel/ReasoningSection.tsx +31 -0
  108. package/src/client/panel/RunPanel.tsx +64 -32
  109. package/src/client/panel/SettingsView.tsx +354 -0
  110. package/src/client/panel/ShelfView.tsx +266 -236
  111. package/src/client/panel/SubPage.tsx +42 -0
  112. package/src/client/panel/TensionView.tsx +212 -0
  113. package/src/client/panel/TimelineView.tsx +208 -0
  114. package/src/client/panel/WorldTab.tsx +202 -202
  115. package/src/client/panel/helpers.ts +7 -5
  116. package/src/client/panel/panel.module.css +4481 -1655
  117. package/src/client/panel/toast.ts +51 -0
  118. package/src/client/panel/views.tsx +401 -314
  119. package/src/client/run-status.ts +171 -0
  120. package/src/client/sidebar-entry.ts +1 -1
  121. package/src/client/text.ts +28 -0
  122. package/src/docx-core.ts +53 -0
  123. package/src/docx.ts +44 -83
  124. package/src/engine.ts +4672 -2943
  125. package/src/global-assets.ts +59 -0
  126. package/src/index.ts +89 -33
  127. package/src/llm-live.ts +260 -0
  128. package/src/llm-retry.ts +77 -0
  129. package/src/market-radar-scan.ts +41 -0
  130. package/src/market-radar-sources.ts +173 -0
  131. package/src/migrate-output.ts +76 -0
  132. package/src/novel-context.ts +286 -0
  133. package/src/prompt-slots.ts +125 -0
  134. package/src/prompting.ts +26 -0
  135. package/src/protocol.ts +1388 -158
  136. package/src/review-policy.ts +161 -0
  137. package/src/revision.ts +283 -0
  138. package/src/routes.ts +1934 -434
  139. package/src/run.ts +118 -21
  140. package/src/snapshots.ts +174 -0
  141. package/src/stage-contract.ts +189 -0
  142. package/src/tension.ts +251 -0
  143. package/src/timeline.ts +159 -0
  144. package/lib/client.js.map +0 -1
  145. package/lib/index.js.map +0 -1
  146. package/lib/types/comic-presets.d.ts +0 -12
  147. package/scripts/assistant-frames.txt +0 -8
  148. package/scripts/diagnose-adapter.mts +0 -81
  149. package/scripts/diagnose-assistant.mjs +0 -58
  150. package/scripts/diagnose-bible.mjs +0 -72
  151. package/scripts/diagnose-harness.mts +0 -75
  152. package/scripts/novel-forge-restart.log +0 -10
  153. package/scripts/novel-forge-web.stderr.log +0 -2
  154. package/scripts/novel-forge-web.stdout.log +0 -1
  155. package/scripts/outline-sample.txt +0 -449
  156. package/scripts/probe.mjs +0 -6
  157. package/scripts/restart-web.ps1 +0 -88
  158. package/scripts/smoke.mts +0 -72
  159. package/scripts/upload-github.ps1 +0 -51
  160. package/src/client/panel/BookshelfBar.tsx +0 -170
  161. package/src/comic-presets.ts +0 -67
@@ -0,0 +1,535 @@
1
+ /**
2
+ * 改编模式 · 分步向导(上传全文主线):
3
+ * ① 导入全文 → ② 设定卡片/可改范围 → ③ 确认改编维度 → ④ 编辑改编方案 → ⑤ 提炼新书资料 → ⑥ 保存并开始写书。
4
+ */
5
+ import { useRef, useState } from 'react'
6
+ import { Wand2, Upload, Download, ListChecks, Play, ArrowLeft, ArrowRight, FileDown, Save, Plus, Trash2, BookOpen } from 'lucide-react'
7
+ import type { NovelApi } from '../api.ts'
8
+ import type {
9
+ AdaptAnalyzeResponse,
10
+ AdaptationProposal,
11
+ AdaptationMapping,
12
+ AdaptationRules,
13
+ AdaptExecuteResponse,
14
+ AdaptSaveResponse,
15
+ AdaptMaterializeResponse,
16
+ AdaptMaterializeSaveResponse,
17
+ } from '../../protocol.ts'
18
+ import { readFileTextSmart } from '../text.ts'
19
+ import css from './panel.module.css'
20
+ import { SubPage } from './SubPage.tsx'
21
+
22
+ const MUTABILITY_LABEL: Record<string, string> = {
23
+ locked: ' 建议保留', big: ' 可改影响大', small: ' 可改影响小', free: ' 可自由改', visual: ' 仅视觉包装',
24
+ };
25
+ const MUTABILITY_CLS: Record<string, string> = {
26
+ locked: 'var(--nf-muted)', big: 'var(--nf-warn)', small: 'var(--nf-info)', free: 'var(--nf-accent)', visual: 'var(--nf-muted)',
27
+ };
28
+ const RISK_LABEL: Record<string, string> = { high: '高风险', medium: '中风险', low: '低风险' };
29
+ const SCOPE_LABEL: Record<string, string> = { name: '人名', realm: '境界/体系', faction: '势力/组织', term: '术语', other: '其他' };
30
+
31
+ const STEPS = [
32
+ { n: 1, label: '导入全文' },
33
+ { n: 2, label: '设定卡片' },
34
+ { n: 3, label: '确认改编维度' },
35
+ { n: 4, label: '编辑改编方案' },
36
+ { n: 5, label: '提炼新书资料' },
37
+ { n: 6, label: '保存并开始写书' },
38
+ ] as const;
39
+
40
+ interface EditState { enabled: boolean; target: string }
41
+ type RuleKey = keyof AdaptationRules
42
+
43
+ export function AdaptModeView({ api, onOpenBook }: { api: NovelApi; onOpenBook?: (id: string) => void }) {
44
+ const [step, setStep] = useState(1);
45
+ const [text, setText] = useState('');
46
+ const [result, setResult] = useState<AdaptAnalyzeResponse | null>(null);
47
+ const [edits, setEdits] = useState<Record<string, EditState>>({});
48
+ const [proposal, setProposal] = useState<AdaptationProposal | null>(null);
49
+ const [executed, setExecuted] = useState<AdaptExecuteResponse | null>(null);
50
+ const [saved, setSaved] = useState<AdaptSaveResponse | null>(null);
51
+ const [materialized, setMaterialized] = useState<AdaptMaterializeResponse | null>(null);
52
+ const [savedMaterialized, setSavedMaterialized] = useState<AdaptMaterializeSaveResponse | null>(null);
53
+ const [chapterCount, setChapterCount] = useState(30);
54
+ const [rewriteStart, setRewriteStart] = useState(1);
55
+ const [rewriteEnd, setRewriteEnd] = useState(0);
56
+ const [rewriteProgress, setRewriteProgress] = useState<{ completed: number; total: number; no: number; title: string } | null>(null);
57
+ const [busy, setBusy] = useState(false);
58
+ const [busyLabel, setBusyLabel] = useState('');
59
+ const [error, setError] = useState('');
60
+ const fileRef = useRef<HTMLInputElement | null>(null);
61
+
62
+ const onFile = async (e: React.ChangeEvent<HTMLInputElement>): Promise<void> => {
63
+ const file = e.target.files?.[0];
64
+ if (file === undefined) return;
65
+ try { setText(await readFileTextSmart(file)) } catch (err) { setError((err as Error).message) }
66
+ e.target.value = '';
67
+ };
68
+
69
+ const analyze = async (): Promise<void> => {
70
+ if (text.trim().length < 200) { setError('请粘贴或上传完整小说全文(至少 200 字符)'); return }
71
+ setBusy(true); setBusyLabel('分析中…'); setError(''); setProposal(null); setExecuted(null); setSaved(null); setMaterialized(null);
72
+ try {
73
+ const res = await api.adaptAnalyze(text);
74
+ setResult(res);
75
+ const next: Record<string, EditState> = {};
76
+ for (const d of res.dimensions) {
77
+ next[d.key] = { enabled: d.mutability !== 'locked', target: d.candidates?.[0]?.name ?? '' };
78
+ }
79
+ setEdits(next);
80
+ setStep(2);
81
+ } catch (err) { setError((err as Error).message) } finally { setBusy(false); setBusyLabel('') }
82
+ };
83
+
84
+ const setEdit = (key: string, patch: Partial<EditState>): void => {
85
+ setEdits(prev => ({ ...prev, [key]: { ...prev[key], ...patch } }));
86
+ };
87
+
88
+ const selectedDims = result === null ? [] : result.dimensions.filter(d => {
89
+ const e = edits[d.key];
90
+ return e?.enabled === true && (e.target ?? '').trim() !== '';
91
+ });
92
+
93
+ const buildSelections = () => selectedDims.map(d => ({ key: d.key, title: d.title, current: d.current, target: (edits[d.key]?.target ?? '').trim(), mutability: d.mutability }));
94
+
95
+ const propose = async (): Promise<void> => {
96
+ if (result === null || text.trim().length < 200) { setError('请先分析'); return }
97
+ const selections = buildSelections();
98
+ if (selections.length === 0) { setError('请至少选择一条要改的维度并填写新值'); return }
99
+ setBusy(true); setBusyLabel('生成改编方案…'); setError(''); setExecuted(null); setSaved(null); setMaterialized(null);
100
+ try {
101
+ const res = await api.adaptPropose({ text, selections, dimensions: result.dimensions });
102
+ setProposal(res.proposal);
103
+ setStep(4);
104
+ } catch (err) { setError((err as Error).message) } finally { setBusy(false); setBusyLabel('') }
105
+ };
106
+
107
+ const patchMapping = (idx: number, patch: Partial<AdaptationMapping>): void => {
108
+ setProposal(prev => prev === null ? prev : ({ ...prev, mappings: prev.mappings.map((m, i) => i === idx ? { ...m, ...patch } : m) }));
109
+ };
110
+ const addMapping = (): void => {
111
+ setProposal(prev => prev === null ? prev : ({ ...prev, mappings: [...prev.mappings, { source: '', target: '', scope: 'other', note: '' }] }));
112
+ };
113
+ const removeMapping = (idx: number): void => {
114
+ setProposal(prev => prev === null ? prev : ({ ...prev, mappings: prev.mappings.filter((_, i) => i !== idx) }));
115
+ };
116
+ const patchRule = (key: RuleKey, idx: number, value: string): void => {
117
+ setProposal(prev => prev === null ? prev : ({ ...prev, rules: { ...prev.rules, [key]: prev.rules[key].map((s, i) => i === idx ? value : s) } }));
118
+ };
119
+ const addRule = (key: RuleKey): void => {
120
+ setProposal(prev => prev === null ? prev : ({ ...prev, rules: { ...prev.rules, [key]: [...prev.rules[key], ''] } }));
121
+ };
122
+ const removeRule = (key: RuleKey, idx: number): void => {
123
+ setProposal(prev => prev === null ? prev : ({ ...prev, rules: { ...prev.rules, [key]: prev.rules[key].filter((_, i) => i !== idx) } }));
124
+ };
125
+
126
+ const execute = async (): Promise<void> => {
127
+ if (proposal === null || proposal.mappings.length === 0) { setError('请先生成改编方案'); return }
128
+ setBusy(true); setBusyLabel('执行术语替换…'); setError(''); setSaved(null);
129
+ try {
130
+ const res = await api.adaptExecute({ text, mappings: proposal.mappings, mode: 'replace' });
131
+ setExecuted(res);
132
+ } catch (err) { setError((err as Error).message) } finally { setBusy(false); setBusyLabel('') }
133
+ };
134
+
135
+ const executeRewrite = async (): Promise<void> => {
136
+ if (proposal === null || proposal.mappings.length === 0) { setError('请先生成改编方案'); return }
137
+ setBusy(true); setBusyLabel('深度改写(重写引擎)…'); setError(''); setSaved(null); setExecuted(null); setRewriteProgress(null);
138
+ try {
139
+ await api.adaptRewriteStream({ text, mappings: proposal.mappings, rules: proposal.rules, mode: 'rewrite', startNo: rewriteStart, endNo: rewriteEnd }, (frame) => {
140
+ if (frame.type === 'progress') {
141
+ setRewriteProgress({ completed: frame.completed, total: frame.total, no: frame.no, title: frame.title });
142
+ setBusyLabel('深度改写中:第 ' + frame.completed + '/' + frame.total + ' 章…');
143
+ } else if (frame.type === 'done') {
144
+ setExecuted(frame.result);
145
+ setRewriteProgress(null);
146
+ } else if (frame.type === 'error') {
147
+ setError(frame.message);
148
+ setRewriteProgress(null);
149
+ }
150
+ });
151
+ } catch (err) { setError((err as Error).message); setRewriteProgress(null) } finally { setBusy(false); setBusyLabel('') }
152
+ };
153
+
154
+ const saveBook = async (): Promise<void> => {
155
+ if (executed === null) return;
156
+ setBusy(true); setBusyLabel('保存为改编新书…'); setError(''); setSaved(null);
157
+ try {
158
+ const base = result?.bookName !== undefined && result.bookName !== '' ? result.bookName + '·改编版' : '改编版';
159
+ const res = await api.adaptSave({ text: executed.adaptedText, bookName: base, outline: result?.outline });
160
+ setSaved(res);
161
+ } catch (err) { setError((err as Error).message) } finally { setBusy(false); setBusyLabel('') }
162
+ };
163
+
164
+ const materializeBook = async (): Promise<void> => {
165
+ if (proposal === null || proposal.mappings.length === 0) { setError('方案缺少映射条目,请至少保留一条'); return }
166
+ if (result === null || text.trim().length < 200) { setError('请先完成分析'); return }
167
+ setBusy(true); setBusyLabel('提炼新书资料…'); setError(''); setMaterialized(null); setSavedMaterialized(null);
168
+ try {
169
+ const base = result.bookName !== '' ? result.bookName + '·改编版' : '改编版';
170
+ const res = await api.adaptMaterialize({
171
+ text,
172
+ bookName: base,
173
+ outline: result.outline,
174
+ proposal,
175
+ chapterCount,
176
+ });
177
+ setMaterialized(res);
178
+ setStep(6);
179
+ } catch (err) { setError((err as Error).message) } finally { setBusy(false); setBusyLabel('') }
180
+ };
181
+
182
+ const patchMaterial = (patch: Partial<AdaptMaterializeResponse>): void => {
183
+ setMaterialized(prev => prev === null ? prev : ({ ...prev, ...patch }));
184
+ };
185
+ const patchBibleList = (key: 'worldRules' | 'redLines' | 'style', idx: number, value: string): void => {
186
+ setMaterialized(prev => prev === null ? prev : ({ ...prev, bible: { ...prev.bible, [key]: prev.bible[key].map((s, i) => i === idx ? value : s) } }));
187
+ };
188
+ const addBibleList = (key: 'worldRules' | 'redLines' | 'style'): void => {
189
+ setMaterialized(prev => prev === null ? prev : ({ ...prev, bible: { ...prev.bible, [key]: [...prev.bible[key], ''] } }));
190
+ };
191
+ const removeBibleList = (key: 'worldRules' | 'redLines' | 'style', idx: number): void => {
192
+ setMaterialized(prev => prev === null ? prev : ({ ...prev, bible: { ...prev.bible, [key]: prev.bible[key].filter((_, i) => i !== idx) } }));
193
+ };
194
+ const patchRoleName = (idx: number, value: string): void => {
195
+ setMaterialized(prev => prev === null ? prev : ({ ...prev, roles: prev.roles.map((r, i) => i === idx ? { ...r, name: value } : r) }));
196
+ };
197
+ const patchVolume = (idx: number, patch: Partial<AdaptMaterializeResponse['volumes'][number]>): void => {
198
+ setMaterialized(prev => prev === null ? prev : ({ ...prev, volumes: prev.volumes.map((v, i) => i === idx ? { ...v, ...patch } : v) }));
199
+ };
200
+ const saveMaterialized = async (): Promise<void> => {
201
+ if (materialized === null) return;
202
+ setBusy(true); setBusyLabel('保存新书…'); setError(''); setSavedMaterialized(null);
203
+ try {
204
+ const res = await api.adaptMaterializeSave({
205
+ bookName: materialized.bookName,
206
+ outputDir: materialized.outputDir,
207
+ outline: materialized.outline,
208
+ bible: materialized.bible,
209
+ roles: materialized.roles,
210
+ world: materialized.world,
211
+ volumes: materialized.volumes,
212
+ chapters: materialized.chapters,
213
+ });
214
+ setSavedMaterialized(res);
215
+ setMaterialized(prev => prev === null ? prev : ({ ...prev, book: res.book }));
216
+ } catch (err) { setError((err as Error).message) } finally { setBusy(false); setBusyLabel('') }
217
+ };
218
+
219
+ const downloadFile = (ext: 'md' | 'txt'): void => {
220
+ if (executed === null) return;
221
+ const blob = new Blob([executed.adaptedText], { type: ext === 'md' ? 'text/markdown;charset=utf-8' : 'text/plain;charset=utf-8' });
222
+ const url = URL.createObjectURL(blob);
223
+ const a = document.createElement('a');
224
+ a.href = url;
225
+ const base = result?.bookName !== undefined && result.bookName !== '' ? result.bookName + '·改编版' : '改编版';
226
+ a.download = base + '.' + ext;
227
+ a.click();
228
+ URL.revokeObjectURL(url);
229
+ };
230
+
231
+ const canGo = (n: number): boolean => {
232
+ if (n === 1) return true;
233
+ if (n === 2 || n === 3) return result !== null;
234
+ if (n === 4) return selectedDims.length > 0;
235
+ if (n === 5) return proposal !== null && proposal.mappings.length > 0;
236
+ if (n === 6) return materialized !== null;
237
+ return false;
238
+ };
239
+
240
+ return (
241
+ <SubPage
242
+ title="改编模式"
243
+ meta="六步向导:导入全文 → 设定卡片 → 确认改编维度 → 编辑改编方案 → 提炼新书资料 → 保存并写书。"
244
+ >
245
+ <div className={css.card + ' ' + css.settingsCard} style={{ gap: 'var(--nf-space-10)' }}>
246
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
247
+ {STEPS.map(s => (
248
+ <button key={s.n} type="button"
249
+ className={css.button + ' ' + css.buttonSmall + (step === s.n ? ' ' + css.buttonPrimary : '')}
250
+ disabled={!canGo(s.n)}
251
+ onClick={() => setStep(s.n)}
252
+ title={canGo(s.n) ? '第 ' + s.n + ' 步:' + s.label : '需先完成前置步骤'}>
253
+ {s.n}. {s.label}
254
+ </button>
255
+ ))}
256
+ </div>
257
+ </div>
258
+
259
+ {error !== '' && <div className={css.noticeError}>{error}</div>}
260
+
261
+ {step === 1 && (
262
+ <div className={css.adaptInputCard}>
263
+ <div className={css.rowEnd} style={{ justifyContent: 'space-between' }}>
264
+ <span className={css.meta}>① 粘贴全文,或上传 txt/md 文件</span>
265
+ <button type="button" className={css.button + ' ' + css.buttonSmall} onClick={() => fileRef.current?.click()}><Upload size={13} style={{ verticalAlign: -2 }} /> 上传文件</button>
266
+ </div>
267
+ <input ref={fileRef} type="file" accept=".txt,.md,text/plain,text/markdown" style={{ display: 'none' }} onChange={e => { void onFile(e) }} />
268
+ <textarea className={css.input + ' ' + css.adaptTextarea} value={text} onChange={e => setText(e.target.value)} placeholder="粘贴小说全文(含章节标题/分卷更好)…" />
269
+ <div className={css.rowEnd}><span className={css.meta}>已输入 {text.length} 字</span><button type="button" className={css.button + ' ' + css.buttonPrimary} onClick={() => { void analyze() }} disabled={busy}><Wand2 size={14} style={{ verticalAlign: -2 }} /> {busy && busyLabel === '分析中…' ? '分析中…' : '开始分析'}</button></div>
270
+ </div>
271
+ )}
272
+
273
+ {step === 2 && result !== null && (
274
+ <div className={css.adaptResult}>
275
+ <div className={css.authorPageHeader} style={{ alignItems: 'center' }}>
276
+ <h3 style={{ margin: 0 }}>② 《{result.bookName}》 · {result.chapters} 章</h3>
277
+ <span className={css.meta}>{result.note}</span>
278
+ </div>
279
+ {result.outline !== undefined && result.outline !== '' && (
280
+ <details className={css.adaptOutline}><summary>反推大纲(点击展开)</summary><pre className={css.adaptOutlinePre}>{result.outline}</pre></details>
281
+ )}
282
+ <div className={css.adaptMatrix}>
283
+ {result.dimensions.map((d, i) => (
284
+ <div key={d.key + '-' + i} className={css.dimCard}>
285
+ <div className={css.dimCardHead}>
286
+ <span className={css.dimTitle}>{d.title}</span>
287
+ <span className={css.badge} style={{ color: MUTABILITY_CLS[d.mutability] ?? 'var(--nf-muted)' }}>{MUTABILITY_LABEL[d.mutability] ?? d.mutability}</span>
288
+ <span className={css.meta}>风险:{RISK_LABEL[d.risk] ?? d.risk}</span>
289
+ </div>
290
+ <div className={css.dimCurrent}><b>当前值</b>:{d.current}</div>
291
+ {(d.evidence !== undefined && d.evidence !== '') && <div className={css.meta}>证据:{d.evidence}</div>}
292
+ {(d.candidates ?? []).length > 0 && <div className={css.meta}>候选:{(d.candidates ?? []).map(c => `${c.name}${c.desc !== undefined && c.desc !== '' ? `(${c.desc})` : ''}`).join(' / ')}</div>}
293
+ <div className={css.meta}>影响:{d.impact}</div>
294
+ </div>
295
+ ))}
296
+ </div>
297
+ <div className={css.rowEnd} style={{ justifyContent: 'flex-end' }}>
298
+ <button type="button" className={css.button + ' ' + css.buttonPrimary} onClick={() => setStep(3)}><ArrowRight size={14} style={{ verticalAlign: -2 }} /> 下一步:③ 确认改编维度</button>
299
+ </div>
300
+ </div>
301
+ )}
302
+
303
+ {step === 3 && result !== null && (
304
+ <div className={css.adaptResult}>
305
+ <div className={css.authorPageHeader} style={{ alignItems: 'center' }}>
306
+ <b>③ 确认改编维度(勾选要改 + 填新值)</b>
307
+ <button type="button" className={css.button + ' ' + css.buttonPrimary} onClick={() => { void propose() }} disabled={busy}><ListChecks size={13} style={{ verticalAlign: -2 }} /> {busy && busyLabel === '生成改编方案…' ? '生成中…' : '生成改编方案'} ({selectedDims.length})</button>
308
+ </div>
309
+ <div className={css.adaptMatrix}>
310
+ {result.dimensions.map((d, i) => {
311
+ const e = edits[d.key];
312
+ const enabled = e?.enabled === true;
313
+ const target = e?.target ?? '';
314
+ return (
315
+ <div key={d.key + '-' + i} className={css.dimCard + (enabled ? ' ' + css.dimCardSelected : '')}>
316
+ <div className={css.dimCardHead}>
317
+ <span className={css.dimTitle}>{d.title}</span>
318
+ <span className={css.badge} style={{ color: MUTABILITY_CLS[d.mutability] ?? 'var(--nf-muted)' }}>{MUTABILITY_LABEL[d.mutability] ?? d.mutability}</span>
319
+ <span className={css.meta}>风险:{RISK_LABEL[d.risk] ?? d.risk}</span>
320
+ </div>
321
+ <div className={css.dimCurrent}><b>当前值</b>:{d.current}</div>
322
+ {d.mutability !== 'locked' && (
323
+ <div className={css.dimEdit}>
324
+ <input type="checkbox" checked={enabled} onChange={e2 => setEdit(d.key, { enabled: e2.target.checked })} />
325
+ <input className={css.input} value={target} disabled={!enabled} placeholder="改为什么?" onChange={e2 => setEdit(d.key, { target: e2.target.value })} />
326
+ </div>
327
+ )}
328
+ {(d.candidates ?? []).length > 0 && (
329
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--nf-space-6)', marginTop: 6 }}>
330
+ {(d.candidates ?? []).map((c, ci) => (
331
+ <button
332
+ key={ci}
333
+ type="button"
334
+ className={css.badge}
335
+ title={c.desc ?? ''}
336
+ style={{ cursor: 'pointer', borderColor: target === c.name ? 'var(--nf-accent)' : 'var(--nf-border)', color: target === c.name ? 'var(--nf-accent)' : 'var(--nf-text-2)', background: 'transparent' }}
337
+ onClick={() => setEdit(d.key, { enabled: true, target: c.name })}
338
+ >
339
+ {c.name}{c.desc !== undefined && c.desc !== '' ? ` · ${c.desc}` : ''}
340
+ </button>
341
+ ))}
342
+ </div>
343
+ )}
344
+ <div className={css.meta}>影响:{d.impact}</div>
345
+ </div>
346
+ );
347
+ })}
348
+ </div>
349
+ <div className={css.rowEnd} style={{ justifyContent: 'space-between' }}>
350
+ <button type="button" className={css.button} onClick={() => setStep(2)}><ArrowLeft size={13} style={{ verticalAlign: -2 }} /> 上一步</button>
351
+ </div>
352
+ </div>
353
+ )}
354
+
355
+ {step === 4 && proposal !== null && (
356
+ <div className={css.adaptProposal}>
357
+ <div className={css.authorPageHeader} style={{ alignItems: 'center' }}>
358
+ <b>④ 编辑改编方案(映射/规则可增删改)</b>
359
+ <button type="button" className={css.button + ' ' + css.buttonPrimary} onClick={() => setStep(5)}><ArrowRight size={14} style={{ verticalAlign: -2 }} /> 下一步:⑤ 提炼新书资料</button>
360
+ </div>
361
+
362
+ <h4 style={{ margin: '10px 0 4px' }}>映射表</h4>
363
+ <div className={css.adaptTableScroll}>
364
+ <table className={css.adaptTable}><thead><tr><th>原值</th><th>新值</th><th>范围</th><th>说明</th><th></th></tr></thead><tbody>
365
+ {proposal.mappings.map((m, idx) => (
366
+ <tr key={idx}>
367
+ <td><input className={css.input + ' ' + css.adaptCell} value={m.source} placeholder="原值" onChange={e => patchMapping(idx, { source: e.target.value })} /></td>
368
+ <td><input className={css.input + ' ' + css.adaptCell} value={m.target} placeholder="新值" onChange={e => patchMapping(idx, { target: e.target.value })} /></td>
369
+ <td>
370
+ <select className={css.input + ' ' + css.adaptCell} value={m.scope} onChange={e => patchMapping(idx, { scope: e.target.value as AdaptationMapping['scope'] })}>
371
+ {Object.keys(SCOPE_LABEL).map(s => <option key={s} value={s}>{SCOPE_LABEL[s]}</option>)}
372
+ </select>
373
+ </td>
374
+ <td><input className={css.input + ' ' + css.adaptCell} value={m.note ?? ''} placeholder="说明" onChange={e => patchMapping(idx, { note: e.target.value })} /></td>
375
+ <td><button type="button" className={css.iconBtn} onClick={() => removeMapping(idx)}><Trash2 size={14} /></button></td>
376
+ </tr>
377
+ ))}
378
+ </tbody></table>
379
+ </div>
380
+ <div className={css.rowEnd}>
381
+ <button type="button" className={css.button + ' ' + css.buttonSmall} onClick={addMapping}><Plus size={13} style={{ verticalAlign: -2 }} /> 加一条映射</button>
382
+ </div>
383
+
384
+ <h4 style={{ margin: '10px 0 4px' }}>改编规则</h4>
385
+ <div className={css.adaptRules}>
386
+ {(['preserve', 'change', 'constraints'] as RuleKey[]).map(key => (
387
+ <div key={key} className={css.adaptRule}>
388
+ <b>{key === 'preserve' ? ' 保留' : key === 'change' ? ' 允许变' : ' 红线'}</b>
389
+ {proposal.rules[key].map((s, idx) => (
390
+ <div key={idx} className={css.adaptRuleRow}>
391
+ <input className={css.input} value={s} onChange={e => patchRule(key, idx, e.target.value)} />
392
+ <button type="button" className={css.iconBtn} onClick={() => removeRule(key, idx)}><Trash2 size={13} /></button>
393
+ </div>
394
+ ))}
395
+ <button type="button" className={css.button + ' ' + css.buttonSmall} onClick={() => addRule(key)}><Plus size={12} style={{ verticalAlign: -2 }} /> 加一条</button>
396
+ </div>
397
+ ))}
398
+ </div>
399
+
400
+ <h4 style={{ margin: '10px 0 4px' }}>联动影响</h4>
401
+ <div className={css.adaptImpacts}>
402
+ {proposal.impacts.map((im, i3) => (<div key={i3} className={css.adaptImpact}><b style={{ color: im.risk === 'high' ? 'var(--nf-error)' : im.risk === 'medium' ? 'var(--nf-warn)' : 'var(--nf-info)' }}>[{RISK_LABEL[im.risk] ?? im.risk}]</b> {im.item}:{im.detail}{im.chapters !== undefined && im.chapters.length > 0 ? '(章:' + im.chapters.join(',') + ')' : ''}</div>))}
403
+ </div>
404
+
405
+ <details className={css.adaptOutline}>
406
+ <summary>快速路径:仅术语替换 / 深度改写(重写引擎)(可选)</summary>
407
+ <div className={css.rowEnd} style={{ gap: 8, flexWrap: 'wrap' }}>
408
+ <button type="button" className={css.button} onClick={() => { void execute() }} disabled={busy}><Play size={13} style={{ verticalAlign: -2 }} /> {busy && busyLabel === '执行术语替换…' ? '执行中…' : '执行术语替换'}</button>
409
+ <button type="button" className={css.button + ' ' + css.buttonPrimary} onClick={() => { void executeRewrite() }} disabled={busy}><Wand2 size={13} style={{ verticalAlign: -2 }} /> {busy && busyLabel === '深度改写(重写引擎)…' ? '改写中…' : '深度改写(重写引擎)'}</button>
410
+ <span className={css.meta}>起始</span>
411
+ <input type="number" className={css.input + ' ' + css.adaptCell} style={{ width: 64 }} value={rewriteStart} onChange={e => setRewriteStart(Math.max(1, Math.min(Number(e.target.value) || 1, 5000)))} />
412
+ <span className={css.meta}>至</span>
413
+ <input type="number" className={css.input + ' ' + css.adaptCell} style={{ width: 64 }} value={rewriteEnd} onChange={e => setRewriteEnd(Math.max(0, Math.min(Number(e.target.value) || 0, 5000)))} />
414
+ <span className={css.meta}>章(0=到末尾)</span>
415
+ </div>
416
+ {rewriteProgress !== null && (
417
+ <div className={css.meta}>⏳ 改写中:第 {rewriteProgress.completed}/{rewriteProgress.total} 章(当前:第 {rewriteProgress.no} 章《{rewriteProgress.title}》)</div>
418
+ )}
419
+ {executed !== null && (
420
+ <>
421
+ <div className={css.meta}>
422
+ {executed.mode === 'rewrite'
423
+ ? '重写完成:' + (executed.rewritten ?? []).length + ' 章;保留原章:' + ((executed.skipped ?? []).join('、') || '无')
424
+ : '命中:' + (executed.hits.filter(h => h.count > 0).map(h => h.source + '→' + h.target + '×' + h.count).join(',') || '无命中')}
425
+ </div>
426
+ <div className={css.rowEnd} style={{ gap: 8, flexWrap: 'wrap' }}>
427
+ <button type="button" className={css.button + ' ' + css.buttonSmall} onClick={() => downloadFile('md')}><FileDown size={13} style={{ verticalAlign: -2 }} /> 下载 .md</button>
428
+ <button type="button" className={css.button + ' ' + css.buttonSmall} onClick={() => downloadFile('txt')}><Download size={13} style={{ verticalAlign: -2 }} /> 下载 .txt</button>
429
+ {executed.mode === 'rewrite' ? (
430
+ <>
431
+ <button type="button" className={css.button + ' ' + css.buttonSmall} onClick={() => { setText(executed.adaptedText); setExecuted(null); setSaved(null); setStep(5); }}><Wand2 size={13} style={{ verticalAlign: -2 }} /> 用改写结果继续提炼新书</button>
432
+ <button type="button" className={css.button + ' ' + css.buttonSmall} onClick={() => { void saveBook() }} disabled={busy}><Save size={13} style={{ verticalAlign: -2 }} /> 保存为新书(改写全文)</button>
433
+ </>
434
+ ) : (
435
+ <button type="button" className={css.button + ' ' + css.buttonSmall} onClick={() => { void saveBook() }} disabled={busy}><Save size={13} style={{ verticalAlign: -2 }} /> 保存为新书(全文替换)</button>
436
+ )}
437
+ </div>
438
+ <details className={css.adaptOutline}><summary>{executed.mode === 'rewrite' ? '改写后正文预览' : '替换后正文预览'}(点击展开)</summary><pre className={css.adaptOutlinePre}>{executed.adaptedText.slice(0, 8000)}</pre></details>
439
+ </>
440
+ )}
441
+ {saved !== null && (
442
+ <div className={css.noticeSuccess}>
443
+ 已保存为新书《{saved.bookName}》({saved.chapters} 章)。原书未改动。
444
+ {onOpenBook !== undefined && <button type="button" className={css.button + ' ' + css.buttonSmall} onClick={() => onOpenBook(saved.book.id)}>打开新书</button>}
445
+ </div>
446
+ )}
447
+ <span className={css.meta}>提示:「深度改写」逐章 LLM 重写(耗额度,适合结构性改编);「仅替换」更快但只换词。若想生成「待写新书」,请在上方继续「⑤ 提炼新书资料」;点「用改写结果继续提炼新书」则让提炼基于改写后正文。</span>
448
+ </details>
449
+
450
+ <div className={css.rowEnd} style={{ justifyContent: 'space-between' }}>
451
+ <button type="button" className={css.button} onClick={() => setStep(3)}><ArrowLeft size={13} style={{ verticalAlign: -2 }} /> 上一步</button>
452
+ </div>
453
+ </div>
454
+ )}
455
+
456
+ {step === 5 && proposal !== null && result !== null && (
457
+ <div className={css.adaptProposal}>
458
+ <div className={css.authorPageHeader} style={{ alignItems: 'center' }}>
459
+ <b>⑤ 提炼新书资料(预览,暂不保存)</b>
460
+ <button type="button" className={css.button + ' ' + css.buttonPrimary} onClick={() => { void materializeBook() }} disabled={busy}><Wand2 size={14} style={{ verticalAlign: -2 }} /> {busy && busyLabel === '提炼新书资料…' ? '提炼中…' : '提炼新书资料(预览)'}</button>
461
+ </div>
462
+ <span className={css.meta}>将基于编辑后的映射/规则,从源文提炼:改编后总纲、道藏、角色库、大世界、卷计划、章节计划。生成后到第⑥步可微调,确认后再保存为《{result.bookName}·改编版》的待写新书(章节为 pending,随后可在小说工坊逐章生成)。</span>
463
+ <div className={css.rowEnd} style={{ gap: 8 }}>
464
+ <span className={css.meta}>拟规划章节数</span>
465
+ <input type="number" className={css.input + ' ' + css.adaptCell} style={{ width: 90 }} value={chapterCount}
466
+ onChange={e => setChapterCount(Math.max(1, Math.min(Number(e.target.value) || 30, 500)))} />
467
+ </div>
468
+ <div className={css.rowEnd} style={{ justifyContent: 'space-between' }}>
469
+ <button type="button" className={css.button} onClick={() => setStep(4)}><ArrowLeft size={13} style={{ verticalAlign: -2 }} /> 上一步</button>
470
+ </div>
471
+ </div>
472
+ )}
473
+
474
+ {step === 6 && materialized !== null && (
475
+ <div className={css.adaptProposal}>
476
+ <div className={css.authorPageHeader} style={{ alignItems: 'center' }}>
477
+ <b>⑥ 预览与微调(保存为新书)</b>
478
+ <button type="button" className={css.button + ' ' + css.buttonPrimary} onClick={() => { void saveMaterialized() }} disabled={busy || savedMaterialized !== null}><Save size={14} style={{ verticalAlign: -2 }} /> {busy ? '保存中…' : '保存为新书'}</button>
479
+ </div>
480
+ {savedMaterialized !== null && (
481
+ <div className={css.noticeSuccess}>
482
+ 已保存为新书《{savedMaterialized.bookName}》({savedMaterialized.chapters} 章待写)。原书未改动。
483
+ {onOpenBook !== undefined && <button type="button" className={css.button + ' ' + css.buttonSmall} onClick={() => onOpenBook(savedMaterialized.book.id)}><BookOpen size={14} style={{ verticalAlign: -2 }} /> 打开新书开始编写</button>}
484
+ </div>
485
+ )}
486
+ <span className={css.meta}>可微调以下资料后再保存:总纲、道藏、角色名、卷计划。保存后即为「待写新书」,可到小说工坊逐章生成。</span>
487
+
488
+ <h4 style={{ margin: '10px 0 4px' }}>改编后总纲</h4>
489
+ <textarea className={css.input + ' ' + css.adaptTextarea} value={materialized.outline} onChange={e => patchMaterial({ outline: e.target.value })} />
490
+
491
+ <h4 style={{ margin: '10px 0 4px' }}>道藏</h4>
492
+ <div className={css.adaptRules}>
493
+ {(['worldRules', 'redLines', 'style'] as const).map(key => (
494
+ <div key={key} className={css.adaptRule}>
495
+ <b>{key === 'worldRules' ? ' 世界观规则' : key === 'redLines' ? ' 红线' : ' 风格'}</b>
496
+ {materialized.bible[key].map((s, i) => (
497
+ <div key={i} className={css.adaptRuleRow}>
498
+ <input className={css.input} value={s} onChange={e => patchBibleList(key, i, e.target.value)} />
499
+ <button type="button" className={css.iconBtn} onClick={() => removeBibleList(key, i)}><Trash2 size={13} /></button>
500
+ </div>
501
+ ))}
502
+ <button type="button" className={css.button + ' ' + css.buttonSmall} onClick={() => addBibleList(key)}><Plus size={12} style={{ verticalAlign: -2 }} /> 加一条</button>
503
+ </div>
504
+ ))}
505
+ </div>
506
+
507
+ <h4 style={{ margin: '10px 0 4px' }}>角色库(可改名)</h4>
508
+ <div className={css.adaptRules}>
509
+ {materialized.roles.map((r, i) => (
510
+ <div key={i} className={css.adaptRuleRow}>
511
+ <input className={css.input} value={r.name} onChange={e => patchRoleName(i, e.target.value)} />
512
+ <span className={css.meta}>{r.identity.slice(0, 40)}</span>
513
+ </div>
514
+ ))}
515
+ </div>
516
+
517
+ <h4 style={{ margin: '10px 0 4px' }}>卷计划</h4>
518
+ {materialized.volumes.map((v, i) => (
519
+ <div key={i} className={css.adaptRuleRow}>
520
+ <input className={css.input} value={v.title} onChange={e => patchVolume(i, { title: e.target.value })} style={{ width: '40%' }} />
521
+ <input className={css.input} value={v.summary} onChange={e => patchVolume(i, { summary: e.target.value })} />
522
+ </div>
523
+ ))}
524
+
525
+ <h4 style={{ margin: '10px 0 4px' }}>章节计划(只读;可回第⑤步改章节数重新提炼)</h4>
526
+ <div className={css.meta}>{materialized.chapters.map(c => '第' + c.no + '章 ' + c.title).join('、') || '—'}</div>
527
+
528
+ <div className={css.rowEnd} style={{ justifyContent: 'space-between' }}>
529
+ <button type="button" className={css.button} onClick={() => setStep(5)}><ArrowLeft size={13} style={{ verticalAlign: -2 }} /> 上一步</button>
530
+ </div>
531
+ </div>
532
+ )}
533
+ </SubPage>
534
+ );
535
+ }
@@ -0,0 +1,7 @@
1
+ /** 墨案签名元素 #3:AI 微签。AI 能力 = 名称旁一枚 12px 琥珀 `AI` mono 微签,
2
+ * 不加 、不加"AI ×××"帽。用 `--nf-ai`(琥珀)着色,不发光。 */
3
+ import css from './panel.module.css'
4
+
5
+ export function AiTag(): JSX.Element {
6
+ return <span className={css.aiTag}>AI</span>
7
+ }