@waterwx/dsh-novel-forge 2.0.1-alpha → 2.1.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 +368 -362
- package/lib/client.js +20340 -19622
- package/lib/index.js +11313 -7285
- 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/outline-markdown.d.ts +112 -0
- package/lib/types/client/panel/status-badge.d.ts +13 -0
- package/lib/types/client/panel/storage-parse.d.ts +46 -0
- package/lib/types/client/panel/toast.d.ts +2 -0
- package/lib/types/client/panel/usePersistentState.d.ts +17 -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/digest-parse.d.ts +29 -0
- package/lib/types/docx-core.d.ts +16 -0
- package/lib/types/docx.d.ts +2 -3
- package/lib/types/engine.d.ts +150 -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 +119 -0
- package/lib/types/model-request.d.ts +36 -0
- package/lib/types/output-budget.d.ts +86 -0
- package/lib/types/plotline-align.d.ts +35 -0
- package/lib/types/prompt-slots.d.ts +57 -0
- package/lib/types/prompting.d.ts +10 -0
- package/lib/types/protocol.d.ts +841 -601
- package/lib/types/revision.d.ts +76 -0
- package/lib/types/routes.d.ts +9 -3
- package/lib/types/run-selection.d.ts +51 -0
- package/lib/types/run.d.ts +32 -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/stream-fallback.d.ts +60 -0
- package/lib/types/strip-headings.d.ts +33 -0
- package/lib/types/tension.d.ts +53 -0
- package/lib/types/timeline.d.ts +37 -0
- package/package.json +50 -27
- 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/ai-scan.ts +0 -179
- package/src/assets.ts +0 -879
- package/src/assistant.ts +0 -770
- package/src/author-assets.ts +0 -147
- package/src/bookshelf.ts +0 -167
- package/src/client/api.ts +0 -617
- package/src/client/css-modules.d.ts +0 -8
- package/src/client/docx.ts +0 -69
- package/src/client/index.ts +0 -34
- package/src/client/locales.ts +0 -418
- package/src/client/mount.tsx +0 -97
- 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/AdaptModeView.tsx +0 -520
- package/src/client/panel/AssetsTab.tsx +0 -458
- package/src/client/panel/AssistantTab.tsx +0 -301
- package/src/client/panel/AuthorAssetsView.tsx +0 -208
- package/src/client/panel/AuthorHome.tsx +0 -204
- package/src/client/panel/BookshelfBar.tsx +0 -170
- package/src/client/panel/CreateBookView.tsx +0 -352
- package/src/client/panel/FlowGuide.tsx +0 -84
- package/src/client/panel/GlobalAssetLibraryView.tsx +0 -172
- package/src/client/panel/ImportModal.tsx +0 -303
- package/src/client/panel/MangaRoleLibrary.tsx +0 -1002
- package/src/client/panel/MangaWorkspace.tsx +0 -715
- package/src/client/panel/ModelManager.tsx +0 -205
- package/src/client/panel/NovelPanel.tsx +0 -4620
- package/src/client/panel/ProgressConsole.tsx +0 -64
- package/src/client/panel/ProgressHomeView.tsx +0 -87
- package/src/client/panel/PropLibrary.tsx +0 -175
- package/src/client/panel/ReaderView.tsx +0 -306
- package/src/client/panel/ReasoningSection.tsx +0 -31
- package/src/client/panel/RunPanel.tsx +0 -204
- package/src/client/panel/SceneLibrary.tsx +0 -453
- package/src/client/panel/SettingsView.tsx +0 -286
- package/src/client/panel/ShelfView.tsx +0 -242
- 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/client/panel/WorldTab.tsx +0 -202
- package/src/client/panel/controller.ts +0 -45
- package/src/client/panel/helpers.ts +0 -67
- package/src/client/panel/panel.module.css +0 -4360
- package/src/client/panel/views.tsx +0 -314
- package/src/client/sidebar-entry.ts +0 -122
- package/src/client/text.ts +0 -28
- package/src/comic-presets.ts +0 -67
- package/src/docx.ts +0 -83
- package/src/engine.ts +0 -5200
- package/src/index.ts +0 -329
- package/src/manga-genre-rules.ts +0 -87
- package/src/novel-context.ts +0 -287
- package/src/prompt-utils.ts +0 -73
- package/src/protocol.ts +0 -2233
- package/src/review-policy.ts +0 -161
- package/src/routes.ts +0 -3419
- package/src/run.ts +0 -331
- package/src/shot-language.ts +0 -181
- package/src/story-beat-language.ts +0 -130
- package/src/style-library.ts +0 -84
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 开书向导:独立页面视图 —— 书名 + 大纲(选择 docx / 拖拽 / 粘贴),
|
|
3
|
-
* 实时书名识别与题材提示,开书即建项目并进入工作台。
|
|
4
|
-
* 另含「想法 → AI 大纲」:输入一句话想法生成 2-3 个方案,可暂留换批,选中后回填大纲框。
|
|
5
|
-
*/
|
|
6
|
-
import { useRef, useState } from 'react'
|
|
7
|
-
import type { NovelApi } from '../api.ts'
|
|
8
|
-
import type { OutlineCandidate } from '../../protocol.ts'
|
|
9
|
-
import { extractDocxTextFromBuffer } from '../docx.ts'
|
|
10
|
-
import css from './panel.module.css'
|
|
11
|
-
|
|
12
|
-
/** 从大纲首行推断书名(与服务端 inferBookName 一致,供实时预览)。 */
|
|
13
|
-
function inferBookNamePreview(outline: string): string {
|
|
14
|
-
const line = outline.split('\n').map(l => l.trim()).find(l => l.length > 0)
|
|
15
|
-
if (line === undefined) return ''
|
|
16
|
-
return line.replace(/^《/, '').replace(/》.*$/, '').slice(0, 40)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** 简单题材识别(提示用)。 */
|
|
20
|
-
function guessGenre(outline: string): string | null {
|
|
21
|
-
const map: Array<[string, string[]]> = [
|
|
22
|
-
['仙侠修真', ['仙', '修', '灵根', '元婴', '宗门', '飞升']],
|
|
23
|
-
['都市', ['都市', '公司', '外卖', '职场', '总裁']],
|
|
24
|
-
['玄幻', ['斗气', '魂力', '大陆', '斗罗', '神']],
|
|
25
|
-
['悬疑', ['悬疑', '密室', '案件', '推理', '凶']],
|
|
26
|
-
['科幻', ['机甲', '星舰', 'AI', '未来', '星际']],
|
|
27
|
-
['历史', ['朝代', '皇帝', '将军', '古代', '王朝']],
|
|
28
|
-
['游戏', ['游戏', '副本', '装备', '等级', '职业']],
|
|
29
|
-
]
|
|
30
|
-
for (const [genre, keywords] of map) {
|
|
31
|
-
if (keywords.some(k => outline.includes(k))) return genre
|
|
32
|
-
}
|
|
33
|
-
return null
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** 开书向导页。 */
|
|
37
|
-
export function CreateBookView({
|
|
38
|
-
api,
|
|
39
|
-
onBack,
|
|
40
|
-
onCreated,
|
|
41
|
-
}: {
|
|
42
|
-
api: NovelApi
|
|
43
|
-
/** 返回书架。 */
|
|
44
|
-
onBack: () => void
|
|
45
|
-
/** 开书成功:进入新书工作台。 */
|
|
46
|
-
onCreated: (id: string) => void
|
|
47
|
-
}) {
|
|
48
|
-
const [name, setName] = useState('')
|
|
49
|
-
const [outlineText, setOutlineText] = useState('')
|
|
50
|
-
const [outlineName, setOutlineName] = useState('')
|
|
51
|
-
const [dragActive, setDragActive] = useState(false)
|
|
52
|
-
const [busy, setBusy] = useState(false)
|
|
53
|
-
const [error, setError] = useState('')
|
|
54
|
-
const outlineFileRef = useRef<HTMLInputElement | null>(null)
|
|
55
|
-
|
|
56
|
-
// ---- 想法 → AI 大纲状态 ----
|
|
57
|
-
const [ideaOpen, setIdeaOpen] = useState(false)
|
|
58
|
-
const [idea, setIdea] = useState('')
|
|
59
|
-
const [suggesting, setSuggesting] = useState(false)
|
|
60
|
-
const [candidates, setCandidates] = useState<OutlineCandidate[]>([])
|
|
61
|
-
const [pinned, setPinned] = useState<string[]>([])
|
|
62
|
-
/** 回填后提示(如「已填入方案A,可继续修改」)。 */
|
|
63
|
-
const [fillNotice, setFillNotice] = useState('')
|
|
64
|
-
|
|
65
|
-
const autoName = inferBookNamePreview(outlineText)
|
|
66
|
-
const effectiveName = name.trim() !== '' ? name.trim() : autoName
|
|
67
|
-
const genre = guessGenre(outlineText)
|
|
68
|
-
|
|
69
|
-
const handlePickOutlineFile = async (file: File | undefined): Promise<void> => {
|
|
70
|
-
if (file === undefined) return
|
|
71
|
-
try {
|
|
72
|
-
const buffer = await file.arrayBuffer()
|
|
73
|
-
const text = extractDocxTextFromBuffer(buffer)
|
|
74
|
-
if (text.length < 50) {
|
|
75
|
-
setError('大纲内容过短(<50 字符),请检查文件')
|
|
76
|
-
return
|
|
77
|
-
}
|
|
78
|
-
setOutlineText(text)
|
|
79
|
-
setOutlineName(file.name)
|
|
80
|
-
if (name.trim() === '') setName(inferBookNamePreview(text))
|
|
81
|
-
setError('')
|
|
82
|
-
} catch (err) {
|
|
83
|
-
setError(`读取大纲失败:${(err as Error).message}`)
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** 生成/换批:只补未暂留的空槽;exclude 传已暂留方案的卖点方向。 */
|
|
88
|
-
const handleSuggest = async (): Promise<void> => {
|
|
89
|
-
if (idea.trim().length < 10) {
|
|
90
|
-
setError('想法太短(<10 字):至少写一句完整想法,如「男主穿越修仙界靠做菜无敌」')
|
|
91
|
-
return
|
|
92
|
-
}
|
|
93
|
-
setSuggesting(true)
|
|
94
|
-
setError('')
|
|
95
|
-
try {
|
|
96
|
-
const exclude = candidates
|
|
97
|
-
.filter(c => pinned.includes(c.id))
|
|
98
|
-
.map(c => `${c.bookName}:${c.sellingPoint}${c.genre !== '' ? `(${c.genre})` : ''}`)
|
|
99
|
-
const count = Math.max(1, 3 - pinned.length)
|
|
100
|
-
const result = await api.outlineSuggest(idea.trim(), count, exclude)
|
|
101
|
-
// 换批合并:已暂留的原地保留,新生成的补充进空槽。
|
|
102
|
-
setCandidates(prev => {
|
|
103
|
-
const kept = prev.filter(c => pinned.includes(c.id))
|
|
104
|
-
return [...kept, ...result.candidates].slice(0, 3)
|
|
105
|
-
})
|
|
106
|
-
setFillNotice('')
|
|
107
|
-
} catch (err) {
|
|
108
|
-
setError((err as Error).message)
|
|
109
|
-
} finally {
|
|
110
|
-
setSuggesting(false)
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** 暂留/取消暂留。 */
|
|
115
|
-
const togglePin = (id: string): void => {
|
|
116
|
-
setPinned(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id])
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** 选中方案:回填大纲框 + 书名,滚动到上方。 */
|
|
120
|
-
const handlePick = (candidate: OutlineCandidate): void => {
|
|
121
|
-
setOutlineText(candidate.outline)
|
|
122
|
-
setName(candidate.bookName)
|
|
123
|
-
setFillNotice(`已填入方案《${candidate.bookName}》(${candidate.genre}),可继续修改后开书`)
|
|
124
|
-
setIdeaOpen(false)
|
|
125
|
-
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const handleCreate = async (): Promise<void> => {
|
|
129
|
-
if (effectiveName === '') {
|
|
130
|
-
setError('请填写书名,或提供大纲自动识别')
|
|
131
|
-
return
|
|
132
|
-
}
|
|
133
|
-
setBusy(true)
|
|
134
|
-
setError('')
|
|
135
|
-
try {
|
|
136
|
-
const snapshot = await api.bookCreate(effectiveName, undefined, outlineText.trim() !== '' ? outlineText.trim() : undefined)
|
|
137
|
-
const created = snapshot.books.find(b => b.id === snapshot.activeBookId)
|
|
138
|
-
if (created !== undefined) {
|
|
139
|
-
onCreated(created.id)
|
|
140
|
-
} else {
|
|
141
|
-
setError('开书失败:未找到新书')
|
|
142
|
-
}
|
|
143
|
-
} catch (err) {
|
|
144
|
-
setError((err as Error).message)
|
|
145
|
-
} finally {
|
|
146
|
-
setBusy(false)
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return (
|
|
151
|
-
<div className={css.createBookView}>
|
|
152
|
-
<div className={css.createBookTop}>
|
|
153
|
-
<button type="button" className={css.iconButton} title="返回书架" aria-label="返回书架" onClick={onBack}>
|
|
154
|
-
← 书架
|
|
155
|
-
</button>
|
|
156
|
-
</div>
|
|
157
|
-
|
|
158
|
-
<div className={css.createBookCard}>
|
|
159
|
-
<span className={css.createBookIcon}>✒️</span>
|
|
160
|
-
<h2 className={css.createBookTitle}>开书向导</h2>
|
|
161
|
-
<span className={css.meta}>把一份大纲「编译」成一本完整的小说</span>
|
|
162
|
-
|
|
163
|
-
{error !== '' && (
|
|
164
|
-
<div className={css.card} style={{ borderColor: 'var(--nf-error)', padding: 'var(--nf-space-8) var(--nf-space-12)' }}>
|
|
165
|
-
<span style={{ color: 'var(--nf-error)', fontSize: 'var(--nf-fs-12)' }}>{error}</span>
|
|
166
|
-
</div>
|
|
167
|
-
)}
|
|
168
|
-
|
|
169
|
-
{fillNotice !== '' && (
|
|
170
|
-
<div className={css.card} style={{ borderColor: 'var(--nf-success)', padding: 'var(--nf-space-8) var(--nf-space-12)' }}>
|
|
171
|
-
<span style={{ color: 'var(--nf-success)', fontSize: 'var(--nf-fs-12)' }}>✅ {fillNotice}</span>
|
|
172
|
-
</div>
|
|
173
|
-
)}
|
|
174
|
-
|
|
175
|
-
<div className={css.field}>
|
|
176
|
-
<label className={css.fieldLabel}>书名</label>
|
|
177
|
-
<input
|
|
178
|
-
className={css.input}
|
|
179
|
-
placeholder={autoName !== '' ? `自动识别:${autoName}` : '输入书名(提供大纲后自动识别)'}
|
|
180
|
-
value={name}
|
|
181
|
-
onChange={e => { setName(e.target.value) }}
|
|
182
|
-
onKeyDown={e => { if (e.key === 'Enter') void handleCreate() }}
|
|
183
|
-
autoFocus
|
|
184
|
-
/>
|
|
185
|
-
</div>
|
|
186
|
-
|
|
187
|
-
<div className={css.field}>
|
|
188
|
-
<label className={css.fieldLabel}>大纲</label>
|
|
189
|
-
<div
|
|
190
|
-
className={`${css.dropzone} ${dragActive ? css.dropzoneActive : ''}`}
|
|
191
|
-
role="button"
|
|
192
|
-
tabIndex={0}
|
|
193
|
-
aria-label="选择或拖入 docx 大纲文件"
|
|
194
|
-
onClick={() => { outlineFileRef.current?.click() }}
|
|
195
|
-
onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); outlineFileRef.current?.click() } }}
|
|
196
|
-
onDragOver={e => { e.preventDefault(); setDragActive(true) }}
|
|
197
|
-
onDragLeave={() => { setDragActive(false) }}
|
|
198
|
-
onDrop={e => {
|
|
199
|
-
e.preventDefault()
|
|
200
|
-
setDragActive(false)
|
|
201
|
-
void handlePickOutlineFile(e.dataTransfer.files?.[0])
|
|
202
|
-
}}
|
|
203
|
-
>
|
|
204
|
-
<span className={css.dropzoneIcon}>📄</span>
|
|
205
|
-
<span>{outlineName !== '' ? `已选择:${outlineName}` : '点击选择 docx 大纲,或将文件拖到这里'}</span>
|
|
206
|
-
<span className={css.meta}>推荐提供大纲:开书即建立项目,书名自动识别</span>
|
|
207
|
-
<input
|
|
208
|
-
ref={outlineFileRef}
|
|
209
|
-
type="file"
|
|
210
|
-
accept=".docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
211
|
-
style={{ display: 'none' }}
|
|
212
|
-
onChange={e => {
|
|
213
|
-
void handlePickOutlineFile(e.target.files?.[0])
|
|
214
|
-
e.target.value = ''
|
|
215
|
-
}}
|
|
216
|
-
/>
|
|
217
|
-
</div>
|
|
218
|
-
<textarea
|
|
219
|
-
className={css.textarea}
|
|
220
|
-
style={{ minHeight: 130 }}
|
|
221
|
-
placeholder="或直接粘贴大纲文本(50 字以上)…"
|
|
222
|
-
value={outlineText}
|
|
223
|
-
onChange={e => { setOutlineText(e.target.value) }}
|
|
224
|
-
spellCheck={false}
|
|
225
|
-
/>
|
|
226
|
-
</div>
|
|
227
|
-
|
|
228
|
-
{(outlineText.trim().length > 0 || effectiveName !== '') && (
|
|
229
|
-
<div className={css.row} style={{ flexWrap: 'wrap' }}>
|
|
230
|
-
{outlineText.trim().length > 0 && <span className={css.meta}>大纲 {outlineText.length} 字</span>}
|
|
231
|
-
{effectiveName !== '' && <span className={css.meta}>书名:{effectiveName}</span>}
|
|
232
|
-
{genre !== null && <span className={css.meta}>题材:{genre}</span>}
|
|
233
|
-
</div>
|
|
234
|
-
)}
|
|
235
|
-
|
|
236
|
-
{/* 想法 → AI 大纲(可折叠) */}
|
|
237
|
-
<div className={css.ideaCard}>
|
|
238
|
-
<button
|
|
239
|
-
type="button"
|
|
240
|
-
className={css.ideaToggle}
|
|
241
|
-
onClick={() => { setIdeaOpen(v => !v) }}
|
|
242
|
-
aria-expanded={ideaOpen}
|
|
243
|
-
>
|
|
244
|
-
✨ {ideaOpen ? '▾' : '▸'} 没有大纲?用一句话想法让 AI 生成
|
|
245
|
-
</button>
|
|
246
|
-
{ideaOpen && (
|
|
247
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-8)' }}>
|
|
248
|
-
<textarea
|
|
249
|
-
className={css.textarea}
|
|
250
|
-
style={{ minHeight: 60 }}
|
|
251
|
-
placeholder="例如:现代外卖员被雷劈穿越到修仙世界,靠祖传古玉捡漏发育,苟着苟着成了大佬…(≥50 字)"
|
|
252
|
-
value={idea}
|
|
253
|
-
onChange={e => { setIdea(e.target.value) }}
|
|
254
|
-
spellCheck={false}
|
|
255
|
-
/>
|
|
256
|
-
<div className={css.row} style={{ flexWrap: 'wrap' }}>
|
|
257
|
-
<button
|
|
258
|
-
type="button"
|
|
259
|
-
className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`}
|
|
260
|
-
disabled={suggesting || idea.trim().length < 10}
|
|
261
|
-
onClick={() => { void handleSuggest() }}
|
|
262
|
-
title="生成 3 个方向不同的大纲方案供选择(约消耗 6-8k token)"
|
|
263
|
-
>
|
|
264
|
-
{suggesting ? '⏳ 生成中…' : candidates.length === 0 ? '✨ 生成大纲方案' : `↻ 换一批(${Math.max(1, 3 - pinned.length)} 个)`}
|
|
265
|
-
</button>
|
|
266
|
-
{candidates.length > 0 && (
|
|
267
|
-
<span className={css.meta}>
|
|
268
|
-
已暂留 {pinned.length}/3 · 换批保留已暂留,只补新方案
|
|
269
|
-
</span>
|
|
270
|
-
)}
|
|
271
|
-
</div>
|
|
272
|
-
|
|
273
|
-
{/* 方案卡 */}
|
|
274
|
-
{candidates.length > 0 && (
|
|
275
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-8)' }}>
|
|
276
|
-
{candidates.map(candidate => {
|
|
277
|
-
const isPinned = pinned.includes(candidate.id)
|
|
278
|
-
return (
|
|
279
|
-
<div
|
|
280
|
-
key={candidate.id}
|
|
281
|
-
className={css.ideaCandidate}
|
|
282
|
-
style={isPinned ? { borderColor: 'var(--nf-accent)', boxShadow: '0 0 0 2px var(--nf-accent-soft)' } : undefined}
|
|
283
|
-
>
|
|
284
|
-
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap', gap: 'var(--nf-space-6)' }}>
|
|
285
|
-
<span style={{ display: 'flex', alignItems: 'center', gap: 'var(--nf-space-8)', flexWrap: 'wrap' }}>
|
|
286
|
-
<b>《{candidate.bookName}》</b>
|
|
287
|
-
{candidate.genre !== '' && <span className={css.badge} style={{ borderColor: 'var(--nf-accent)', color: 'var(--nf-accent)' }}>{candidate.genre}</span>}
|
|
288
|
-
{isPinned && <span className={css.badge} style={{ borderColor: 'var(--nf-warn)', color: 'var(--nf-warn)' }}>★ 已暂留</span>}
|
|
289
|
-
</span>
|
|
290
|
-
<span style={{ display: 'flex', gap: 'var(--nf-space-6)' }}>
|
|
291
|
-
<button
|
|
292
|
-
type="button"
|
|
293
|
-
className={`${css.button} ${css.buttonSmall} ${isPinned ? '' : css.buttonPrimary}`}
|
|
294
|
-
onClick={() => { togglePin(candidate.id) }}
|
|
295
|
-
title={isPinned ? '取消暂留:下次换批会覆盖此槽位' : '暂留此方案:换批时保留,继续对比新方案'}
|
|
296
|
-
>
|
|
297
|
-
{isPinned ? '★ 取消暂留' : '☆ 暂留'}
|
|
298
|
-
</button>
|
|
299
|
-
<button
|
|
300
|
-
type="button"
|
|
301
|
-
className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`}
|
|
302
|
-
onClick={() => { handlePick(candidate) }}
|
|
303
|
-
title="将这份大纲填入上方大纲框(可继续修改后开书)"
|
|
304
|
-
>
|
|
305
|
-
选这个
|
|
306
|
-
</button>
|
|
307
|
-
</span>
|
|
308
|
-
</div>
|
|
309
|
-
{candidate.sellingPoint !== '' && (
|
|
310
|
-
<div className={css.meta}><b>卖点:</b>{candidate.sellingPoint}</div>
|
|
311
|
-
)}
|
|
312
|
-
<div
|
|
313
|
-
className={css.meta}
|
|
314
|
-
style={{
|
|
315
|
-
display: '-webkit-box',
|
|
316
|
-
WebkitLineClamp: 4,
|
|
317
|
-
WebkitBoxOrient: 'vertical',
|
|
318
|
-
overflow: 'hidden',
|
|
319
|
-
whiteSpace: 'pre-wrap',
|
|
320
|
-
}}
|
|
321
|
-
title={candidate.outline}
|
|
322
|
-
>
|
|
323
|
-
{candidate.outline}
|
|
324
|
-
</div>
|
|
325
|
-
</div>
|
|
326
|
-
)
|
|
327
|
-
})}
|
|
328
|
-
{pinned.length >= 3 && (
|
|
329
|
-
<span className={css.meta}>已暂留全部方案——直接挑一个「选这个」即可;换批需先取消某个暂留。</span>
|
|
330
|
-
)}
|
|
331
|
-
</div>
|
|
332
|
-
)}
|
|
333
|
-
</div>
|
|
334
|
-
)}
|
|
335
|
-
</div>
|
|
336
|
-
|
|
337
|
-
<button
|
|
338
|
-
type="button"
|
|
339
|
-
className={`${css.button} ${css.buttonPrimary}`}
|
|
340
|
-
style={{ width: '100%', padding: 'var(--nf-space-10) 0', fontSize: 'var(--nf-fs-14)' }}
|
|
341
|
-
disabled={busy || effectiveName === ''}
|
|
342
|
-
onClick={() => { void handleCreate() }}
|
|
343
|
-
>
|
|
344
|
-
✨ 开书并进入工作台
|
|
345
|
-
</button>
|
|
346
|
-
<span className={css.meta} style={{ textAlign: 'center' }}>
|
|
347
|
-
未提供大纲也能开书,稍后可在大纲页导入
|
|
348
|
-
</span>
|
|
349
|
-
</div>
|
|
350
|
-
</div>
|
|
351
|
-
)
|
|
352
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 漫剧工作台·全流程步骤条(唯一导航,方案X):
|
|
3
|
-
* ①创建方案 → ②一键生成 → ③分镜 → ④角色定妆 → ⑤场景底图 → ⑥导出使用。
|
|
4
|
-
* 每个步骤对应一个独立页面主体;完成度自动判定(读 project 现有字段)。
|
|
5
|
-
*/
|
|
6
|
-
import type { ProjectState } from '../../protocol.ts'
|
|
7
|
-
import css from './panel.module.css'
|
|
8
|
-
|
|
9
|
-
/** 步骤 → 页面主体的一一对应目标。 */
|
|
10
|
-
export type FlowTarget =
|
|
11
|
-
| 'plan' // ① 创建方案页
|
|
12
|
-
| 'rules' // ② 视觉规则页
|
|
13
|
-
| 'skeleton' // ③ 剧情骨架页(分镜·骨架)
|
|
14
|
-
| 'table' // ④ 分镜表页(分镜·分镜表)
|
|
15
|
-
| 'import' // ⑤ 导入角色页(角色·导入)
|
|
16
|
-
| 'makeup' // ⑥ 角色定妆页(角色·卡片)
|
|
17
|
-
| 'prompts' // ⑦ 视频提示词页(分镜·提示词)
|
|
18
|
-
| 'scenes' // ⑧ 场景库页
|
|
19
|
-
| 'props' // ⑨ 道具库页
|
|
20
|
-
| 'export' // ⑩ 导出使用页
|
|
21
|
-
|
|
22
|
-
export interface FlowStep {
|
|
23
|
-
no: number
|
|
24
|
-
label: string
|
|
25
|
-
hint: string
|
|
26
|
-
done: boolean
|
|
27
|
-
target: FlowTarget
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** 依据 project 数据计算 6 步完成度(与存储顺序无关,按生产顺序排列)。 */
|
|
31
|
-
export function computeFlowSteps(project: ProjectState | null, exported?: boolean): FlowStep[] {
|
|
32
|
-
const plans = project?.mangaPlans ?? []
|
|
33
|
-
const rules = project?.visualRules ?? []
|
|
34
|
-
const sbs = project?.storyboards ?? []
|
|
35
|
-
const manga = project?.mangaRoles ?? []
|
|
36
|
-
const scenes = project?.scenes ?? []
|
|
37
|
-
const props = project?.props ?? []
|
|
38
|
-
const hasSkeleton = sbs.some(e => e.skeleton !== undefined)
|
|
39
|
-
const hasTable = sbs.some(e => e.table !== undefined)
|
|
40
|
-
const hasPrompts = sbs.some(e => (e.prompts ?? []).length > 0)
|
|
41
|
-
return [
|
|
42
|
-
{ no: 1, label: '创建方案', hint: '选基底风格+题材并命名;此后所有提示词按此风格生成', done: plans.length > 0, target: 'plan' },
|
|
43
|
-
{ no: 2, label: '一键生成', hint: '选章节后自动做 剧情骨架→分镜表→角色导入;视频提示词留到⑤', done: hasTable, target: 'import' },
|
|
44
|
-
{ no: 3, label: '角色库', hint: '主要角色定妆图提示词→去即梦出图→建智能角色', done: manga.some(c => c.status === 'anchored'), target: 'makeup' },
|
|
45
|
-
{ no: 4, label: '场景库', hint: '(选做)从正文提炼场景卡并出场景参考图,提示词自动引用', done: scenes.length > 0, target: 'scenes' },
|
|
46
|
-
{ no: 5, label: '道具库', hint: '(选做)从正文提炼常驻道具,提示词里道具外观跨镜头统一', done: props.length > 0, target: 'props' },
|
|
47
|
-
{ no: 6, label: '分镜·提示词', hint: '生成逐镜即梦提示词(可粘贴)→复制去即梦;含导出存档', done: hasPrompts, target: 'prompts' },
|
|
48
|
-
]
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function FlowGuide({
|
|
52
|
-
project,
|
|
53
|
-
onNavigate,
|
|
54
|
-
exported,
|
|
55
|
-
}: {
|
|
56
|
-
project: ProjectState | null
|
|
57
|
-
onNavigate: (target: FlowTarget) => void
|
|
58
|
-
exported?: boolean
|
|
59
|
-
}) {
|
|
60
|
-
const steps = computeFlowSteps(project, exported)
|
|
61
|
-
const doneCount = steps.filter(s => s.done).length
|
|
62
|
-
return (
|
|
63
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)', marginTop: 'var(--nf-space-8)' }}>
|
|
64
|
-
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
65
|
-
<span className={css.meta}>🧭 全流程({doneCount}/{steps.length}):每步一个页面,完成自动打勾</span>
|
|
66
|
-
<span className={css.meta}>点击步骤打开对应页面</span>
|
|
67
|
-
</div>
|
|
68
|
-
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--nf-space-6)' }}>
|
|
69
|
-
{steps.map(s => (
|
|
70
|
-
<button
|
|
71
|
-
key={s.no}
|
|
72
|
-
type="button"
|
|
73
|
-
title={s.hint}
|
|
74
|
-
className={css.button + ' ' + css.buttonSmall + (s.done ? ' ' + css.buttonPrimary : '')}
|
|
75
|
-
style={{ minWidth: 0 }}
|
|
76
|
-
onClick={() => { onNavigate(s.target) }}
|
|
77
|
-
>
|
|
78
|
-
{s.done ? '✓ ' : s.no + '. '}{s.label}
|
|
79
|
-
</button>
|
|
80
|
-
))}
|
|
81
|
-
</div>
|
|
82
|
-
</div>
|
|
83
|
-
)
|
|
84
|
-
}
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 全局写作资产库:只读浏览插件内置的公共资产(题材/反AI规则/风格模板/推进模式)。
|
|
3
|
-
* 内置库为固定内容;实际绑定到某本书在「创作资产」页。
|
|
4
|
-
*/
|
|
5
|
-
import { useEffect, useState } from 'react'
|
|
6
|
-
import { Info, BookOpen } from 'lucide-react'
|
|
7
|
-
import type { NovelApi } from '../api.ts'
|
|
8
|
-
import type { AssetsResponse, GenreNode, AntiAiRule, StyleTemplate, ProgressionMode, PlotBeatTemplate } from '../../protocol.ts'
|
|
9
|
-
import css from './panel.module.css'
|
|
10
|
-
|
|
11
|
-
type Tab = 'genre' | 'antiAi' | 'style' | 'progression' | 'plotBeat'
|
|
12
|
-
|
|
13
|
-
export function GlobalAssetLibraryView({ api }: { api: NovelApi }) {
|
|
14
|
-
const [data, setData] = useState<AssetsResponse | null>(null);
|
|
15
|
-
const [error, setError] = useState('');
|
|
16
|
-
const [tab, setTab] = useState<Tab>('genre');
|
|
17
|
-
|
|
18
|
-
useEffect(() => {
|
|
19
|
-
let cancelled = false
|
|
20
|
-
void (async () => {
|
|
21
|
-
try {
|
|
22
|
-
const res = await api.assets()
|
|
23
|
-
if (!cancelled) setData(res)
|
|
24
|
-
} catch (err) {
|
|
25
|
-
if (!cancelled) setError((err as Error).message)
|
|
26
|
-
}
|
|
27
|
-
})()
|
|
28
|
-
return () => { cancelled = true }
|
|
29
|
-
}, [api]);
|
|
30
|
-
|
|
31
|
-
const renderGenreDescendants = (children: GenreNode[], depth: number): JSX.Element[] =>
|
|
32
|
-
children.map(c => (
|
|
33
|
-
<div key={c.name} style={{ paddingLeft: depth * 12, marginTop: 2 }}>
|
|
34
|
-
<span className={css.meta}><b>{c.name}</b>:{c.description}</span>
|
|
35
|
-
{(c.children ?? []).length > 0 && renderGenreDescendants(c.children ?? [], depth + 1)}
|
|
36
|
-
</div>
|
|
37
|
-
));
|
|
38
|
-
|
|
39
|
-
const renderGenre = (nodes: GenreNode[]): JSX.Element[] =>
|
|
40
|
-
nodes.map(n => (
|
|
41
|
-
<div key={n.name} className={css.assetCard}>
|
|
42
|
-
<div className={css.assetCardName}>{n.name}</div>
|
|
43
|
-
<div className={css.meta}>{n.description}</div>
|
|
44
|
-
{(n.children ?? []).length > 0 && (
|
|
45
|
-
<div style={{ marginTop: 6 }}>{renderGenreDescendants(n.children ?? [], 0)}</div>
|
|
46
|
-
)}
|
|
47
|
-
</div>
|
|
48
|
-
));
|
|
49
|
-
|
|
50
|
-
const renderAntiAi = (rules: AntiAiRule[]): JSX.Element[] =>
|
|
51
|
-
rules.map(r => (
|
|
52
|
-
<div key={r.name} className={css.assetCard}>
|
|
53
|
-
<div className={css.assetCardTop}><span className={css.assetKind} style={{ color: 'var(--nf-warn)' }}>反AI</span></div>
|
|
54
|
-
<div className={css.assetCardName}>{r.name}</div>
|
|
55
|
-
<div className={css.assetCardContent}>避免:{r.avoid}{r.fix !== '' ? '\n修正:' + r.fix : ''}</div>
|
|
56
|
-
{(r.detectPatterns ?? []).length > 0 && <div className={css.meta}>命中模式:{(r.detectPatterns ?? []).join('、')}</div>}
|
|
57
|
-
</div>
|
|
58
|
-
));
|
|
59
|
-
|
|
60
|
-
const renderStyle = (templates: StyleTemplate[]): JSX.Element[] =>
|
|
61
|
-
templates.map(t => (
|
|
62
|
-
<div key={t.key} className={css.assetCard}>
|
|
63
|
-
<div className={css.assetCardTop}><span className={css.assetKind} style={{ color: 'var(--nf-info)' }}>{t.category}</span></div>
|
|
64
|
-
<div className={css.assetCardName}>{t.name}</div>
|
|
65
|
-
<div className={css.meta}>{t.description}</div>
|
|
66
|
-
<div className={css.assetCardContent}>
|
|
67
|
-
适用:{(t.applicableGenres ?? []).join('、')}
|
|
68
|
-
{t.proseRules.length > 0 ? '\n叙述:' + t.proseRules.join(';') : ''}
|
|
69
|
-
{t.dialogueRules.length > 0 ? '\n台词:' + t.dialogueRules.join(';') : ''}
|
|
70
|
-
{t.languageRules.length > 0 ? '\n语言:' + t.languageRules.join(';') : ''}
|
|
71
|
-
{t.rhythmRules.length > 0 ? '\n节奏:' + t.rhythmRules.join(';') : ''}
|
|
72
|
-
</div>
|
|
73
|
-
</div>
|
|
74
|
-
));
|
|
75
|
-
|
|
76
|
-
const renderProgression = (modes: ProgressionMode[]): JSX.Element[] =>
|
|
77
|
-
modes.map(m => (
|
|
78
|
-
<div key={m.name} className={css.assetCard}>
|
|
79
|
-
<div className={css.assetCardTop}><span className={css.assetKind} style={{ color: 'var(--nf-accent)' }}>{m.primary ? '主模式' : '辅助'}</span></div>
|
|
80
|
-
<div className={css.assetCardName}>{m.name}</div>
|
|
81
|
-
<div className={css.meta}>驱动:{m.driver}</div>
|
|
82
|
-
<div className={css.assetCardContent}>
|
|
83
|
-
期待:{m.readerExpectation}
|
|
84
|
-
{m.payoffs.length > 0 ? '\n兑现:' + m.payoffs.join(';') : ''}
|
|
85
|
-
{m.risks.length > 0 ? '\n风险:' + m.risks.join(';') : ''}
|
|
86
|
-
</div>
|
|
87
|
-
</div>
|
|
88
|
-
));
|
|
89
|
-
|
|
90
|
-
const renderPlotBeat = (beats: PlotBeatTemplate[]): JSX.Element[] =>
|
|
91
|
-
beats.map(b => (
|
|
92
|
-
<div key={b.key} className={css.assetCard}>
|
|
93
|
-
<div className={css.assetCardTop}><span className={css.assetKind} style={{ color: 'var(--nf-accent)' }}>{b.category}</span></div>
|
|
94
|
-
<div className={css.assetCardName}>{b.name}</div>
|
|
95
|
-
<div className={css.meta}>{b.summary}</div>
|
|
96
|
-
<div className={css.assetCardContent}>
|
|
97
|
-
位置:{b.position}
|
|
98
|
-
{(b.preconditions ?? []).length > 0 ? '\n前置:' + b.preconditions.join(';') : ''}
|
|
99
|
-
{(b.payoffSource ?? []).length > 0 ? '\n爽点:' + b.payoffSource.join(';') : ''}
|
|
100
|
-
{(b.combos ?? []).length > 0 ? '\n组合:' + b.combos.join(';') : ''}
|
|
101
|
-
{(b.taboos ?? []).length > 0 ? '\n禁忌:' + b.taboos.join(';') : ''}
|
|
102
|
-
</div>
|
|
103
|
-
</div>
|
|
104
|
-
));
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
<div className={css.authorPageBody}>
|
|
108
|
-
<div className={css.authorPageHero}>
|
|
109
|
-
<div className={css.authorPageHeader}>
|
|
110
|
-
<div>
|
|
111
|
-
<h2 className={css.panelTitle} style={{ margin: 0 }}>🎨 全局写作资产库</h2>
|
|
112
|
-
<span className={css.meta}>内置题材基底库 / 反AI规则库 / 风格模板 / 推进模式 / 剧情桥段库(跨书)——插件自带、只读。</span>
|
|
113
|
-
</div>
|
|
114
|
-
</div>
|
|
115
|
-
<div className={css.assetFilterBar}>
|
|
116
|
-
<button type="button" className={css.assetFilterChip + (tab === 'genre' ? ' ' + css.assetFilterChipActive : '')} onClick={() => setTab('genre')}>题材 {(data?.genreLibrary ?? []).length}</button>
|
|
117
|
-
<button type="button" className={css.assetFilterChip + (tab === 'antiAi' ? ' ' + css.assetFilterChipActive : '')} onClick={() => setTab('antiAi')}>反AI规则 {(data?.antiAiLibrary ?? []).length}</button>
|
|
118
|
-
<button type="button" className={css.assetFilterChip + (tab === 'style' ? ' ' + css.assetFilterChipActive : '')} onClick={() => setTab('style')}>风格模板 {(data?.styleTemplates ?? []).length}</button>
|
|
119
|
-
<button type="button" className={css.assetFilterChip + (tab === 'progression' ? ' ' + css.assetFilterChipActive : '')} onClick={() => setTab('progression')}>推进模式 {(data?.progressionLibrary ?? []).length}</button>
|
|
120
|
-
<button type="button" className={css.assetFilterChip + (tab === 'plotBeat' ? ' ' + css.assetFilterChipActive : '')} onClick={() => setTab('plotBeat')}>剧情桥段 {(data?.plotBeatLibrary ?? []).length}</button>
|
|
121
|
-
</div>
|
|
122
|
-
</div>
|
|
123
|
-
|
|
124
|
-
{error !== '' && <div className={css.noticeError}>{error}</div>}
|
|
125
|
-
|
|
126
|
-
{data === null ? (
|
|
127
|
-
<div className={css.shelfEmpty} style={{ minHeight: 160 }}>
|
|
128
|
-
<span className={css.shelfEmptyIcon}><Info size={30} /></span>
|
|
129
|
-
<span className={css.shelfEmptyTitle}>加载中…</span>
|
|
130
|
-
<span className={css.meta}>正在读取全局内置资产库。</span>
|
|
131
|
-
</div>
|
|
132
|
-
) : (
|
|
133
|
-
<div className={css.assetGroups}>
|
|
134
|
-
{tab === 'genre' && (
|
|
135
|
-
<div className={css.assetGroup}>
|
|
136
|
-
<div className={css.assetGroupHeader}>题材基底库 <span className={css.meta}>树形 / 只读</span></div>
|
|
137
|
-
<div className={css.assetGrid}>{renderGenre(data.genreLibrary)}</div>
|
|
138
|
-
</div>
|
|
139
|
-
)}
|
|
140
|
-
{tab === 'antiAi' && (
|
|
141
|
-
<div className={css.assetGroup}>
|
|
142
|
-
<div className={css.assetGroupHeader}>反AI规则库 <span className={css.meta}>只读 / 可在书内复制为自定义</span></div>
|
|
143
|
-
<div className={css.assetGrid}>{renderAntiAi(data.antiAiLibrary)}</div>
|
|
144
|
-
</div>
|
|
145
|
-
)}
|
|
146
|
-
{tab === 'style' && (
|
|
147
|
-
<div className={css.assetGroup}>
|
|
148
|
-
<div className={css.assetGroupHeader}>风格模板 <span className={css.meta}>只读 / 可在书内一键绑定</span></div>
|
|
149
|
-
<div className={css.assetGrid}>{renderStyle(data.styleTemplates)}</div>
|
|
150
|
-
</div>
|
|
151
|
-
)}
|
|
152
|
-
{tab === 'progression' && (
|
|
153
|
-
<div className={css.assetGroup}>
|
|
154
|
-
<div className={css.assetGroupHeader}>推进模式 <span className={css.meta}>只读</span></div>
|
|
155
|
-
<div className={css.assetGrid}>{renderProgression(data.progressionLibrary)}</div>
|
|
156
|
-
</div>
|
|
157
|
-
)}
|
|
158
|
-
{tab === 'plotBeat' && (
|
|
159
|
-
<div className={css.assetGroup}>
|
|
160
|
-
<div className={css.assetGroupHeader}>剧情桥段库 <span className={css.meta}>只读 / 可收藏或导入作者库</span></div>
|
|
161
|
-
<div className={css.assetGrid}>{renderPlotBeat(data.plotBeatLibrary)}</div>
|
|
162
|
-
</div>
|
|
163
|
-
)}
|
|
164
|
-
</div>
|
|
165
|
-
)}
|
|
166
|
-
|
|
167
|
-
<div className={css.meta} style={{ marginTop: 10 }}>
|
|
168
|
-
提示:内置库为插件自带、只读。想要在书里使用,请进入某本书的「创作资产」页绑定为项目资产,或在「作者资产库」里「导入默认资产」沉淀成可复用的个人条目。
|
|
169
|
-
</div>
|
|
170
|
-
</div>
|
|
171
|
-
);
|
|
172
|
-
}
|