@waterwx/dsh-novel-forge 2.0.0-alpha.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +286 -285
- package/lib/client.js +19561 -19439
- package/lib/index.js +8159 -4456
- package/lib/types/action-guard.d.ts +61 -0
- package/lib/types/ai-scan.d.ts +0 -6
- package/lib/types/assets.d.ts +20 -9
- package/lib/types/assistant.d.ts +29 -0
- package/lib/types/author-assets.d.ts +1 -3
- package/lib/types/bookshelf.d.ts +13 -3
- package/lib/types/client/api.d.ts +136 -27
- package/lib/types/client/chip-prefs.d.ts +36 -0
- package/lib/types/client/docx.d.ts +2 -2
- package/lib/types/client/llmLive.d.ts +31 -0
- package/lib/types/client/locales.d.ts +15 -70
- package/lib/types/client/mount.d.ts +0 -2
- 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/AuthorHome.d.ts +13 -11
- 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/IdeaInspirationView.d.ts +6 -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 +2 -2
- 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/PromptSlotsView.d.ts +11 -0
- package/lib/types/client/panel/SettingsView.d.ts +12 -9
- package/lib/types/client/panel/ShelfView.d.ts +10 -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/market-radar-state.d.ts +56 -0
- package/lib/types/client/panel/toast.d.ts +2 -0
- package/lib/types/client/panel/views.d.ts +21 -35
- package/lib/types/client/run-status.d.ts +21 -0
- package/lib/types/client/sidebar-entry.d.ts +0 -2
- package/lib/types/docx-core.d.ts +16 -0
- package/lib/types/docx.d.ts +2 -3
- package/lib/types/engine.d.ts +78 -103
- package/lib/types/global-assets.d.ts +6 -0
- package/lib/types/home.d.ts +11 -0
- package/lib/types/index.d.ts +26 -12
- package/lib/types/llm-live.d.ts +70 -0
- package/lib/types/llm-retry.d.ts +38 -0
- package/lib/types/market-radar-scan.d.ts +18 -0
- package/lib/types/market-radar-sources.d.ts +31 -0
- package/lib/types/migrate-output.d.ts +25 -0
- package/lib/types/model-capability.d.ts +93 -0
- package/lib/types/output-budget.d.ts +86 -0
- package/lib/types/prompt-slots.d.ts +57 -0
- package/lib/types/prompting.d.ts +10 -0
- package/lib/types/protocol.d.ts +816 -601
- package/lib/types/revision.d.ts +76 -0
- package/lib/types/routes.d.ts +9 -3
- package/lib/types/run.d.ts +23 -1
- package/lib/types/settings-persist.d.ts +59 -0
- package/lib/types/snapshots.d.ts +46 -0
- package/lib/types/stage-contract.d.ts +58 -0
- package/lib/types/tension.d.ts +53 -0
- package/lib/types/timeline.d.ts +37 -0
- package/package.json +50 -25
- package/src/action-guard.ts +127 -0
- package/src/ai-scan.ts +0 -24
- package/src/assets.ts +1801 -879
- package/src/assistant.ts +1098 -770
- package/src/author-assets.ts +131 -147
- package/src/bookshelf.ts +193 -167
- package/src/client/api.ts +767 -617
- package/src/client/chip-prefs.ts +121 -0
- package/src/client/docx.ts +32 -69
- package/src/client/index.ts +2 -0
- package/src/client/llmLive.ts +111 -0
- package/src/client/locales.ts +15 -274
- package/src/client/mount.tsx +3 -2
- package/src/client/panel/AdaptModeView.tsx +535 -520
- 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 +205 -207
- package/src/client/panel/AuthorHome.tsx +309 -204
- package/src/client/panel/BookAnalysisView.tsx +63 -0
- package/src/client/panel/CmdPalette.tsx +102 -0
- package/src/client/panel/CreateBookView.tsx +366 -352
- package/src/client/panel/DirectorView.tsx +200 -0
- package/src/client/panel/GlobalAssetLibraryView.tsx +168 -172
- package/src/client/panel/IdeaInspirationView.tsx +76 -0
- package/src/client/panel/ImportModal.tsx +302 -302
- package/src/client/panel/KnowledgeBaseView.tsx +125 -0
- package/src/client/panel/LiveFeedLog.tsx +204 -0
- package/src/client/panel/MarketRadarView.tsx +415 -0
- package/src/client/panel/ModelManager.tsx +205 -205
- package/src/client/panel/NovelPanel.tsx +3144 -2205
- package/src/client/panel/PageHeader.tsx +37 -0
- package/src/client/panel/ProgressConsole.tsx +63 -64
- package/src/client/panel/PromptSlotsView.tsx +97 -0
- package/src/client/panel/ReaderView.tsx +306 -306
- package/src/client/panel/RunPanel.tsx +56 -24
- package/src/client/panel/SettingsView.tsx +175 -106
- package/src/client/panel/ShelfView.tsx +63 -39
- 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/market-radar-state.ts +79 -0
- package/src/client/panel/panel.module.css +5767 -4360
- package/src/client/panel/toast.ts +51 -0
- package/src/client/panel/views.tsx +316 -314
- package/src/client/run-status.ts +171 -0
- package/src/client/sidebar-entry.ts +3 -3
- package/src/docx-core.ts +53 -0
- package/src/docx.ts +44 -83
- package/src/engine.ts +1197 -1638
- package/src/global-assets.ts +58 -0
- package/src/home.ts +33 -0
- package/src/index.ts +181 -101
- package/src/llm-live.ts +245 -0
- package/src/llm-retry.ts +77 -0
- package/src/market-radar-scan.ts +35 -0
- package/src/market-radar-sources.ts +173 -0
- package/src/migrate-output.ts +76 -0
- package/src/model-capability.ts +215 -0
- package/src/novel-context.ts +286 -287
- package/src/output-budget.ts +147 -0
- package/src/prompt-slots.ts +125 -0
- package/src/prompting.ts +26 -0
- package/src/protocol.ts +835 -614
- package/src/revision.ts +283 -0
- package/src/routes.ts +1286 -910
- package/src/run.ts +113 -17
- package/src/settings-persist.ts +114 -0
- package/src/snapshots.ts +174 -0
- package/src/stage-contract.ts +186 -0
- package/src/tension.ts +251 -0
- package/src/timeline.ts +159 -0
- package/assets/styles/arcane-thick-3d.webp +0 -0
- package/assets/styles/chinese-ink.webp +0 -0
- package/assets/styles/clay-stopmotion-3d.webp +0 -0
- package/assets/styles/dark-ghibli.webp +0 -0
- package/assets/styles/dark-gothic-3d.webp +0 -0
- package/assets/styles/film-photo.webp +0 -0
- package/assets/styles/guofeng-heavy.webp +0 -0
- package/assets/styles/guoman-2d-gufeng.webp +0 -0
- package/assets/styles/guoman-3d-tianyuan.webp +0 -0
- package/assets/styles/guoman-3d-xuanhuan.webp +0 -0
- package/assets/styles/horror-game-3d.webp +0 -0
- package/assets/styles/hyperreal-3d.webp +0 -0
- package/assets/styles/japan-cel-dark.webp +0 -0
- package/assets/styles/korean-webtoon.webp +0 -0
- package/assets/styles/low-cyberpunk-3d.webp +0 -0
- package/assets/styles/nextgen-cg.webp +0 -0
- package/assets/styles/noir-film.webp +0 -0
- package/assets/styles/openworld-survival.webp +0 -0
- package/assets/styles/paper-cut-3d.webp +0 -0
- package/assets/styles/pixar-adult-3d.webp +0 -0
- package/assets/styles/pop-marvel-2d.webp +0 -0
- package/assets/styles/sandiao-qban.webp +0 -0
- package/assets/styles/shinkai-anime.webp +0 -0
- package/assets/styles/tactical-stealth.webp +0 -0
- package/assets/styles/urban-lightreal-3d.webp +0 -0
- package/assets/styles/vaporwave-3d.webp +0 -0
- package/assets/styles/vhs90.webp +0 -0
- package/lib/types/client/panel/BookshelfBar.d.ts +0 -11
- package/lib/types/client/panel/FlowGuide.d.ts +0 -22
- package/lib/types/client/panel/MangaRoleLibrary.d.ts +0 -20
- package/lib/types/client/panel/MangaWorkspace.d.ts +0 -12
- package/lib/types/client/panel/ModelPicker.d.ts +0 -11
- package/lib/types/client/panel/ProgressHomeView.d.ts +0 -5
- package/lib/types/client/panel/PropLibrary.d.ts +0 -8
- package/lib/types/client/panel/SceneLibrary.d.ts +0 -16
- package/lib/types/client/panel/StoryboardTab.d.ts +0 -19
- package/lib/types/client/panel/StyleCard.d.ts +0 -8
- package/lib/types/client/panel/StyleGallery.d.ts +0 -1
- package/lib/types/comic-presets.d.ts +0 -12
- package/lib/types/manga-genre-rules.d.ts +0 -22
- package/lib/types/prompt-utils.d.ts +0 -35
- package/lib/types/review-policy.d.ts +0 -97
- package/lib/types/shot-language.d.ts +0 -46
- package/lib/types/story-beat-language.d.ts +0 -35
- package/lib/types/style-library.d.ts +0 -33
- package/skills/chapter-batch/SKILL.md +0 -81
- package/src/client/mount.tsx._new +0 -97
- package/src/client/mount.tsx._nf +0 -97
- package/src/client/mount.tsx._tmp +0 -97
- package/src/client/panel/BookshelfBar.tsx +0 -170
- package/src/client/panel/FlowGuide.tsx +0 -84
- package/src/client/panel/MangaRoleLibrary.tsx +0 -1002
- package/src/client/panel/MangaWorkspace.tsx +0 -715
- package/src/client/panel/ProgressHomeView.tsx +0 -87
- package/src/client/panel/PropLibrary.tsx +0 -175
- package/src/client/panel/SceneLibrary.tsx +0 -453
- package/src/client/panel/StoryboardTab.tsx +0 -472
- package/src/client/panel/StyleCard.tsx +0 -90
- package/src/client/panel/StyleGallery.tsx +0 -58
- package/src/comic-presets.ts +0 -67
- package/src/manga-genre-rules.ts +0 -87
- package/src/prompt-utils.ts +0 -73
- package/src/review-policy.ts +0 -161
- package/src/shot-language.ts +0 -181
- package/src/story-beat-language.ts +0 -130
- package/src/style-library.ts +0 -84
|
@@ -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: '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}>暂无境界体系 — 点击
|
|
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
|
+
/**
|
|
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))
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 热门题材雷达的界面状态规则。
|
|
3
|
+
*
|
|
4
|
+
* 职责边界:本文件只回答「界面上此刻能不能点」,不持有任何数据。抽成纯函数的
|
|
5
|
+
* 理由是它可以离线测——组件渲染不好测,判定逻辑好测。
|
|
6
|
+
*
|
|
7
|
+
* 下面先用**独立表述的验收条件**写清楚要什么,再由两个函数实现;这样即使日后
|
|
8
|
+
* 需求变化,改动方向也是照着条件走,而不是照着某个外部实现的形状走。
|
|
9
|
+
*
|
|
10
|
+
* R1 扫榜或分析进行中,不得改动作品勾选(否则分析输入与界面不一致);
|
|
11
|
+
* R2 未勾选任何作品时不能开始分析;
|
|
12
|
+
* R3 已经有报告之后,仍允许改选作品并**再次**分析 —— 不得退化成"只能查看";
|
|
13
|
+
* R4 信号勾选只在确实换了一份报告时复位;同一份报告的重渲染不得冲掉作者手选。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** 雷达三个入口此刻的可用性,以及分析按钮该显示哪种文案。 */
|
|
17
|
+
export interface RadarControls {
|
|
18
|
+
/** 扫榜按钮是否可点。 */
|
|
19
|
+
canScan: boolean
|
|
20
|
+
/** 作品勾选(含全选 / 清空)是否可改。 */
|
|
21
|
+
canSelectCandidates: boolean
|
|
22
|
+
/** 分析按钮是否可点。 */
|
|
23
|
+
canAnalyze: boolean
|
|
24
|
+
/**
|
|
25
|
+
* 分析按钮的文案标识:
|
|
26
|
+
* - `wait`:还在扫榜,先等榜单;
|
|
27
|
+
* - `pick`:还没勾选作品;
|
|
28
|
+
* - `first`:首次分析;
|
|
29
|
+
* - `again`:已有报告,这次是再出一份。
|
|
30
|
+
*/
|
|
31
|
+
analyzeLabel: 'wait' | 'pick' | 'first' | 'again'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 实现 R1-R3:判定三个入口的可用性。
|
|
36
|
+
* @param state - 当前界面状态。
|
|
37
|
+
* @returns 各入口可用性与分析按钮文案标识。
|
|
38
|
+
*/
|
|
39
|
+
export function radarControls(state: {
|
|
40
|
+
/** 正在抓榜单。 */
|
|
41
|
+
scanning: boolean
|
|
42
|
+
/** 正在做 AI 分析。 */
|
|
43
|
+
analyzing: boolean
|
|
44
|
+
/** 已勾选的作品数。 */
|
|
45
|
+
pickedCount: number
|
|
46
|
+
/** 界面上是否已有可查看的报告。 */
|
|
47
|
+
hasReport: boolean
|
|
48
|
+
}): RadarControls {
|
|
49
|
+
if (state.analyzing) {
|
|
50
|
+
// 分析中:三个入口都锁住(R1),按钮文案沿用上一次的状态不必区分。
|
|
51
|
+
return { canScan: false, canSelectCandidates: false, canAnalyze: false, analyzeLabel: 'first' }
|
|
52
|
+
}
|
|
53
|
+
if (state.scanning) {
|
|
54
|
+
return { canScan: false, canSelectCandidates: false, canAnalyze: false, analyzeLabel: 'wait' }
|
|
55
|
+
}
|
|
56
|
+
if (state.pickedCount === 0) {
|
|
57
|
+
return { canScan: true, canSelectCandidates: true, canAnalyze: false, analyzeLabel: 'pick' }
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
canScan: true,
|
|
61
|
+
canSelectCandidates: true,
|
|
62
|
+
canAnalyze: true,
|
|
63
|
+
// R3:有报告也只是换个文案,**不禁用**再分析。
|
|
64
|
+
analyzeLabel: state.hasReport ? 'again' : 'first',
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 实现 R4:本次报告是否**不是**勾选当前所依据的那一份。
|
|
70
|
+
*
|
|
71
|
+
* 传入的是上一次已经据以复位过的报告标识:相同 → 不动作者勾选;不同 → 勾选所依据
|
|
72
|
+
* 的信号集合已经变了,需要按新报告重新给出推荐勾选。
|
|
73
|
+
* @param selectionBasis - 勾选当前依据的报告标识(空串表示尚未复位过)。
|
|
74
|
+
* @param incoming - 本次拿到的报告标识(空串表示旧版产物没有该标识)。
|
|
75
|
+
* @returns 需要按新报告重置勾选时为 true。
|
|
76
|
+
*/
|
|
77
|
+
export function isDifferentReport(selectionBasis: string, incoming: string): boolean {
|
|
78
|
+
return selectionBasis !== incoming
|
|
79
|
+
}
|