@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
|
@@ -1,202 +1,202 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 大世界页签:境界体系 / 地理区域 / 势力分布 的结构化编辑 +
|
|
3
|
-
* 数据注入每章生成与审稿提示词(renderWorld),保证设定不写飞。
|
|
4
|
-
*/
|
|
5
|
-
import { useEffect, useState } from 'react'
|
|
6
|
-
import type { NovelApi } from '../api.ts'
|
|
7
|
-
import type { WorldFaction, WorldRealm, WorldRegion, WorldState } from '../../protocol.ts'
|
|
8
|
-
import css from './panel.module.css'
|
|
9
|
-
|
|
10
|
-
const EMPTY: WorldState = { realms: [], regions: [], factions: [] }
|
|
11
|
-
|
|
12
|
-
/** 一个可编辑条目行。 */
|
|
13
|
-
function EditableRow({
|
|
14
|
-
name,
|
|
15
|
-
detail,
|
|
16
|
-
onName,
|
|
17
|
-
onDetail,
|
|
18
|
-
onRemove,
|
|
19
|
-
namePlaceholder,
|
|
20
|
-
detailPlaceholder,
|
|
21
|
-
}: {
|
|
22
|
-
name: string
|
|
23
|
-
detail: string
|
|
24
|
-
onName: (v: string) => void
|
|
25
|
-
onDetail: (v: string) => void
|
|
26
|
-
onRemove: () => void
|
|
27
|
-
namePlaceholder: string
|
|
28
|
-
detailPlaceholder: string
|
|
29
|
-
}) {
|
|
30
|
-
return (
|
|
31
|
-
<div style={{ display: 'flex', gap: 6, alignItems: 'flex-start' }}>
|
|
32
|
-
<input className={css.input} style={{ flex: 2, minWidth: 0 }} placeholder={namePlaceholder} value={name} onChange={e => { onName(e.target.value) }} />
|
|
33
|
-
<input className={css.input} style={{ flex: 3, minWidth: 0 }} placeholder={detailPlaceholder} value={detail} onChange={e => { onDetail(e.target.value) }} />
|
|
34
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} title="删除" onClick={onRemove}>×</button>
|
|
35
|
-
</div>
|
|
36
|
-
)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** 大世界页签。 */
|
|
40
|
-
export function WorldTab({
|
|
41
|
-
api,
|
|
42
|
-
world,
|
|
43
|
-
onChanged,
|
|
44
|
-
}: {
|
|
45
|
-
api: NovelApi
|
|
46
|
-
/** 当前项目的大世界数据(可能为空)。 */
|
|
47
|
-
world: WorldState | undefined
|
|
48
|
-
/** 保存/提炼成功后由父组件刷新项目状态。 */
|
|
49
|
-
onChanged: (world: WorldState) => void
|
|
50
|
-
}) {
|
|
51
|
-
const [draft, setDraft] = useState<WorldState>(() => world ?? EMPTY)
|
|
52
|
-
const [busy, setBusy] = useState(false)
|
|
53
|
-
const [error, setError] = useState('')
|
|
54
|
-
|
|
55
|
-
// 外部数据变化时同步草稿(提炼/保存/切换书后)。
|
|
56
|
-
useEffect(() => {
|
|
57
|
-
setDraft(world ?? EMPTY)
|
|
58
|
-
}, [world])
|
|
59
|
-
|
|
60
|
-
const handleGenerate = async (): Promise<void> => {
|
|
61
|
-
setBusy(true)
|
|
62
|
-
setError('')
|
|
63
|
-
try {
|
|
64
|
-
const result = await api.world('generate')
|
|
65
|
-
setDraft(result.world)
|
|
66
|
-
onChanged(result.world)
|
|
67
|
-
} catch (err) {
|
|
68
|
-
setError((err as Error).message)
|
|
69
|
-
} finally {
|
|
70
|
-
setBusy(false)
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const handleSave = async (): Promise<void> => {
|
|
75
|
-
setBusy(true)
|
|
76
|
-
setError('')
|
|
77
|
-
try {
|
|
78
|
-
const result = await api.world('save', draft)
|
|
79
|
-
onChanged(result.world)
|
|
80
|
-
} catch (err) {
|
|
81
|
-
setError((err as Error).message)
|
|
82
|
-
} finally {
|
|
83
|
-
setBusy(false)
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const setRealms = (realms: WorldRealm[]): void => setDraft(prev => ({ ...prev, realms }))
|
|
88
|
-
const setRegions = (regions: WorldRegion[]): void => setDraft(prev => ({ ...prev, regions }))
|
|
89
|
-
const setFactions = (factions: WorldFaction[]): void => setDraft(prev => ({ ...prev, factions }))
|
|
90
|
-
|
|
91
|
-
return (
|
|
92
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
93
|
-
{error !== '' && <div className={css.card} style={{ borderColor: 'var(--nf-error)' }}><span style={{ color: 'var(--nf-error)' }}>{error}</span></div>}
|
|
94
|
-
|
|
95
|
-
<div className={css.card}>
|
|
96
|
-
<div className={css.row} style={{ justifyContent: 'space-between' }}>
|
|
97
|
-
<span className={css.cardTitle}>大世界</span>
|
|
98
|
-
<div className={css.row}>
|
|
99
|
-
<button type="button" className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void handleGenerate() }}>
|
|
100
|
-
|
|
101
|
-
</button>
|
|
102
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={busy} onClick={() => { void handleSave() }}>
|
|
103
|
-
|
|
104
|
-
</button>
|
|
105
|
-
</div>
|
|
106
|
-
</div>
|
|
107
|
-
<span className={css.meta}>
|
|
108
|
-
|
|
109
|
-
</span>
|
|
110
|
-
</div>
|
|
111
|
-
|
|
112
|
-
{/* 境界体系 */}
|
|
113
|
-
<div className={css.card}>
|
|
114
|
-
<div className={css.row} style={{ justifyContent: 'space-between' }}>
|
|
115
|
-
<span className={css.cardTitle}>境界体系({draft.realms.length})</span>
|
|
116
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setRealms([...draft.realms, { name: '', description: '' }]) }}>
|
|
117
|
-
+ 新增境界
|
|
118
|
-
</button>
|
|
119
|
-
</div>
|
|
120
|
-
{draft.realms.length === 0 ? (
|
|
121
|
-
<span className={css.meta}>暂无境界体系 — 点击
|
|
122
|
-
) : (
|
|
123
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
124
|
-
{draft.realms.map((realm, i) => (
|
|
125
|
-
<EditableRow
|
|
126
|
-
key={i}
|
|
127
|
-
name={realm.name}
|
|
128
|
-
detail={realm.description}
|
|
129
|
-
onName={v => { const next = [...draft.realms]; next[i] = { ...next[i]!, name: v }; setRealms(next) }}
|
|
130
|
-
onDetail={v => { const next = [...draft.realms]; next[i] = { ...next[i]!, description: v }; setRealms(next) }}
|
|
131
|
-
onRemove={() => { setRealms(draft.realms.filter((_, idx) => idx !== i)) }}
|
|
132
|
-
namePlaceholder={`第 ${i + 1} 阶境界名`}
|
|
133
|
-
detailPlaceholder="突破条件 / 寿命 / 标志…"
|
|
134
|
-
/>
|
|
135
|
-
))}
|
|
136
|
-
</div>
|
|
137
|
-
)}
|
|
138
|
-
</div>
|
|
139
|
-
|
|
140
|
-
{/* 地理区域 */}
|
|
141
|
-
<div className={css.card}>
|
|
142
|
-
<div className={css.row} style={{ justifyContent: 'space-between' }}>
|
|
143
|
-
<span className={css.cardTitle}>地理区域({draft.regions.length})</span>
|
|
144
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setRegions([...draft.regions, { name: '', description: '' }]) }}>
|
|
145
|
-
+ 新增区域
|
|
146
|
-
</button>
|
|
147
|
-
</div>
|
|
148
|
-
{draft.regions.length === 0 ? (
|
|
149
|
-
<span className={css.meta}>暂无地理区域 — 大陆 / 海域 / 秘境 / 遗迹…</span>
|
|
150
|
-
) : (
|
|
151
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
152
|
-
{draft.regions.map((region, i) => (
|
|
153
|
-
<EditableRow
|
|
154
|
-
key={i}
|
|
155
|
-
name={region.name}
|
|
156
|
-
detail={region.description}
|
|
157
|
-
onName={v => { const next = [...draft.regions]; next[i] = { ...next[i]!, name: v }; setRegions(next) }}
|
|
158
|
-
onDetail={v => { const next = [...draft.regions]; next[i] = { ...next[i]!, description: v }; setRegions(next) }}
|
|
159
|
-
onRemove={() => { setRegions(draft.regions.filter((_, idx) => idx !== i)) }}
|
|
160
|
-
namePlaceholder="区域名(如大荒 / 青云山脉)"
|
|
161
|
-
detailPlaceholder="描述…"
|
|
162
|
-
/>
|
|
163
|
-
))}
|
|
164
|
-
</div>
|
|
165
|
-
)}
|
|
166
|
-
</div>
|
|
167
|
-
|
|
168
|
-
{/* 势力分布 */}
|
|
169
|
-
<div className={css.card}>
|
|
170
|
-
<div className={css.row} style={{ justifyContent: 'space-between' }}>
|
|
171
|
-
<span className={css.cardTitle}>势力分布({draft.factions.length})</span>
|
|
172
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setFactions([...draft.factions, { name: '', kind: '宗门', description: '' }]) }}>
|
|
173
|
-
+ 新增势力
|
|
174
|
-
</button>
|
|
175
|
-
</div>
|
|
176
|
-
{draft.factions.length === 0 ? (
|
|
177
|
-
<span className={css.meta}>暂无势力 — 宗门 / 家族 / 王朝 / 组织…</span>
|
|
178
|
-
) : (
|
|
179
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
180
|
-
{draft.factions.map((faction, i) => (
|
|
181
|
-
<EditableRow
|
|
182
|
-
key={i}
|
|
183
|
-
name={`${faction.name}(${faction.kind})`}
|
|
184
|
-
detail={faction.description}
|
|
185
|
-
onName={v => {
|
|
186
|
-
const next = [...draft.factions]
|
|
187
|
-
const match = /^(.*)((.*))$/.exec(v)
|
|
188
|
-
next[i] = { ...next[i]!, name: (match?.[1] ?? v).trim(), kind: (match?.[2] ?? next[i]!.kind).trim() || '宗门' }
|
|
189
|
-
setFactions(next)
|
|
190
|
-
}}
|
|
191
|
-
onDetail={v => { const next = [...draft.factions]; next[i] = { ...next[i]!, description: v }; setFactions(next) }}
|
|
192
|
-
onRemove={() => { setFactions(draft.factions.filter((_, idx) => idx !== i)) }}
|
|
193
|
-
namePlaceholder="势力名(类型)如:青云宗(宗门)"
|
|
194
|
-
detailPlaceholder="描述…"
|
|
195
|
-
/>
|
|
196
|
-
))}
|
|
197
|
-
</div>
|
|
198
|
-
)}
|
|
199
|
-
</div>
|
|
200
|
-
</div>
|
|
201
|
-
)
|
|
202
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 大世界页签:境界体系 / 地理区域 / 势力分布 的结构化编辑 + 提炼。
|
|
3
|
+
* 数据注入每章生成与审稿提示词(renderWorld),保证设定不写飞。
|
|
4
|
+
*/
|
|
5
|
+
import { useEffect, useState } from 'react'
|
|
6
|
+
import type { NovelApi } from '../api.ts'
|
|
7
|
+
import type { WorldFaction, WorldRealm, WorldRegion, WorldState } from '../../protocol.ts'
|
|
8
|
+
import css from './panel.module.css'
|
|
9
|
+
|
|
10
|
+
const EMPTY: WorldState = { realms: [], regions: [], factions: [] }
|
|
11
|
+
|
|
12
|
+
/** 一个可编辑条目行。 */
|
|
13
|
+
function EditableRow({
|
|
14
|
+
name,
|
|
15
|
+
detail,
|
|
16
|
+
onName,
|
|
17
|
+
onDetail,
|
|
18
|
+
onRemove,
|
|
19
|
+
namePlaceholder,
|
|
20
|
+
detailPlaceholder,
|
|
21
|
+
}: {
|
|
22
|
+
name: string
|
|
23
|
+
detail: string
|
|
24
|
+
onName: (v: string) => void
|
|
25
|
+
onDetail: (v: string) => void
|
|
26
|
+
onRemove: () => void
|
|
27
|
+
namePlaceholder: string
|
|
28
|
+
detailPlaceholder: string
|
|
29
|
+
}) {
|
|
30
|
+
return (
|
|
31
|
+
<div style={{ display: 'flex', gap: 'var(--nf-space-6)', alignItems: 'flex-start' }}>
|
|
32
|
+
<input className={css.input} style={{ flex: 2, minWidth: 0 }} placeholder={namePlaceholder} value={name} onChange={e => { onName(e.target.value) }} />
|
|
33
|
+
<input className={css.input} style={{ flex: 3, minWidth: 0 }} placeholder={detailPlaceholder} value={detail} onChange={e => { onDetail(e.target.value) }} />
|
|
34
|
+
<button type="button" className={`${css.button} ${css.buttonSmall}`} title="删除" onClick={onRemove}>×</button>
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 大世界页签。 */
|
|
40
|
+
export function WorldTab({
|
|
41
|
+
api,
|
|
42
|
+
world,
|
|
43
|
+
onChanged,
|
|
44
|
+
}: {
|
|
45
|
+
api: NovelApi
|
|
46
|
+
/** 当前项目的大世界数据(可能为空)。 */
|
|
47
|
+
world: WorldState | undefined
|
|
48
|
+
/** 保存/提炼成功后由父组件刷新项目状态。 */
|
|
49
|
+
onChanged: (world: WorldState) => void
|
|
50
|
+
}) {
|
|
51
|
+
const [draft, setDraft] = useState<WorldState>(() => world ?? EMPTY)
|
|
52
|
+
const [busy, setBusy] = useState(false)
|
|
53
|
+
const [error, setError] = useState('')
|
|
54
|
+
|
|
55
|
+
// 外部数据变化时同步草稿(提炼/保存/切换书后)。
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
setDraft(world ?? EMPTY)
|
|
58
|
+
}, [world])
|
|
59
|
+
|
|
60
|
+
const handleGenerate = async (): Promise<void> => {
|
|
61
|
+
setBusy(true)
|
|
62
|
+
setError('')
|
|
63
|
+
try {
|
|
64
|
+
const result = await api.world('generate')
|
|
65
|
+
setDraft(result.world)
|
|
66
|
+
onChanged(result.world)
|
|
67
|
+
} catch (err) {
|
|
68
|
+
setError((err as Error).message)
|
|
69
|
+
} finally {
|
|
70
|
+
setBusy(false)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const handleSave = async (): Promise<void> => {
|
|
75
|
+
setBusy(true)
|
|
76
|
+
setError('')
|
|
77
|
+
try {
|
|
78
|
+
const result = await api.world('save', draft)
|
|
79
|
+
onChanged(result.world)
|
|
80
|
+
} catch (err) {
|
|
81
|
+
setError((err as Error).message)
|
|
82
|
+
} finally {
|
|
83
|
+
setBusy(false)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const setRealms = (realms: WorldRealm[]): void => setDraft(prev => ({ ...prev, realms }))
|
|
88
|
+
const setRegions = (regions: WorldRegion[]): void => setDraft(prev => ({ ...prev, regions }))
|
|
89
|
+
const setFactions = (factions: WorldFaction[]): void => setDraft(prev => ({ ...prev, factions }))
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-12)' }}>
|
|
93
|
+
{error !== '' && <div className={css.card} style={{ borderColor: 'var(--nf-error)' }}><span style={{ color: 'var(--nf-error)' }}>{error}</span></div>}
|
|
94
|
+
|
|
95
|
+
<div className={css.card}>
|
|
96
|
+
<div className={css.row} style={{ justifyContent: 'space-between' }}>
|
|
97
|
+
<span className={css.cardTitle}>大世界</span>
|
|
98
|
+
<div className={css.row}>
|
|
99
|
+
<button type="button" className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void handleGenerate() }}>
|
|
100
|
+
提炼
|
|
101
|
+
</button>
|
|
102
|
+
<button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={busy} onClick={() => { void handleSave() }}>
|
|
103
|
+
保存
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
<span className={css.meta}>
|
|
108
|
+
境界体系按由低到高排序注入章节生成提示词,模型不得随意跳级或自创境界;区域与势力约束地理/势力设定。提炼不满意可逐条编辑后保存。
|
|
109
|
+
</span>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{/* 境界体系 */}
|
|
113
|
+
<div className={css.card}>
|
|
114
|
+
<div className={css.row} style={{ justifyContent: 'space-between' }}>
|
|
115
|
+
<span className={css.cardTitle}>境界体系({draft.realms.length})</span>
|
|
116
|
+
<button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setRealms([...draft.realms, { name: '', description: '' }]) }}>
|
|
117
|
+
+ 新增境界
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
{draft.realms.length === 0 ? (
|
|
121
|
+
<span className={css.meta}>暂无境界体系 — 点击 提炼 或手动添加(由低到高)。</span>
|
|
122
|
+
) : (
|
|
123
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)' }}>
|
|
124
|
+
{draft.realms.map((realm, i) => (
|
|
125
|
+
<EditableRow
|
|
126
|
+
key={i}
|
|
127
|
+
name={realm.name}
|
|
128
|
+
detail={realm.description}
|
|
129
|
+
onName={v => { const next = [...draft.realms]; next[i] = { ...next[i]!, name: v }; setRealms(next) }}
|
|
130
|
+
onDetail={v => { const next = [...draft.realms]; next[i] = { ...next[i]!, description: v }; setRealms(next) }}
|
|
131
|
+
onRemove={() => { setRealms(draft.realms.filter((_, idx) => idx !== i)) }}
|
|
132
|
+
namePlaceholder={`第 ${i + 1} 阶境界名`}
|
|
133
|
+
detailPlaceholder="突破条件 / 寿命 / 标志…"
|
|
134
|
+
/>
|
|
135
|
+
))}
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
{/* 地理区域 */}
|
|
141
|
+
<div className={css.card}>
|
|
142
|
+
<div className={css.row} style={{ justifyContent: 'space-between' }}>
|
|
143
|
+
<span className={css.cardTitle}>地理区域({draft.regions.length})</span>
|
|
144
|
+
<button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setRegions([...draft.regions, { name: '', description: '' }]) }}>
|
|
145
|
+
+ 新增区域
|
|
146
|
+
</button>
|
|
147
|
+
</div>
|
|
148
|
+
{draft.regions.length === 0 ? (
|
|
149
|
+
<span className={css.meta}>暂无地理区域 — 大陆 / 海域 / 秘境 / 遗迹…</span>
|
|
150
|
+
) : (
|
|
151
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)' }}>
|
|
152
|
+
{draft.regions.map((region, i) => (
|
|
153
|
+
<EditableRow
|
|
154
|
+
key={i}
|
|
155
|
+
name={region.name}
|
|
156
|
+
detail={region.description}
|
|
157
|
+
onName={v => { const next = [...draft.regions]; next[i] = { ...next[i]!, name: v }; setRegions(next) }}
|
|
158
|
+
onDetail={v => { const next = [...draft.regions]; next[i] = { ...next[i]!, description: v }; setRegions(next) }}
|
|
159
|
+
onRemove={() => { setRegions(draft.regions.filter((_, idx) => idx !== i)) }}
|
|
160
|
+
namePlaceholder="区域名(如大荒 / 青云山脉)"
|
|
161
|
+
detailPlaceholder="描述…"
|
|
162
|
+
/>
|
|
163
|
+
))}
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
{/* 势力分布 */}
|
|
169
|
+
<div className={css.card}>
|
|
170
|
+
<div className={css.row} style={{ justifyContent: 'space-between' }}>
|
|
171
|
+
<span className={css.cardTitle}>势力分布({draft.factions.length})</span>
|
|
172
|
+
<button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setFactions([...draft.factions, { name: '', kind: '宗门', description: '' }]) }}>
|
|
173
|
+
+ 新增势力
|
|
174
|
+
</button>
|
|
175
|
+
</div>
|
|
176
|
+
{draft.factions.length === 0 ? (
|
|
177
|
+
<span className={css.meta}>暂无势力 — 宗门 / 家族 / 王朝 / 组织…</span>
|
|
178
|
+
) : (
|
|
179
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)' }}>
|
|
180
|
+
{draft.factions.map((faction, i) => (
|
|
181
|
+
<EditableRow
|
|
182
|
+
key={i}
|
|
183
|
+
name={`${faction.name}(${faction.kind})`}
|
|
184
|
+
detail={faction.description}
|
|
185
|
+
onName={v => {
|
|
186
|
+
const next = [...draft.factions]
|
|
187
|
+
const match = /^(.*)((.*))$/.exec(v)
|
|
188
|
+
next[i] = { ...next[i]!, name: (match?.[1] ?? v).trim(), kind: (match?.[2] ?? next[i]!.kind).trim() || '宗门' }
|
|
189
|
+
setFactions(next)
|
|
190
|
+
}}
|
|
191
|
+
onDetail={v => { const next = [...draft.factions]; next[i] = { ...next[i]!, description: v }; setFactions(next) }}
|
|
192
|
+
onRemove={() => { setFactions(draft.factions.filter((_, idx) => idx !== i)) }}
|
|
193
|
+
namePlaceholder="势力名(类型)如:青云宗(宗门)"
|
|
194
|
+
detailPlaceholder="描述…"
|
|
195
|
+
/>
|
|
196
|
+
))}
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
)
|
|
202
|
+
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tiny translation helper for the panel
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Tiny translation helper for the panel.
|
|
3
|
+
*
|
|
4
|
+
* 面板是中文单语界面:文案只在 `locales.ts` 定义一次,这里按类型化键位取值
|
|
5
|
+
* (键位不匹配会在编译期报错),保持客户端 bundle 自包含、不引入运行时语言服务。
|
|
6
|
+
* 需要多语言时再接入 DSH 的 locale 服务,届时只需替换本函数实现。
|
|
5
7
|
*/
|
|
6
|
-
import { zh,
|
|
8
|
+
import { zh, type NovelKey } from '../locales.ts'
|
|
7
9
|
|
|
8
10
|
/** Translate one key with optional {placeholder} substitution. */
|
|
9
11
|
export function tt(key: NovelKey, params?: Record<string, string | number>): string {
|
|
10
|
-
let text: string = zh[key] ??
|
|
12
|
+
let text: string = zh[key] ?? key
|
|
11
13
|
if (params !== undefined) {
|
|
12
14
|
for (const [name, value] of Object.entries(params)) {
|
|
13
15
|
text = text.replaceAll(`{${name}}`, String(value))
|