@waterwx/dsh-novel-forge 2.1.2-alpha → 2.1.3
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 +278 -368
- package/lib/client.js +19568 -21381
- package/lib/index.js +5158 -3958
- package/lib/types/action-guard.d.ts +61 -0
- package/lib/types/ai-scan.d.ts +80 -12
- package/lib/types/assets.d.ts +7 -9
- package/lib/types/assistant.d.ts +25 -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 +49 -29
- 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 +12 -14
- package/lib/types/client/locales.d.ts +12 -67
- package/lib/types/client/mount.d.ts +0 -2
- package/lib/types/client/panel/AdaptModeView.d.ts +1 -1
- package/lib/types/client/panel/AiTag.d.ts +1 -0
- package/lib/types/client/panel/AssetsTab.d.ts +1 -1
- package/lib/types/client/panel/AssistantTab.d.ts +4 -1
- package/lib/types/client/panel/AuthorAssetsView.d.ts +1 -1
- package/lib/types/client/panel/AuthorHome.d.ts +12 -12
- package/lib/types/client/panel/CmdPalette.d.ts +12 -0
- package/lib/types/client/panel/CreateBookView.d.ts +1 -1
- package/lib/types/client/panel/GlobalAssetLibraryView.d.ts +1 -1
- package/lib/types/client/panel/ImportModal.d.ts +1 -1
- package/lib/types/client/panel/LiveFeedLog.d.ts +8 -0
- package/lib/types/client/panel/ModelManager.d.ts +2 -2
- package/lib/types/client/panel/NovelPanel.d.ts +7 -2
- package/lib/types/client/panel/PageHeader.d.ts +18 -0
- package/lib/types/client/panel/ProgressConsole.d.ts +1 -1
- package/lib/types/client/panel/PromptSlotsView.d.ts +11 -0
- package/lib/types/client/panel/ReaderView.d.ts +1 -1
- package/lib/types/client/panel/RunPanel.d.ts +1 -1
- package/lib/types/client/panel/SettingsView.d.ts +13 -10
- package/lib/types/client/panel/ShelfView.d.ts +11 -2
- 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/WorldTab.d.ts +1 -1
- 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 +122 -103
- package/lib/types/home.d.ts +11 -0
- package/lib/types/index.d.ts +21 -13
- package/lib/types/llm-live.d.ts +62 -36
- package/lib/types/llm-retry.d.ts +38 -0
- package/lib/types/market-radar-scan.d.ts +0 -1
- package/lib/types/market-radar-sources.d.ts +1 -1
- 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 +2 -27
- package/lib/types/protocol.d.ts +479 -595
- 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 +31 -0
- 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/style-fingerprint.d.ts +78 -0
- package/lib/types/tension.d.ts +53 -0
- package/lib/types/timeline.d.ts +37 -0
- package/package.json +41 -28
- 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/src/ai-scan.d.ts +0 -36
- package/lib/types/src/assets.d.ts +0 -37
- package/lib/types/src/assistant.d.ts +0 -45
- package/lib/types/src/author-assets.d.ts +0 -25
- package/lib/types/src/bookshelf.d.ts +0 -42
- package/lib/types/src/client/api.d.ts +0 -234
- package/lib/types/src/client/docx.d.ts +0 -15
- package/lib/types/src/client/index.d.ts +0 -14
- package/lib/types/src/client/locales.d.ts +0 -212
- package/lib/types/src/client/mount.d.ts +0 -9
- package/lib/types/src/client/panel/AdaptModeView.d.ts +0 -5
- package/lib/types/src/client/panel/AssetsTab.d.ts +0 -12
- package/lib/types/src/client/panel/AssistantTab.d.ts +0 -7
- package/lib/types/src/client/panel/AuthorAssetsView.d.ts +0 -4
- package/lib/types/src/client/panel/AuthorHome.d.ts +0 -32
- package/lib/types/src/client/panel/BookshelfBar.d.ts +0 -11
- package/lib/types/src/client/panel/CreateBookView.d.ts +0 -9
- package/lib/types/src/client/panel/FlowGuide.d.ts +0 -22
- package/lib/types/src/client/panel/GlobalAssetLibraryView.d.ts +0 -4
- package/lib/types/src/client/panel/ImportModal.d.ts +0 -7
- package/lib/types/src/client/panel/MangaRoleLibrary.d.ts +0 -20
- package/lib/types/src/client/panel/MangaWorkspace.d.ts +0 -12
- package/lib/types/src/client/panel/ModelManager.d.ts +0 -14
- package/lib/types/src/client/panel/NovelPanel.d.ts +0 -13
- package/lib/types/src/client/panel/ProgressConsole.d.ts +0 -17
- package/lib/types/src/client/panel/ProgressHomeView.d.ts +0 -5
- package/lib/types/src/client/panel/PropLibrary.d.ts +0 -8
- package/lib/types/src/client/panel/ReaderView.d.ts +0 -8
- package/lib/types/src/client/panel/ReasoningSection.d.ts +0 -10
- package/lib/types/src/client/panel/RunPanel.d.ts +0 -5
- package/lib/types/src/client/panel/SceneLibrary.d.ts +0 -16
- package/lib/types/src/client/panel/SettingsView.d.ts +0 -16
- package/lib/types/src/client/panel/ShelfView.d.ts +0 -15
- package/lib/types/src/client/panel/StoryboardTab.d.ts +0 -19
- package/lib/types/src/client/panel/StyleCard.d.ts +0 -8
- package/lib/types/src/client/panel/StyleGallery.d.ts +0 -1
- package/lib/types/src/client/panel/WorldTab.d.ts +0 -10
- package/lib/types/src/client/panel/controller.d.ts +0 -19
- package/lib/types/src/client/panel/helpers.d.ts +0 -18
- package/lib/types/src/client/panel/views.d.ts +0 -77
- package/lib/types/src/client/sidebar-entry.d.ts +0 -13
- package/lib/types/src/client/text.d.ts +0 -9
- package/lib/types/src/comic-presets.d.ts +0 -12
- package/lib/types/src/docx.d.ts +0 -19
- package/lib/types/src/engine.d.ts +0 -402
- package/lib/types/src/index.d.ts +0 -79
- package/lib/types/src/manga-genre-rules.d.ts +0 -22
- package/lib/types/src/novel-context.d.ts +0 -80
- package/lib/types/src/prompt-utils.d.ts +0 -35
- package/lib/types/src/protocol.d.ts +0 -2176
- package/lib/types/src/review-policy.d.ts +0 -97
- package/lib/types/src/routes.d.ts +0 -29
- package/lib/types/src/run.d.ts +0 -32
- package/lib/types/src/shot-language.d.ts +0 -46
- package/lib/types/src/story-beat-language.d.ts +0 -35
- package/lib/types/src/style-library.d.ts +0 -33
- package/lib/types/story-beat-language.d.ts +0 -35
- package/lib/types/style-library.d.ts +0 -33
- package/src/ai-scan.ts +0 -179
- package/src/assets.ts +0 -1774
- package/src/assistant.ts +0 -915
- package/src/author-assets.ts +0 -147
- package/src/bookshelf.ts +0 -167
- package/src/client/api.ts +0 -716
- 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/llmLive.ts +0 -101
- 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 -535
- package/src/client/panel/AssetsTab.tsx +0 -458
- package/src/client/panel/AssistantTab.tsx +0 -339
- package/src/client/panel/AuthorAssetsView.tsx +0 -208
- package/src/client/panel/AuthorHome.tsx +0 -234
- package/src/client/panel/BookAnalysisView.tsx +0 -60
- package/src/client/panel/BookshelfBar.tsx +0 -170
- package/src/client/panel/CreateBookView.tsx +0 -371
- package/src/client/panel/DirectorView.tsx +0 -200
- package/src/client/panel/FlowGuide.tsx +0 -84
- package/src/client/panel/GlobalAssetLibraryView.tsx +0 -172
- package/src/client/panel/IdeaInspirationView.tsx +0 -71
- package/src/client/panel/ImportModal.tsx +0 -303
- package/src/client/panel/KnowledgeBaseView.tsx +0 -73
- package/src/client/panel/LiveFeedLog.tsx +0 -76
- package/src/client/panel/MangaRoleLibrary.tsx +0 -1002
- package/src/client/panel/MangaWorkspace.tsx +0 -715
- package/src/client/panel/MarketRadarView.tsx +0 -374
- package/src/client/panel/ModelManager.tsx +0 -205
- package/src/client/panel/NovelPanel.tsx +0 -4721
- package/src/client/panel/ProgressConsole.tsx +0 -63
- 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 -289
- 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 -4359
- 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 -5636
- package/src/global-assets.ts +0 -59
- package/src/index.ts +0 -346
- package/src/llm-live.ts +0 -68
- package/src/manga-genre-rules.ts +0 -87
- package/src/market-radar-scan.ts +0 -41
- package/src/market-radar-sources.ts +0 -173
- package/src/novel-context.ts +0 -287
- package/src/prompt-utils.ts +0 -73
- package/src/prompting.ts +0 -159
- package/src/protocol.ts +0 -2565
- package/src/review-policy.ts +0 -161
- package/src/routes.ts +0 -3939
- package/src/run.ts +0 -338
- package/src/shot-language.ts +0 -181
- package/src/story-beat-language.ts +0 -130
- package/src/style-library.ts +0 -84
|
@@ -1,371 +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 { setCurrentBook } from '../api.ts'
|
|
9
|
-
import type { OutlineCandidate } from '../../protocol.ts'
|
|
10
|
-
import { extractDocxTextFromBuffer } from '../docx.ts'
|
|
11
|
-
import css from './panel.module.css'
|
|
12
|
-
|
|
13
|
-
/** 从大纲首行推断书名(与服务端 inferBookName 一致,供实时预览)。 */
|
|
14
|
-
function inferBookNamePreview(outline: string): string {
|
|
15
|
-
const line = outline.split('\n').map(l => l.trim()).find(l => l.length > 0)
|
|
16
|
-
if (line === undefined) return ''
|
|
17
|
-
return line.replace(/^《/, '').replace(/》.*$/, '').slice(0, 40)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** 简单题材识别(提示用)。 */
|
|
21
|
-
function guessGenre(outline: string): string | null {
|
|
22
|
-
const map: Array<[string, string[]]> = [
|
|
23
|
-
['仙侠修真', ['仙', '修', '灵根', '元婴', '宗门', '飞升']],
|
|
24
|
-
['都市', ['都市', '公司', '外卖', '职场', '总裁']],
|
|
25
|
-
['玄幻', ['斗气', '魂力', '大陆', '斗罗', '神']],
|
|
26
|
-
['悬疑', ['悬疑', '密室', '案件', '推理', '凶']],
|
|
27
|
-
['科幻', ['机甲', '星舰', 'AI', '未来', '星际']],
|
|
28
|
-
['历史', ['朝代', '皇帝', '将军', '古代', '王朝']],
|
|
29
|
-
['游戏', ['游戏', '副本', '装备', '等级', '职业']],
|
|
30
|
-
]
|
|
31
|
-
for (const [genre, keywords] of map) {
|
|
32
|
-
if (keywords.some(k => outline.includes(k))) return genre
|
|
33
|
-
}
|
|
34
|
-
return null
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** 开书向导页。 */
|
|
38
|
-
export function CreateBookView({
|
|
39
|
-
api,
|
|
40
|
-
onBack,
|
|
41
|
-
onCreated,
|
|
42
|
-
initialIdea,
|
|
43
|
-
initialName,
|
|
44
|
-
}: {
|
|
45
|
-
api: NovelApi
|
|
46
|
-
/** 返回书架。 */
|
|
47
|
-
onBack: () => void
|
|
48
|
-
/** 开书成功:进入新书工作台。 */
|
|
49
|
-
onCreated: (id: string) => void
|
|
50
|
-
/** 从「创意灵感」带过来的起步想法(预填到「一句话想法」输入框)。 */
|
|
51
|
-
initialIdea?: string
|
|
52
|
-
/** 从「创意灵感」带过来的初始书名。 */
|
|
53
|
-
initialName?: string
|
|
54
|
-
}) {
|
|
55
|
-
const [name, setName] = useState(initialName ?? '')
|
|
56
|
-
const [outlineText, setOutlineText] = useState('')
|
|
57
|
-
const [outlineName, setOutlineName] = useState('')
|
|
58
|
-
const [dragActive, setDragActive] = useState(false)
|
|
59
|
-
const [busy, setBusy] = useState(false)
|
|
60
|
-
const [error, setError] = useState('')
|
|
61
|
-
const outlineFileRef = useRef<HTMLInputElement | null>(null)
|
|
62
|
-
|
|
63
|
-
// ---- 想法 → AI 大纲状态 ----
|
|
64
|
-
const hasPrefill = initialIdea !== undefined && initialIdea.trim() !== ''
|
|
65
|
-
const [ideaOpen, setIdeaOpen] = useState(hasPrefill)
|
|
66
|
-
const [idea, setIdea] = useState(initialIdea ?? '')
|
|
67
|
-
const [suggesting, setSuggesting] = useState(false)
|
|
68
|
-
const [candidates, setCandidates] = useState<OutlineCandidate[]>([])
|
|
69
|
-
const [pinned, setPinned] = useState<string[]>([])
|
|
70
|
-
/** 回填后提示(如「已填入方案A,可继续修改」)。 */
|
|
71
|
-
const [fillNotice, setFillNotice] = useState('')
|
|
72
|
-
/** 选定方案自带的题材(创建后自动预填到本书创作资产)。 */
|
|
73
|
-
const [selectedGenre, setSelectedGenre] = useState('')
|
|
74
|
-
|
|
75
|
-
const autoName = inferBookNamePreview(outlineText)
|
|
76
|
-
const effectiveName = name.trim() !== '' ? name.trim() : autoName
|
|
77
|
-
const genre = guessGenre(outlineText)
|
|
78
|
-
|
|
79
|
-
const handlePickOutlineFile = async (file: File | undefined): Promise<void> => {
|
|
80
|
-
if (file === undefined) return
|
|
81
|
-
try {
|
|
82
|
-
const buffer = await file.arrayBuffer()
|
|
83
|
-
const text = extractDocxTextFromBuffer(buffer)
|
|
84
|
-
if (text.length < 50) {
|
|
85
|
-
setError('大纲内容过短(<50 字符),请检查文件')
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
setOutlineText(text)
|
|
89
|
-
setOutlineName(file.name)
|
|
90
|
-
if (name.trim() === '') setName(inferBookNamePreview(text))
|
|
91
|
-
setError('')
|
|
92
|
-
} catch (err) {
|
|
93
|
-
setError(`读取大纲失败:${(err as Error).message}`)
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** 生成/换批:只补未暂留的空槽;exclude 传已暂留方案的卖点方向。 */
|
|
98
|
-
const handleSuggest = async (): Promise<void> => {
|
|
99
|
-
if (idea.trim().length < 10) {
|
|
100
|
-
setError('想法太短(<10 字):至少写一句完整想法,如「男主穿越修仙界靠做菜无敌」')
|
|
101
|
-
return
|
|
102
|
-
}
|
|
103
|
-
setSuggesting(true)
|
|
104
|
-
setError('')
|
|
105
|
-
try {
|
|
106
|
-
const exclude = candidates
|
|
107
|
-
.filter(c => pinned.includes(c.id))
|
|
108
|
-
.map(c => `${c.bookName}:${c.sellingPoint}${c.genre !== '' ? `(${c.genre})` : ''}`)
|
|
109
|
-
const count = Math.max(1, 3 - pinned.length)
|
|
110
|
-
const result = await api.outlineSuggest(idea.trim(), count, exclude)
|
|
111
|
-
// 换批合并:已暂留的原地保留,新生成的补充进空槽。
|
|
112
|
-
setCandidates(prev => {
|
|
113
|
-
const kept = prev.filter(c => pinned.includes(c.id))
|
|
114
|
-
return [...kept, ...result.candidates].slice(0, 3)
|
|
115
|
-
})
|
|
116
|
-
setFillNotice('')
|
|
117
|
-
} catch (err) {
|
|
118
|
-
setError((err as Error).message)
|
|
119
|
-
} finally {
|
|
120
|
-
setSuggesting(false)
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** 暂留/取消暂留。 */
|
|
125
|
-
const togglePin = (id: string): void => {
|
|
126
|
-
setPinned(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id])
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** 选中方案:回填大纲框 + 书名,滚动到上方。 */
|
|
130
|
-
const handlePick = (candidate: OutlineCandidate): void => {
|
|
131
|
-
setOutlineText(candidate.outline)
|
|
132
|
-
setName(candidate.bookName)
|
|
133
|
-
setSelectedGenre(candidate.genre ?? '')
|
|
134
|
-
setFillNotice(`已填入方案《${candidate.bookName}》(${candidate.genre}),可继续修改后开书`)
|
|
135
|
-
setIdeaOpen(false)
|
|
136
|
-
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const handleCreate = async (): Promise<void> => {
|
|
140
|
-
if (effectiveName === '') {
|
|
141
|
-
setError('请填写书名,或提供大纲自动识别')
|
|
142
|
-
return
|
|
143
|
-
}
|
|
144
|
-
setBusy(true)
|
|
145
|
-
setError('')
|
|
146
|
-
try {
|
|
147
|
-
const snapshot = await api.bookCreate(effectiveName, undefined, outlineText.trim() !== '' ? outlineText.trim() : undefined)
|
|
148
|
-
const created = snapshot.books.find(b => b.id === snapshot.activeBookId)
|
|
149
|
-
if (created !== undefined) {
|
|
150
|
-
// 选定方案自带题材,或从大纲关键词识别到题材 → 创建后自动预填到本书创作资产(失败不阻断开书)。
|
|
151
|
-
const detectedGenre = selectedGenre.trim() !== '' ? selectedGenre.trim() : (guessGenre(outlineText) ?? '')
|
|
152
|
-
if (detectedGenre !== '') {
|
|
153
|
-
try {
|
|
154
|
-
setCurrentBook(created.id)
|
|
155
|
-
await api.patchAssets({ genre: { name: detectedGenre, description: '', children: [] } })
|
|
156
|
-
} catch { /* ignore */ }
|
|
157
|
-
}
|
|
158
|
-
onCreated(created.id)
|
|
159
|
-
} else {
|
|
160
|
-
setError('开书失败:未找到新书')
|
|
161
|
-
}
|
|
162
|
-
} catch (err) {
|
|
163
|
-
setError((err as Error).message)
|
|
164
|
-
} finally {
|
|
165
|
-
setBusy(false)
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return (
|
|
170
|
-
<div className={css.createBookView}>
|
|
171
|
-
<div className={css.createBookTop}>
|
|
172
|
-
<button type="button" className={css.iconButton} title="返回书架" aria-label="返回书架" onClick={onBack}>
|
|
173
|
-
← 书架
|
|
174
|
-
</button>
|
|
175
|
-
</div>
|
|
176
|
-
|
|
177
|
-
<div className={css.createBookCard}>
|
|
178
|
-
<span className={css.createBookIcon}>✒️</span>
|
|
179
|
-
<h2 className={css.createBookTitle}>开书向导</h2>
|
|
180
|
-
<span className={css.meta}>把一份大纲「编译」成一本完整的小说</span>
|
|
181
|
-
|
|
182
|
-
{error !== '' && (
|
|
183
|
-
<div className={css.card} style={{ borderColor: 'var(--nf-error)', padding: 'var(--nf-space-8) var(--nf-space-12)' }}>
|
|
184
|
-
<span style={{ color: 'var(--nf-error)', fontSize: 'var(--nf-fs-12)' }}>{error}</span>
|
|
185
|
-
</div>
|
|
186
|
-
)}
|
|
187
|
-
|
|
188
|
-
{fillNotice !== '' && (
|
|
189
|
-
<div className={css.card} style={{ borderColor: 'var(--nf-success)', padding: 'var(--nf-space-8) var(--nf-space-12)' }}>
|
|
190
|
-
<span style={{ color: 'var(--nf-success)', fontSize: 'var(--nf-fs-12)' }}>✅ {fillNotice}</span>
|
|
191
|
-
</div>
|
|
192
|
-
)}
|
|
193
|
-
|
|
194
|
-
<div className={css.field}>
|
|
195
|
-
<label className={css.fieldLabel}>书名</label>
|
|
196
|
-
<input
|
|
197
|
-
className={css.input}
|
|
198
|
-
placeholder={autoName !== '' ? `自动识别:${autoName}` : '输入书名(提供大纲后自动识别)'}
|
|
199
|
-
value={name}
|
|
200
|
-
onChange={e => { setName(e.target.value) }}
|
|
201
|
-
onKeyDown={e => { if (e.key === 'Enter') void handleCreate() }}
|
|
202
|
-
autoFocus
|
|
203
|
-
/>
|
|
204
|
-
</div>
|
|
205
|
-
|
|
206
|
-
<div className={css.field}>
|
|
207
|
-
<label className={css.fieldLabel}>大纲</label>
|
|
208
|
-
<div
|
|
209
|
-
className={`${css.dropzone} ${dragActive ? css.dropzoneActive : ''}`}
|
|
210
|
-
role="button"
|
|
211
|
-
tabIndex={0}
|
|
212
|
-
aria-label="选择或拖入 docx 大纲文件"
|
|
213
|
-
onClick={() => { outlineFileRef.current?.click() }}
|
|
214
|
-
onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); outlineFileRef.current?.click() } }}
|
|
215
|
-
onDragOver={e => { e.preventDefault(); setDragActive(true) }}
|
|
216
|
-
onDragLeave={() => { setDragActive(false) }}
|
|
217
|
-
onDrop={e => {
|
|
218
|
-
e.preventDefault()
|
|
219
|
-
setDragActive(false)
|
|
220
|
-
void handlePickOutlineFile(e.dataTransfer.files?.[0])
|
|
221
|
-
}}
|
|
222
|
-
>
|
|
223
|
-
<span className={css.dropzoneIcon}>📄</span>
|
|
224
|
-
<span>{outlineName !== '' ? `已选择:${outlineName}` : '点击选择 docx 大纲,或将文件拖到这里'}</span>
|
|
225
|
-
<span className={css.meta}>推荐提供大纲:开书即建立项目,书名自动识别</span>
|
|
226
|
-
<input
|
|
227
|
-
ref={outlineFileRef}
|
|
228
|
-
type="file"
|
|
229
|
-
accept=".docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
230
|
-
style={{ display: 'none' }}
|
|
231
|
-
onChange={e => {
|
|
232
|
-
void handlePickOutlineFile(e.target.files?.[0])
|
|
233
|
-
e.target.value = ''
|
|
234
|
-
}}
|
|
235
|
-
/>
|
|
236
|
-
</div>
|
|
237
|
-
<textarea
|
|
238
|
-
className={css.textarea}
|
|
239
|
-
style={{ minHeight: 130 }}
|
|
240
|
-
placeholder="或直接粘贴大纲文本(50 字以上)…"
|
|
241
|
-
value={outlineText}
|
|
242
|
-
onChange={e => { setOutlineText(e.target.value) }}
|
|
243
|
-
spellCheck={false}
|
|
244
|
-
/>
|
|
245
|
-
</div>
|
|
246
|
-
|
|
247
|
-
{(outlineText.trim().length > 0 || effectiveName !== '') && (
|
|
248
|
-
<div className={css.row} style={{ flexWrap: 'wrap' }}>
|
|
249
|
-
{outlineText.trim().length > 0 && <span className={css.meta}>大纲 {outlineText.length} 字</span>}
|
|
250
|
-
{effectiveName !== '' && <span className={css.meta}>书名:{effectiveName}</span>}
|
|
251
|
-
{genre !== null && <span className={css.meta}>题材:{genre}</span>}
|
|
252
|
-
</div>
|
|
253
|
-
)}
|
|
254
|
-
|
|
255
|
-
{/* 想法 → AI 大纲(可折叠) */}
|
|
256
|
-
<div className={css.ideaCard}>
|
|
257
|
-
<button
|
|
258
|
-
type="button"
|
|
259
|
-
className={css.ideaToggle}
|
|
260
|
-
onClick={() => { setIdeaOpen(v => !v) }}
|
|
261
|
-
aria-expanded={ideaOpen}
|
|
262
|
-
>
|
|
263
|
-
✨ {ideaOpen ? '▾' : '▸'} 没有大纲?用一句话想法让 AI 生成
|
|
264
|
-
</button>
|
|
265
|
-
{ideaOpen && (
|
|
266
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-8)' }}>
|
|
267
|
-
<textarea
|
|
268
|
-
className={css.textarea}
|
|
269
|
-
style={{ minHeight: 60 }}
|
|
270
|
-
placeholder="例如:现代外卖员被雷劈穿越到修仙世界,靠祖传古玉捡漏发育,苟着苟着成了大佬…(≥50 字)"
|
|
271
|
-
value={idea}
|
|
272
|
-
onChange={e => { setIdea(e.target.value) }}
|
|
273
|
-
spellCheck={false}
|
|
274
|
-
/>
|
|
275
|
-
<div className={css.row} style={{ flexWrap: 'wrap' }}>
|
|
276
|
-
<button
|
|
277
|
-
type="button"
|
|
278
|
-
className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`}
|
|
279
|
-
disabled={suggesting || idea.trim().length < 10}
|
|
280
|
-
onClick={() => { void handleSuggest() }}
|
|
281
|
-
title="生成 3 个方向不同的大纲方案供选择(约消耗 6-8k token)"
|
|
282
|
-
>
|
|
283
|
-
{suggesting ? '⏳ 生成中…' : candidates.length === 0 ? '✨ 生成大纲方案' : `↻ 换一批(${Math.max(1, 3 - pinned.length)} 个)`}
|
|
284
|
-
</button>
|
|
285
|
-
{candidates.length > 0 && (
|
|
286
|
-
<span className={css.meta}>
|
|
287
|
-
已暂留 {pinned.length}/3 · 换批保留已暂留,只补新方案
|
|
288
|
-
</span>
|
|
289
|
-
)}
|
|
290
|
-
</div>
|
|
291
|
-
|
|
292
|
-
{/* 方案卡 */}
|
|
293
|
-
{candidates.length > 0 && (
|
|
294
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-8)' }}>
|
|
295
|
-
{candidates.map(candidate => {
|
|
296
|
-
const isPinned = pinned.includes(candidate.id)
|
|
297
|
-
return (
|
|
298
|
-
<div
|
|
299
|
-
key={candidate.id}
|
|
300
|
-
className={css.ideaCandidate}
|
|
301
|
-
style={isPinned ? { borderColor: 'var(--nf-accent)', boxShadow: '0 0 0 2px var(--nf-accent-soft)' } : undefined}
|
|
302
|
-
>
|
|
303
|
-
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap', gap: 'var(--nf-space-6)' }}>
|
|
304
|
-
<span style={{ display: 'flex', alignItems: 'center', gap: 'var(--nf-space-8)', flexWrap: 'wrap' }}>
|
|
305
|
-
<b>《{candidate.bookName}》</b>
|
|
306
|
-
{candidate.genre !== '' && <span className={css.badge} style={{ borderColor: 'var(--nf-accent)', color: 'var(--nf-accent)' }}>{candidate.genre}</span>}
|
|
307
|
-
{isPinned && <span className={css.badge} style={{ borderColor: 'var(--nf-warn)', color: 'var(--nf-warn)' }}>★ 已暂留</span>}
|
|
308
|
-
</span>
|
|
309
|
-
<span style={{ display: 'flex', gap: 'var(--nf-space-6)' }}>
|
|
310
|
-
<button
|
|
311
|
-
type="button"
|
|
312
|
-
className={`${css.button} ${css.buttonSmall} ${isPinned ? '' : css.buttonPrimary}`}
|
|
313
|
-
onClick={() => { togglePin(candidate.id) }}
|
|
314
|
-
title={isPinned ? '取消暂留:下次换批会覆盖此槽位' : '暂留此方案:换批时保留,继续对比新方案'}
|
|
315
|
-
>
|
|
316
|
-
{isPinned ? '★ 取消暂留' : '☆ 暂留'}
|
|
317
|
-
</button>
|
|
318
|
-
<button
|
|
319
|
-
type="button"
|
|
320
|
-
className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`}
|
|
321
|
-
onClick={() => { handlePick(candidate) }}
|
|
322
|
-
title="将这份大纲填入上方大纲框(可继续修改后开书)"
|
|
323
|
-
>
|
|
324
|
-
选这个
|
|
325
|
-
</button>
|
|
326
|
-
</span>
|
|
327
|
-
</div>
|
|
328
|
-
{candidate.sellingPoint !== '' && (
|
|
329
|
-
<div className={css.meta}><b>卖点:</b>{candidate.sellingPoint}</div>
|
|
330
|
-
)}
|
|
331
|
-
<div
|
|
332
|
-
className={css.meta}
|
|
333
|
-
style={{
|
|
334
|
-
display: '-webkit-box',
|
|
335
|
-
WebkitLineClamp: 4,
|
|
336
|
-
WebkitBoxOrient: 'vertical',
|
|
337
|
-
overflow: 'hidden',
|
|
338
|
-
whiteSpace: 'pre-wrap',
|
|
339
|
-
}}
|
|
340
|
-
title={candidate.outline}
|
|
341
|
-
>
|
|
342
|
-
{candidate.outline}
|
|
343
|
-
</div>
|
|
344
|
-
</div>
|
|
345
|
-
)
|
|
346
|
-
})}
|
|
347
|
-
{pinned.length >= 3 && (
|
|
348
|
-
<span className={css.meta}>已暂留全部方案——直接挑一个「选这个」即可;换批需先取消某个暂留。</span>
|
|
349
|
-
)}
|
|
350
|
-
</div>
|
|
351
|
-
)}
|
|
352
|
-
</div>
|
|
353
|
-
)}
|
|
354
|
-
</div>
|
|
355
|
-
|
|
356
|
-
<button
|
|
357
|
-
type="button"
|
|
358
|
-
className={`${css.button} ${css.buttonPrimary}`}
|
|
359
|
-
style={{ width: '100%', padding: 'var(--nf-space-10) 0', fontSize: 'var(--nf-fs-14)' }}
|
|
360
|
-
disabled={busy || effectiveName === ''}
|
|
361
|
-
onClick={() => { void handleCreate() }}
|
|
362
|
-
>
|
|
363
|
-
✨ 开书并进入工作台
|
|
364
|
-
</button>
|
|
365
|
-
<span className={css.meta} style={{ textAlign: 'center' }}>
|
|
366
|
-
未提供大纲也能开书,稍后可在大纲页导入
|
|
367
|
-
</span>
|
|
368
|
-
</div>
|
|
369
|
-
</div>
|
|
370
|
-
)
|
|
371
|
-
}
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 自动编辑:基于全书上下文,给出下一卷/阶段编排建议 + 修复再平衡。
|
|
3
|
-
* 生成后提供「傻瓜式」后续:一条条采纳 → 加入剧情线 / 加入待办,一键复制。
|
|
4
|
-
*/
|
|
5
|
-
import { useEffect, useState } from 'react'
|
|
6
|
-
import type { NovelApi } from '../api.ts'
|
|
7
|
-
import type { DirectorAdvice, DirectorTodo } from '../../protocol.ts'
|
|
8
|
-
import css from './panel.module.css'
|
|
9
|
-
|
|
10
|
-
export default function DirectorView({ api, todos, onTodosChange }: { api: NovelApi; todos: DirectorTodo[]; onTodosChange?: (todos: DirectorTodo[]) => void }): JSX.Element {
|
|
11
|
-
const [focus, setFocus] = useState('')
|
|
12
|
-
const [busy, setBusy] = useState(false)
|
|
13
|
-
const [error, setError] = useState('')
|
|
14
|
-
const [result, setResult] = useState<DirectorAdvice | null>(null)
|
|
15
|
-
const [adopted, setAdopted] = useState<Set<string>>(new Set())
|
|
16
|
-
const [todosBusy, setTodosBusy] = useState(false)
|
|
17
|
-
|
|
18
|
-
const refreshTodos = async (): Promise<void> => {
|
|
19
|
-
try {
|
|
20
|
-
const r = await api.directorTodosList()
|
|
21
|
-
onTodosChange?.(r.todos)
|
|
22
|
-
} catch { /* ignore */ }
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
useEffect(() => { void refreshTodos() }, [])
|
|
26
|
-
|
|
27
|
-
const markAdopted = (key: string): void => {
|
|
28
|
-
setAdopted(prev => new Set(prev).add(key))
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const run = async (): Promise<void> => {
|
|
32
|
-
setBusy(true); setError('')
|
|
33
|
-
try {
|
|
34
|
-
const r = await api.director(focus.trim())
|
|
35
|
-
setResult(r.result)
|
|
36
|
-
void refreshTodos()
|
|
37
|
-
} catch (e) { setError((e as Error).message) } finally { setBusy(false) }
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const adoptArc = async (s: string): Promise<void> => {
|
|
41
|
-
try {
|
|
42
|
-
await api.plotlines({ op: 'add', line: { id: '', name: s.slice(0, 16), kind: 'branch', goal: s, progress: '', status: 'active', chapters: [], createdAt: '' } })
|
|
43
|
-
markAdopted('arc:' + s)
|
|
44
|
-
} catch (e) { setError('加入剧情线失败:' + ((e as Error).message)) }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const adoptTodo = async (s: string, source: 'risk' | 'fix'): Promise<void> => {
|
|
48
|
-
try {
|
|
49
|
-
const r = await api.directorTodosAdd(s, source)
|
|
50
|
-
onTodosChange?.(r.todos)
|
|
51
|
-
markAdopted('todo:' + source + ':' + s)
|
|
52
|
-
} catch (e) { setError('加入待办失败:' + ((e as Error).message)) }
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const toggleTodo = async (id: string): Promise<void> => {
|
|
56
|
-
setTodosBusy(true)
|
|
57
|
-
try {
|
|
58
|
-
const r = await api.directorTodosToggle(id)
|
|
59
|
-
onTodosChange?.(r.todos)
|
|
60
|
-
} catch (e) { setError((e as Error).message) } finally { setTodosBusy(false) }
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const removeTodo = async (id: string): Promise<void> => {
|
|
64
|
-
setTodosBusy(true)
|
|
65
|
-
try {
|
|
66
|
-
const r = await api.directorTodosRemove(id)
|
|
67
|
-
onTodosChange?.(r.todos)
|
|
68
|
-
} catch (e) { setError((e as Error).message) } finally { setTodosBusy(false) }
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const copyAll = async (): Promise<void> => {
|
|
72
|
-
if (result === null) return
|
|
73
|
-
const text = [
|
|
74
|
-
'【自动编辑】',
|
|
75
|
-
`总体判断:${result.summary}`,
|
|
76
|
-
'',
|
|
77
|
-
'下一阶段节点:',
|
|
78
|
-
...(result.nextArc ?? []).map(s => `- ${s}`),
|
|
79
|
-
'',
|
|
80
|
-
`节奏板:${result.pacing}`,
|
|
81
|
-
'',
|
|
82
|
-
'风险提示:',
|
|
83
|
-
...(result.risks ?? []).map(s => `- ${s}`),
|
|
84
|
-
'',
|
|
85
|
-
'需要修复/再平衡:',
|
|
86
|
-
...(result.fixes ?? []).map(s => `- ${s}`),
|
|
87
|
-
].join('\n')
|
|
88
|
-
try { await navigator.clipboard.writeText(text); setError('') } catch (e) { setError('复制失败:' + ((e as Error).message)) }
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return (
|
|
92
|
-
<div className={css.authorPageBody}>
|
|
93
|
-
<div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-10)' }}>
|
|
94
|
-
<span className={css.cardTitle}>🎬 自动编辑</span>
|
|
95
|
-
<span className={css.meta}>结合本书分卷/剧情线/伏笔/最近事实,给出下一阶段编排 + 修复再平衡。可什么都不填直接生成。</span>
|
|
96
|
-
<textarea className={css.input} style={{ minHeight: 90, resize: 'vertical' }} value={focus} onChange={e => setFocus(e.target.value)} placeholder="可选:聚焦某个方向,如「主角成长节奏」「反派压迫感」「感情线处理」「下一卷转折」…" />
|
|
97
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
|
98
|
-
<button type="button" className={`${css.button} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void run() }}>
|
|
99
|
-
{busy ? '编排中…' : '生成编排建议'}
|
|
100
|
-
</button>
|
|
101
|
-
{result !== null && (
|
|
102
|
-
<button type="button" className={css.button} onClick={() => { void copyAll() }}>📋 一键复制</button>
|
|
103
|
-
)}
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
106
|
-
|
|
107
|
-
{error !== '' && <div style={{ color: 'var(--nf-error)', fontSize: 13 }}>⚠ {error}</div>}
|
|
108
|
-
|
|
109
|
-
{/* 傻瓜式指引:生成后置顶 */}
|
|
110
|
-
{result !== null && (
|
|
111
|
-
<div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-6)' }}>
|
|
112
|
-
<div style={{ fontSize: 14, fontWeight: 600 }}>🧭 这份建议怎么用(三步)</div>
|
|
113
|
-
<ol style={{ margin: 0, paddingLeft: 18, display: 'grid', gap: 4, fontSize: 13 }}>
|
|
114
|
-
<li>先看「风险 / 修复」,点每条旁的 <b>加入待办</b>,记下来逐个处理。</li>
|
|
115
|
-
<li>「下一阶段节点」每条可 <b>加入剧情线</b>,直接把下一步方向写进本书。</li>
|
|
116
|
-
<li>「节奏板」对照你当前写到的地方,看该加快还是放慢;不确定就点 <b>一键复制</b> 保存。</li>
|
|
117
|
-
</ol>
|
|
118
|
-
</div>
|
|
119
|
-
)}
|
|
120
|
-
|
|
121
|
-
{result !== null && (
|
|
122
|
-
<div style={{ display: 'grid', gap: 'var(--nf-space-10)' }}>
|
|
123
|
-
<div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-6)' }}>
|
|
124
|
-
<div style={{ fontSize: 15, fontWeight: 600 }}>总体判断</div>
|
|
125
|
-
<div style={{ fontSize: 13 }}>{result.summary}</div>
|
|
126
|
-
</div>
|
|
127
|
-
|
|
128
|
-
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: 'var(--nf-space-10)' }}>
|
|
129
|
-
<div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-6)' }}>
|
|
130
|
-
<div style={{ fontSize: 15, fontWeight: 600 }}>下一阶段剧情节点</div>
|
|
131
|
-
<ol style={{ margin: 0, paddingLeft: 18, display: 'grid', gap: 6, fontSize: 13 }}>
|
|
132
|
-
{(result.nextArc ?? []).map((s, i) => (
|
|
133
|
-
<li key={i} style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
|
|
134
|
-
<span style={{ flex: 1, minWidth: 160 }}>{s}</span>
|
|
135
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={adopted.has('arc:' + s)} onClick={() => { void adoptArc(s) }}>
|
|
136
|
-
{adopted.has('arc:' + s) ? '已加入' : '采纳 → 加入剧情线'}
|
|
137
|
-
</button>
|
|
138
|
-
</li>
|
|
139
|
-
))}
|
|
140
|
-
</ol>
|
|
141
|
-
</div>
|
|
142
|
-
<div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-6)' }}>
|
|
143
|
-
<div style={{ fontSize: 15, fontWeight: 600 }}>节奏板</div>
|
|
144
|
-
<div style={{ fontSize: 13 }}>{result.pacing}</div>
|
|
145
|
-
</div>
|
|
146
|
-
</div>
|
|
147
|
-
|
|
148
|
-
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: 'var(--nf-space-10)' }}>
|
|
149
|
-
<div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-6)' }}>
|
|
150
|
-
<div style={{ fontSize: 15, fontWeight: 600 }}>风险提示</div>
|
|
151
|
-
<ul style={{ margin: 0, paddingLeft: 18, display: 'grid', gap: 6, fontSize: 13 }}>
|
|
152
|
-
{(result.risks ?? []).map((s, i) => (
|
|
153
|
-
<li key={i} style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
|
|
154
|
-
<span style={{ flex: 1, minWidth: 160 }}>{s}</span>
|
|
155
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={adopted.has('todo:risk:' + s)} onClick={() => { void adoptTodo(s, 'risk') }}>
|
|
156
|
-
{adopted.has('todo:risk:' + s) ? '已加入' : '采纳 → 加入待办'}
|
|
157
|
-
</button>
|
|
158
|
-
</li>
|
|
159
|
-
))}
|
|
160
|
-
</ul>
|
|
161
|
-
</div>
|
|
162
|
-
<div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-6)' }}>
|
|
163
|
-
<div style={{ fontSize: 15, fontWeight: 600 }}>需要修复/再平衡</div>
|
|
164
|
-
<ul style={{ margin: 0, paddingLeft: 18, display: 'grid', gap: 6, fontSize: 13 }}>
|
|
165
|
-
{(result.fixes ?? []).map((s, i) => (
|
|
166
|
-
<li key={i} style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
|
|
167
|
-
<span style={{ flex: 1, minWidth: 160 }}>{s}</span>
|
|
168
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={adopted.has('todo:fix:' + s)} onClick={() => { void adoptTodo(s, 'fix') }}>
|
|
169
|
-
{adopted.has('todo:fix:' + s) ? '已加入' : '采纳 → 加入待办'}
|
|
170
|
-
</button>
|
|
171
|
-
</li>
|
|
172
|
-
))}
|
|
173
|
-
</ul>
|
|
174
|
-
</div>
|
|
175
|
-
</div>
|
|
176
|
-
|
|
177
|
-
{/* 已采纳待办清单(傻瓜式:勾选处理,做完删掉) */}
|
|
178
|
-
<div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-6)' }}>
|
|
179
|
-
<div style={{ fontSize: 15, fontWeight: 600 }}>🎬 编辑待办(已采纳)</div>
|
|
180
|
-
{todos.length === 0 ? (
|
|
181
|
-
<span className={css.meta}>还没有。点上面「加入待办」,把风险/修复记到这里,做完勾掉。这些也会出现在工作流主页的「待办队列」。</span>
|
|
182
|
-
) : (
|
|
183
|
-
<ul style={{ margin: 0, paddingLeft: 0, listStyle: 'none', display: 'grid', gap: 6, fontSize: 13 }}>
|
|
184
|
-
{todos.map(t => (
|
|
185
|
-
<li key={t.id} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
186
|
-
<label style={{ display: 'flex', alignItems: 'center', gap: 6, flex: 1, cursor: 'pointer' }}>
|
|
187
|
-
<input type="checkbox" checked={t.done} disabled={todosBusy} onChange={() => { void toggleTodo(t.id) }} />
|
|
188
|
-
<span style={{ textDecoration: t.done ? 'line-through' : 'none', color: t.done ? 'var(--nf-text-2)' : undefined }}>{t.text}</span>
|
|
189
|
-
</label>
|
|
190
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} title="删除" onClick={() => { void removeTodo(t.id) }}>×</button>
|
|
191
|
-
</li>
|
|
192
|
-
))}
|
|
193
|
-
</ul>
|
|
194
|
-
)}
|
|
195
|
-
</div>
|
|
196
|
-
</div>
|
|
197
|
-
)}
|
|
198
|
-
</div>
|
|
199
|
-
)
|
|
200
|
-
}
|
|
@@ -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
|
-
}
|