@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,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 首页 AI 进度视图:书架级聚合进度——总览 + 每本书进度条 + 进行中高亮。
|
|
3
|
-
* 读取 BookshelfSnapshot,不依赖打开某本书。
|
|
4
|
-
*/
|
|
5
|
-
import { useMemo } from 'react'
|
|
6
|
-
import { BarChart3, BookOpen, PenLine, CheckCircle2, Clock, RefreshCw } from 'lucide-react'
|
|
7
|
-
import type { BookshelfSnapshot } from '../../protocol.ts'
|
|
8
|
-
import css from './panel.module.css'
|
|
9
|
-
|
|
10
|
-
function relativeTime(iso: string): string {
|
|
11
|
-
const t = new Date(iso).getTime()
|
|
12
|
-
if (!Number.isFinite(t)) return '—'
|
|
13
|
-
const diff = Date.now() - t
|
|
14
|
-
if (diff < 60_000) return '刚刚'
|
|
15
|
-
if (diff < 3_600_000) return Math.floor(diff / 60_000) + ' 分钟前'
|
|
16
|
-
if (diff < 86_400_000) return Math.floor(diff / 3_600_000) + ' 小时前'
|
|
17
|
-
if (diff < 7 * 86_400_000) return Math.floor(diff / 86_400_000) + ' 天前'
|
|
18
|
-
return new Date(t).toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric' })
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function bookStatus(book: BookshelfSnapshot['books'][number]): 'none' | 'done' | 'active' {
|
|
22
|
-
if (!book.hasProject) return 'none'
|
|
23
|
-
return book.total > 0 && book.done >= book.total ? 'done' : 'active'
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function ProgressHomeView({ shelf, onOpenBook }: {
|
|
27
|
-
shelf: BookshelfSnapshot;
|
|
28
|
-
onOpenBook: (id: string) => void;
|
|
29
|
-
}) {
|
|
30
|
-
const stats = useMemo(() => {
|
|
31
|
-
let active = 0, done = 0, none = 0, chapters = 0, written = 0;
|
|
32
|
-
for (const b of shelf.books) {
|
|
33
|
-
const s = bookStatus(b)
|
|
34
|
-
if (s === 'none') none++; else if (s === 'done') done++; else active++;
|
|
35
|
-
chapters += b.total;
|
|
36
|
-
written += b.done;
|
|
37
|
-
}
|
|
38
|
-
return { active, done, none, chapters, written }
|
|
39
|
-
}, [shelf.books]);
|
|
40
|
-
|
|
41
|
-
const progressPct = stats.chapters > 0 ? Math.round((stats.written / stats.chapters) * 100) : 0;
|
|
42
|
-
|
|
43
|
-
return (
|
|
44
|
-
<div className={css.authorPageBody}>
|
|
45
|
-
<div className={css.card + ' ' + css.settingsCard} style={{ gap: 'var(--nf-space-6)' }}><h2 className={css.panelTitle} style={{ margin: 0 }}>📊 AI 进度</h2><span className={css.meta}>跨书架聚合进度与最近活动</span></div>
|
|
46
|
-
|
|
47
|
-
<div className={css.progressOverview}>
|
|
48
|
-
<div className={css.progressOverviewCard}><span className={css.progressOverviewNum}>{shelf.books.length}</span><span className={css.meta}>总书数</span></div>
|
|
49
|
-
<div className={css.progressOverviewCard}><span className={css.progressOverviewNum}>{stats.active}</span><span className={css.meta}>进行中</span></div>
|
|
50
|
-
<div className={css.progressOverviewCard}><span className={css.progressOverviewNum}>{stats.done}</span><span className={css.meta}>已完结</span></div>
|
|
51
|
-
<div className={css.progressOverviewCard}><span className={css.progressOverviewNum}>{stats.written}/{stats.chapters}</span><span className={css.meta}>已完成章节</span></div>
|
|
52
|
-
</div>
|
|
53
|
-
|
|
54
|
-
<div className={css.progressTotalBar}><div className={css.progressTotalFill} style={{ width: progressPct + '%' }} /><span className={css.meta}>全书架整体进度 {progressPct}%</span></div>
|
|
55
|
-
|
|
56
|
-
{shelf.books.length === 0 ? (
|
|
57
|
-
<div className={css.shelfEmpty} style={{ minHeight: 160 }}>
|
|
58
|
-
<span className={css.shelfEmptyIcon}><BarChart3 size={30} /></span>
|
|
59
|
-
<span className={css.shelfEmptyTitle}>还没有书</span>
|
|
60
|
-
<span className={css.meta}>去书架开一本书或导入全文后,这里会显示进度。</span>
|
|
61
|
-
</div>
|
|
62
|
-
) : (
|
|
63
|
-
<div className={css.progressBookList}>
|
|
64
|
-
{shelf.books.map(book => {
|
|
65
|
-
const s = bookStatus(book)
|
|
66
|
-
const ratio = book.total > 0 ? Math.min(book.done / book.total, 1) : 0;
|
|
67
|
-
const statusLabel = s === 'none' ? '未开书' : s === 'done' ? '已完结' : '进行中';
|
|
68
|
-
return (
|
|
69
|
-
<div key={book.id} className={css.progressBook + (s === 'active' ? ' ' + css.progressBookActive : '')}>
|
|
70
|
-
<div className={css.progressBookHead}>
|
|
71
|
-
<span className={css.progressBookName} title={book.bookName}>{book.bookName}</span>
|
|
72
|
-
<span className={css.badge + (s === 'done' ? ' ' + css.badgeDone : s === 'active' ? ' ' + css.badgeWritten : ' ' + css.badgePending)}>{statusLabel}</span>
|
|
73
|
-
{s === 'active' && <span className={css.meta + ' ' + css.progressLive}><RefreshCw size={12} style={{ verticalAlign: -2 }} /> 创作中</span>}
|
|
74
|
-
</div>
|
|
75
|
-
<div className={css.progressBookBar}><div className={css.progressBookFill} style={{ width: Math.round(ratio * 100) + '%' }} /></div>
|
|
76
|
-
<div className={css.rowEnd} style={{ justifyContent: 'space-between' }}>
|
|
77
|
-
<span className={css.meta}>{book.total > 0 ? '已完成 ' + book.done + ' / ' + book.total + ' 章' : '尚未规划章节'} · 更新于 {relativeTime(book.updatedAt)}</span>
|
|
78
|
-
<button type="button" className={css.button + ' ' + css.buttonSmall} onClick={() => onOpenBook(book.id)}><PenLine size={12} style={{ verticalAlign: -2 }} /> 进入</button>
|
|
79
|
-
</div>
|
|
80
|
-
</div>
|
|
81
|
-
);
|
|
82
|
-
})}
|
|
83
|
-
</div>
|
|
84
|
-
)}
|
|
85
|
-
</div>
|
|
86
|
-
);
|
|
87
|
-
}
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 道具库:从已写章节自动提炼「常驻道具」(跨镜头需保持一致)→ 注入每个分镜提示词。
|
|
3
|
-
* 支持:AI 提炼 / 手动增删改 / 保存;道具一行统一外观描述,提示词按此外观保持跨镜头一致。
|
|
4
|
-
*/
|
|
5
|
-
import { useState } from 'react'
|
|
6
|
-
import type { NovelApi } from '../api.ts'
|
|
7
|
-
import type { ProjectState, Prop } from '../../protocol.ts'
|
|
8
|
-
import css from './panel.module.css'
|
|
9
|
-
|
|
10
|
-
export function PropLibrary({
|
|
11
|
-
api,
|
|
12
|
-
project,
|
|
13
|
-
refresh,
|
|
14
|
-
onProgress,
|
|
15
|
-
}: {
|
|
16
|
-
api: NovelApi
|
|
17
|
-
project: ProjectState | null
|
|
18
|
-
refresh: () => void | Promise<void>
|
|
19
|
-
onProgress?: (text: string, kind?: 'info' | 'done' | 'error') => void
|
|
20
|
-
}) {
|
|
21
|
-
const [busy, setBusy] = useState(false)
|
|
22
|
-
const [error, setError] = useState('')
|
|
23
|
-
const [notice, setNotice] = useState('')
|
|
24
|
-
const [editing, setEditing] = useState<Prop | null>(null)
|
|
25
|
-
const [extracted, setExtracted] = useState<Prop[] | null>(null)
|
|
26
|
-
|
|
27
|
-
const notify = (msg: string): void => { setNotice(msg); setTimeout(() => { setNotice('') }, 3000) }
|
|
28
|
-
const report = (text: string, kind: 'info' | 'done' | 'error' = 'info'): void => { onProgress?.(text, kind) }
|
|
29
|
-
|
|
30
|
-
const props = (project?.props ?? []).map(p => ({ ...p }))
|
|
31
|
-
|
|
32
|
-
/** AI 提炼:从已写章节识别常驻道具,暂存为候选,待保存采纳。 */
|
|
33
|
-
const extract = async (): Promise<void> => {
|
|
34
|
-
setBusy(true); setError('')
|
|
35
|
-
report('从已写章节提炼常驻道具…')
|
|
36
|
-
try {
|
|
37
|
-
const res = await api.mangaProps({ op: 'extract' })
|
|
38
|
-
setExtracted(res.props ?? [])
|
|
39
|
-
await refresh()
|
|
40
|
-
report('道具提炼完成:' + (res.props ?? []).length + ' 个常驻道具', 'done')
|
|
41
|
-
} catch (err) {
|
|
42
|
-
const m = (err as Error).message
|
|
43
|
-
setError(m)
|
|
44
|
-
report('道具提炼失败:' + m, 'error')
|
|
45
|
-
} finally { setBusy(false) }
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** 保存:整体替换道具库(候选 / 手动清单)。 */
|
|
49
|
-
const save = async (list: Prop[]): Promise<void> => {
|
|
50
|
-
const cleaned = list.map(p => ({ name: p.name.trim(), desc: p.desc.trim() })).filter(p => p.name !== '')
|
|
51
|
-
if (cleaned.length === 0) { setError('道具名为空,无法保存'); return }
|
|
52
|
-
setBusy(true); setError('')
|
|
53
|
-
report('保存道具库…')
|
|
54
|
-
try {
|
|
55
|
-
await api.mangaProps({ op: 'save', props: cleaned })
|
|
56
|
-
await refresh()
|
|
57
|
-
setExtracted(null)
|
|
58
|
-
setEditing(null)
|
|
59
|
-
report('道具库已保存:' + cleaned.length + ' 个', 'done')
|
|
60
|
-
} catch (err) {
|
|
61
|
-
const m = (err as Error).message
|
|
62
|
-
setError(m)
|
|
63
|
-
report('保存道具库失败:' + m, 'error')
|
|
64
|
-
} finally { setBusy(false) }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (project === null) {
|
|
68
|
-
return <div className={css.card}><span className={css.meta}>请先开书或选择一本书,再进入道具库。</span></div>
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const editingList: Prop[] = extracted ?? props
|
|
72
|
-
|
|
73
|
-
return (
|
|
74
|
-
<div className={css.card} style={{ flex: 1, minHeight: 0 }}>
|
|
75
|
-
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
76
|
-
<span className={css.cardTitle} style={{ fontSize: 'var(--nf-fs-16)', fontWeight: 700 }}>🎒 道具库</span>
|
|
77
|
-
<span className={css.meta}>常驻道具(跨镜头需保持一致):提炼自已写章节,自动注入每个分镜提示词</span>
|
|
78
|
-
</div>
|
|
79
|
-
<span className={css.meta} style={{ display: 'block', marginTop: 'var(--nf-space-2)' }}>
|
|
80
|
-
流程位置:道具库(选做) · 前置:已写章节 · 作用:分镜提示词里道具外观跨镜头统一
|
|
81
|
-
</span>
|
|
82
|
-
|
|
83
|
-
<div className={css.row} style={{ flexWrap: 'wrap', gap: 'var(--nf-space-6)', margin: '8px 0' }}>
|
|
84
|
-
<button type="button" className={`${css.button} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void extract() }}>
|
|
85
|
-
{busy ? '⏳ 提炼中…' : '✨ 提炼道具'}
|
|
86
|
-
</button>
|
|
87
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setEditing({ name: '', desc: '' }) }}>+ 手动添加</button>
|
|
88
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { void refresh(); report('道具库已刷新', 'done') }}>🔄 刷新</button>
|
|
89
|
-
</div>
|
|
90
|
-
|
|
91
|
-
{error !== '' && <div className={css.importError}>{error}</div>}
|
|
92
|
-
{notice !== '' && <div className={css.importResult} style={{ padding: 'var(--nf-space-6) var(--nf-space-10)' }}>{notice}</div>}
|
|
93
|
-
|
|
94
|
-
{/* 提炼候选:待保存 */}
|
|
95
|
-
{extracted !== null && (
|
|
96
|
-
<div style={{ border: '1px solid var(--nf-accent)', borderRadius: 'var(--nf-radius-10)', padding: 'var(--nf-space-10)', marginBottom: 'var(--nf-space-10)' }}>
|
|
97
|
-
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
98
|
-
<b style={{ fontSize: 'var(--nf-fs-14)' }}>✨ AI 提炼结果(确认/编辑后保存)</b>
|
|
99
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setExtracted(null) }}>取消</button>
|
|
100
|
-
</div>
|
|
101
|
-
{extracted.length === 0
|
|
102
|
-
? <span className={css.meta} style={{ display: 'block', marginTop: 'var(--nf-space-6)' }}>未识别到明显常驻道具(仅一次性出现的道具不列入)。可手动添加。</span>
|
|
103
|
-
: <PropRows rows={editingList} setRows={setExtracted} />}
|
|
104
|
-
{extracted.length > 0 && (
|
|
105
|
-
<div className={css.row} style={{ marginTop: 'var(--nf-space-8)', gap: 'var(--nf-space-6)' }}>
|
|
106
|
-
<button type="button" className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void save(extracted) }}>保存道具库</button>
|
|
107
|
-
<span className={css.meta}>保存后会注入所有分镜提示词,跨镜头统一此道具外观</span>
|
|
108
|
-
</div>
|
|
109
|
-
)}
|
|
110
|
-
</div>
|
|
111
|
-
)}
|
|
112
|
-
|
|
113
|
-
{/* 手动添加/编辑行 */}
|
|
114
|
-
{editing !== null && extracted === null && (
|
|
115
|
-
<div style={{ border: '1px solid var(--nf-accent)', borderRadius: 'var(--nf-radius-10)', padding: 'var(--nf-space-10)', marginBottom: 'var(--nf-space-10)', display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)' }}>
|
|
116
|
-
<b style={{ fontSize: 'var(--nf-fs-14)' }}>编辑道具</b>
|
|
117
|
-
<input className={css.input} style={{ fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-4) var(--nf-space-8)' }} placeholder="道具名(如 外卖电动车)" value={editing.name} onChange={e => { setEditing({ ...editing, name: e.target.value }) }} />
|
|
118
|
-
<input className={css.input} style={{ fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-4) var(--nf-space-8)' }} placeholder="一行统一外观描述(颜色/材质/状态)" value={editing.desc} onChange={e => { setEditing({ ...editing, desc: e.target.value }) }} />
|
|
119
|
-
<div className={css.row} style={{ gap: 'var(--nf-space-6)' }}>
|
|
120
|
-
<button type="button" className={`${css.button} ${css.buttonPrimary}`} disabled={busy || editing.name.trim() === ''} onClick={() => {
|
|
121
|
-
const list = props
|
|
122
|
-
const idx = list.findIndex(p => p.name === editing.name)
|
|
123
|
-
if (idx >= 0) list[idx] = editing
|
|
124
|
-
else list.push(editing)
|
|
125
|
-
void save(list)
|
|
126
|
-
}}>保存</button>
|
|
127
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setEditing(null) }}>取消</button>
|
|
128
|
-
</div>
|
|
129
|
-
</div>
|
|
130
|
-
)}
|
|
131
|
-
|
|
132
|
-
{props.length === 0 && extracted === null ? (
|
|
133
|
-
<div className={css.shelfEmpty} style={{ minHeight: 140, flex: 1 }}>
|
|
134
|
-
<span className={css.shelfEmptyIcon}>🎒</span>
|
|
135
|
-
<span className={css.shelfEmptyTitle}>道具库为空</span>
|
|
136
|
-
<span className={css.meta}>点「✨ 提炼道具」自动识别,或「+ 手动添加」</span>
|
|
137
|
-
</div>
|
|
138
|
-
) : (
|
|
139
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)' }}>
|
|
140
|
-
{props.map(p => (
|
|
141
|
-
<div key={p.name} style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-10)', padding: 'var(--nf-space-8) var(--nf-space-10)', display: 'flex', alignItems: 'center', gap: 'var(--nf-space-10)' }}>
|
|
142
|
-
<span style={{ fontSize: 'var(--nf-fs-14)', fontWeight: 600, minWidth: 110 }}>{p.name}</span>
|
|
143
|
-
<span className={css.meta} style={{ flex: 1 }}>{p.desc}</span>
|
|
144
|
-
<span className={css.row} style={{ gap: 'var(--nf-space-4)' }}>
|
|
145
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setEditing({ ...p }) }}>✏️ 编辑</button>
|
|
146
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} style={{ color: 'var(--nf-danger, #e05)' }} onClick={() => { void save(props.filter(x => x.name !== p.name)) }}>🗑</button>
|
|
147
|
-
</span>
|
|
148
|
-
</div>
|
|
149
|
-
))}
|
|
150
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} style={{ alignSelf: 'flex-start' }} onClick={() => { void refresh() }}>已是最新</button>
|
|
151
|
-
</div>
|
|
152
|
-
)}
|
|
153
|
-
</div>
|
|
154
|
-
)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/** 可编辑道具行(提炼候选用)。 */
|
|
158
|
-
function PropRows({ rows, setRows }: { rows: Prop[]; setRows: (r: Prop[]) => void }) {
|
|
159
|
-
const set = (idx: number, patch: Partial<Prop>): void => {
|
|
160
|
-
const next = rows.map((r, i) => (i === idx ? { ...r, ...patch } : r))
|
|
161
|
-
setRows(next)
|
|
162
|
-
}
|
|
163
|
-
return (
|
|
164
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-4)', marginTop: 'var(--nf-space-6)' }}>
|
|
165
|
-
{rows.map((p, i) => (
|
|
166
|
-
<div key={i} className={css.row} style={{ gap: 'var(--nf-space-6)' }}>
|
|
167
|
-
<input className={css.input} style={{ flex: '0 0 140px', fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-4) var(--nf-space-8)' }} value={p.name} onChange={e => { set(i, { name: e.target.value }) }} />
|
|
168
|
-
<input className={css.input} style={{ flex: 1, fontSize: 'var(--nf-fs-12)', padding: 'var(--nf-space-4) var(--nf-space-8)' }} value={p.desc} onChange={e => { set(i, { desc: e.target.value }) }} />
|
|
169
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setRows(rows.filter((_, idx) => idx !== i)) }}>删</button>
|
|
170
|
-
</div>
|
|
171
|
-
))}
|
|
172
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} style={{ alignSelf: 'flex-start' }} onClick={() => { setRows([...rows, { name: '', desc: '' }]) }}>+ 添加</button>
|
|
173
|
-
</div>
|
|
174
|
-
)
|
|
175
|
-
}
|