@waterwx/dsh-novel-forge 2.1.0-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 -457
- package/lib/client.js +20495 -22316
- package/lib/index.js +4640 -3932
- package/lib/types/action-guard.d.ts +61 -0
- package/lib/types/ai-scan.d.ts +0 -6
- 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/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/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 -235
- 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 -345
- 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,339 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AI 助手(编辑老师):与 AI 编辑对话讨论剧情,助手通过动作指令直接修改
|
|
3
|
-
* 大纲 / 道藏 / 章节。流式渲染回复;工具调用显示为步骤卡片(中文名 +
|
|
4
|
-
* 状态 + 耗时),顶部状态条展示当前在做什么;支持清空对话记录。
|
|
5
|
-
*/
|
|
6
|
-
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
7
|
-
import type { NovelApi } from '../api.ts'
|
|
8
|
-
import { tt } from './helpers.ts'
|
|
9
|
-
import type { AssistantFrame, AssistantMessage } from '../../protocol.ts'
|
|
10
|
-
import css from './panel.module.css'
|
|
11
|
-
|
|
12
|
-
/** 工具中文名 + 图标映射(作者看得懂)。 */
|
|
13
|
-
const TOOL_LABELS: Record<string, { icon: string; label: string }> = {
|
|
14
|
-
book_overview: { icon: '📖', label: '读取全书上下文' },
|
|
15
|
-
impact_analysis: { icon: '🔗', label: '影响分析' },
|
|
16
|
-
outline_text: { icon: '📄', label: '读取大纲' },
|
|
17
|
-
outline_replace: { icon: '📝', label: '修改大纲' },
|
|
18
|
-
bible_set_rule: { icon: '📖', label: '修改设定规则' },
|
|
19
|
-
bible_set_redline: { icon: '🚫', label: '修改写作红线' },
|
|
20
|
-
chapter_text: { icon: '📄', label: '读取章节' },
|
|
21
|
-
chapter_rewrite: { icon: '✏️', label: '修订章节' },
|
|
22
|
-
chapter_generate: { icon: '✨', label: '生成章节' },
|
|
23
|
-
chapter_review: { icon: '🔍', label: 'AI 审稿' },
|
|
24
|
-
foreshadow_add: { icon: '🪤', label: '新增伏笔' },
|
|
25
|
-
foreshadow_update: { icon: '🪤', label: '更新伏笔' },
|
|
26
|
-
export_txt: { icon: '📦', label: '导出 TXT' },
|
|
27
|
-
assets_status: { icon: '🎨', label: '查看写作资产' },
|
|
28
|
-
assets_set_genre: { icon: '🏷️', label: '设置题材' },
|
|
29
|
-
assets_set_progression: { icon: '📈', label: '设置推进模式' },
|
|
30
|
-
assets_add_rule: { icon: '🚫', label: '新增反AI规则' },
|
|
31
|
-
book_analysis: { icon: '📊', label: '拆书分析' },
|
|
32
|
-
director_advice: { icon: '🎬', label: '自动导演' },
|
|
33
|
-
knowledge_add: { icon: '📚', label: '加入知识库' },
|
|
34
|
-
knowledge_search: { icon: '🔍', label: '检索知识库' },
|
|
35
|
-
plotline_list: { icon: '🧵', label: '查看剧情线' },
|
|
36
|
-
plotline_add: { icon: '🧵', label: '新增剧情线' },
|
|
37
|
-
director_todo_add: { icon: '✅', label: '记入导演待办' },
|
|
38
|
-
director_todo_list: { icon: '✅', label: '查看导演待办' },
|
|
39
|
-
knowledge_list: { icon: '📚', label: '列出全部知识库' },
|
|
40
|
-
breakdown: { icon: '🔍', label: '拆书分析' },
|
|
41
|
-
audit: { icon: '🛡️', label: '全书质检' },
|
|
42
|
-
blurb: { icon: '📄', label: '生成简介' },
|
|
43
|
-
error: { icon: '⚠️', label: '出错了' },
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** 需要渲染成结构化结果卡片的工具(其余工具结果仍走文字回复,避免大段章节/总纲刷屏)。 */
|
|
47
|
-
const CARD_TOOLS = new Set(['director_advice', 'book_analysis', 'breakdown', 'audit', 'knowledge_list', 'plotline_list', 'director_todo_list'])
|
|
48
|
-
|
|
49
|
-
/** One chat bubble (either side). */
|
|
50
|
-
interface ChatLine {
|
|
51
|
-
id: number
|
|
52
|
-
role: 'user' | 'assistant'
|
|
53
|
-
text: string
|
|
54
|
-
/** Tool steps interleaved with the assistant reply. */
|
|
55
|
-
tools: Array<{ name: string; status: 'start' | 'done' | 'error'; detail?: string; startedAt?: number; elapsedMs?: number }>
|
|
56
|
-
/** Live output while a tool runs (generated text streamed into the bubble). */
|
|
57
|
-
live?: string
|
|
58
|
-
/** 结构化工具完整结果(用于渲染成卡片,如拆书/导演/质检/知识库列表)。 */
|
|
59
|
-
resultCard?: { name: string; text: string }
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Props. */
|
|
63
|
-
export interface AssistantTabProps {
|
|
64
|
-
api: NovelApi
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** The assistant conversation tab. */
|
|
68
|
-
export function AssistantTab({ api }: AssistantTabProps) {
|
|
69
|
-
const [lines, setLines] = useState<ChatLine[]>([])
|
|
70
|
-
const [input, setInput] = useState('')
|
|
71
|
-
const [busy, setBusy] = useState(false)
|
|
72
|
-
const [error, setError] = useState('')
|
|
73
|
-
const [notice, setNotice] = useState('')
|
|
74
|
-
/** 思考耗时(秒,busy 时每秒刷新)。 */
|
|
75
|
-
const [thinkSeconds, setThinkSeconds] = useState(0)
|
|
76
|
-
const idRef = useRef(0)
|
|
77
|
-
const scrollRef = useRef<HTMLDivElement | null>(null)
|
|
78
|
-
|
|
79
|
-
/** busy 计时:显示"已思考 Xs",让作者知道在干活。 */
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
if (!busy) {
|
|
82
|
-
setThinkSeconds(0)
|
|
83
|
-
return
|
|
84
|
-
}
|
|
85
|
-
const started = Date.now()
|
|
86
|
-
const timer = window.setInterval(() => {
|
|
87
|
-
setThinkSeconds(Math.floor((Date.now() - started) / 1000))
|
|
88
|
-
}, 1000)
|
|
89
|
-
return () => { window.clearInterval(timer) }
|
|
90
|
-
}, [busy])
|
|
91
|
-
|
|
92
|
-
/** Append a bubble (or extend the current assistant bubble). */
|
|
93
|
-
const pushLine = useCallback((line: Omit<ChatLine, 'id'>) => {
|
|
94
|
-
setLines(prev => {
|
|
95
|
-
const last = prev[prev.length - 1]
|
|
96
|
-
// Extend the live assistant bubble while streaming.
|
|
97
|
-
if (line.role === 'assistant' && last !== undefined && last.role === 'assistant' && last.tools.length === 0) {
|
|
98
|
-
return [...prev.slice(0, -1), { ...last, text: last.text + line.text }]
|
|
99
|
-
}
|
|
100
|
-
return [...prev, { ...line, id: idRef.current++ }]
|
|
101
|
-
})
|
|
102
|
-
}, [])
|
|
103
|
-
|
|
104
|
-
/** Push a tool event: start 创建步骤,done/error 更新同名步骤(含耗时)。 */
|
|
105
|
-
const pushTool = useCallback((tool: ChatLine['tools'][number]) => {
|
|
106
|
-
setLines(prev => {
|
|
107
|
-
const last = prev[prev.length - 1]
|
|
108
|
-
if (last === undefined || last.role !== 'assistant') {
|
|
109
|
-
return [...prev, {
|
|
110
|
-
id: idRef.current++,
|
|
111
|
-
role: 'assistant',
|
|
112
|
-
text: '',
|
|
113
|
-
tools: [{ ...tool, startedAt: tool.status === 'start' ? Date.now() : undefined }],
|
|
114
|
-
}]
|
|
115
|
-
}
|
|
116
|
-
const tools = [...last.tools]
|
|
117
|
-
if (tool.status === 'start') {
|
|
118
|
-
tools.push({ ...tool, startedAt: Date.now() })
|
|
119
|
-
} else {
|
|
120
|
-
// 找到同名未完成的步骤,更新状态与耗时。
|
|
121
|
-
for (let i = tools.length - 1; i >= 0; i--) {
|
|
122
|
-
const step = tools[i]!
|
|
123
|
-
if (step.name === tool.name && step.status === 'start') {
|
|
124
|
-
tools[i] = {
|
|
125
|
-
...step,
|
|
126
|
-
status: tool.status,
|
|
127
|
-
detail: tool.detail,
|
|
128
|
-
elapsedMs: Date.now() - (step.startedAt ?? Date.now()),
|
|
129
|
-
}
|
|
130
|
-
break
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return [...prev.slice(0, -1), { ...last, tools, live: undefined }]
|
|
135
|
-
})
|
|
136
|
-
}, [])
|
|
137
|
-
|
|
138
|
-
/** Append live tool output onto the current assistant bubble. */
|
|
139
|
-
const pushToolDelta = useCallback((text: string) => {
|
|
140
|
-
setLines(prev => {
|
|
141
|
-
const last = prev[prev.length - 1]
|
|
142
|
-
if (last === undefined || last.role !== 'assistant') return prev
|
|
143
|
-
return [...prev.slice(0, -1), { ...last, live: (last.live ?? '') + text }]
|
|
144
|
-
})
|
|
145
|
-
}, [])
|
|
146
|
-
|
|
147
|
-
/** 把结构化工具的结果挂到当前助手气泡(用于渲染结果卡片)。 */
|
|
148
|
-
const pushResultCard = useCallback((card: { name: string; text: string }) => {
|
|
149
|
-
setLines(prev => {
|
|
150
|
-
const last = prev[prev.length - 1]
|
|
151
|
-
if (last === undefined || last.role !== 'assistant') {
|
|
152
|
-
return [...prev, { id: idRef.current++, role: 'assistant', text: '', tools: [], resultCard: card }]
|
|
153
|
-
}
|
|
154
|
-
return [...prev.slice(0, -1), { ...last, resultCard: card }]
|
|
155
|
-
})
|
|
156
|
-
}, [])
|
|
157
|
-
|
|
158
|
-
/** 当前进行中的工具(状态条显示)。 */
|
|
159
|
-
const activeTool = (() => {
|
|
160
|
-
if (!busy) return null
|
|
161
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
162
|
-
const line = lines[i]!
|
|
163
|
-
if (line.role !== 'assistant') continue
|
|
164
|
-
for (let j = line.tools.length - 1; j >= 0; j--) {
|
|
165
|
-
if (line.tools[j]!.status === 'start') return line.tools[j]!
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return null
|
|
169
|
-
})()
|
|
170
|
-
|
|
171
|
-
/** Load persisted history on mount. */
|
|
172
|
-
useEffect(() => {
|
|
173
|
-
let cancelled = false
|
|
174
|
-
void (async () => {
|
|
175
|
-
try {
|
|
176
|
-
const history = await api.assistantHistory()
|
|
177
|
-
if (cancelled) return
|
|
178
|
-
const restored: ChatLine[] = []
|
|
179
|
-
for (const entry of history) {
|
|
180
|
-
if (entry.role === 'user') {
|
|
181
|
-
restored.push({ id: idRef.current++, role: 'user', text: entry.content, tools: [] })
|
|
182
|
-
} else if (entry.role === 'assistant') {
|
|
183
|
-
restored.push({ id: idRef.current++, role: 'assistant', text: entry.content, tools: [] })
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
setLines(restored)
|
|
187
|
-
} catch (err) {
|
|
188
|
-
if (!cancelled) setError((err as Error).message)
|
|
189
|
-
}
|
|
190
|
-
})()
|
|
191
|
-
return () => { cancelled = true }
|
|
192
|
-
}, [api])
|
|
193
|
-
|
|
194
|
-
/** Auto-scroll to the newest line. */
|
|
195
|
-
useEffect(() => {
|
|
196
|
-
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight })
|
|
197
|
-
}, [lines])
|
|
198
|
-
|
|
199
|
-
/** 清空对话记录(服务端删除历史 + 本地清空)。 */
|
|
200
|
-
const handleClear = async (): Promise<void> => {
|
|
201
|
-
if (!window.confirm('清空全部聊天记录?此操作不可恢复(不影响大纲/设定/章节)。')) return
|
|
202
|
-
setBusy(true)
|
|
203
|
-
setError('')
|
|
204
|
-
try {
|
|
205
|
-
await api.assistantClear()
|
|
206
|
-
setLines([])
|
|
207
|
-
setNotice('对话已清空,编辑老师会重新了解项目')
|
|
208
|
-
} catch (err) {
|
|
209
|
-
setError((err as Error).message)
|
|
210
|
-
} finally {
|
|
211
|
-
setBusy(false)
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/** Send one message. */
|
|
216
|
-
const handleSend = async (): Promise<void> => {
|
|
217
|
-
const message = input.trim()
|
|
218
|
-
if (message === '' || busy) return
|
|
219
|
-
setInput('')
|
|
220
|
-
setError('')
|
|
221
|
-
setNotice('')
|
|
222
|
-
pushLine({ role: 'user', text: message, tools: [] })
|
|
223
|
-
// Start an empty assistant bubble.
|
|
224
|
-
setLines(prev => [...prev, { id: idRef.current++, role: 'assistant', text: '', tools: [] }])
|
|
225
|
-
setBusy(true)
|
|
226
|
-
try {
|
|
227
|
-
await api.assistant(message, (frame: AssistantFrame) => {
|
|
228
|
-
if (frame.type === 'delta') {
|
|
229
|
-
pushLine({ role: 'assistant', text: frame.text, tools: [] })
|
|
230
|
-
} else if (frame.type === 'tool') {
|
|
231
|
-
pushTool({
|
|
232
|
-
name: frame.name,
|
|
233
|
-
status: frame.status,
|
|
234
|
-
detail: frame.detail,
|
|
235
|
-
})
|
|
236
|
-
} else if (frame.type === 'toolDelta') {
|
|
237
|
-
pushToolDelta(frame.text)
|
|
238
|
-
} else if (frame.type === 'toolResult') {
|
|
239
|
-
pushResultCard({ name: frame.name, text: frame.text })
|
|
240
|
-
} else if (frame.type === 'error') {
|
|
241
|
-
pushTool({ name: 'error', status: 'error', detail: frame.message })
|
|
242
|
-
}
|
|
243
|
-
})
|
|
244
|
-
} catch (err) {
|
|
245
|
-
setError((err as Error).message)
|
|
246
|
-
} finally {
|
|
247
|
-
setBusy(false)
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return (
|
|
252
|
-
<div className={css.card} style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
|
|
253
|
-
<div className={css.row} style={{ justifyContent: 'space-between', alignItems: 'center', gap: 'var(--nf-space-8)' }}>
|
|
254
|
-
<span className={css.meta}>{tt('assistant.hint')}</span>
|
|
255
|
-
<button type="button" className={css.iconButton} title="清空聊天记录" aria-label="清空聊天记录" onClick={() => { void handleClear() }}>
|
|
256
|
-
🗑️
|
|
257
|
-
</button>
|
|
258
|
-
</div>
|
|
259
|
-
{notice !== '' && <span style={{ color: 'var(--nf-success)', fontSize: 'var(--nf-fs-12)' }}>{notice}</span>}
|
|
260
|
-
{error !== '' && <span style={{ color: 'var(--nf-error)', fontSize: 'var(--nf-fs-12)' }}>{tt('common.error')}: {error}</span>}
|
|
261
|
-
{/* 状态条:编辑老师正在做什么 */}
|
|
262
|
-
<div className={`${css.assistantStatus} ${busy ? css.assistantStatusBusy : ''}`}>
|
|
263
|
-
{busy
|
|
264
|
-
? `🤖 编辑老师 · ${activeTool !== null ? `正在「${TOOL_LABELS[activeTool.name]?.label ?? activeTool.name}」` : `正在思考…(已 ${thinkSeconds}s)`}`
|
|
265
|
-
: '💬 编辑老师 · 等你开口'}
|
|
266
|
-
</div>
|
|
267
|
-
<div
|
|
268
|
-
ref={scrollRef}
|
|
269
|
-
className={css.chatScroll}
|
|
270
|
-
>
|
|
271
|
-
{lines.length === 0 && <span className={css.meta}>{tt('assistant.empty')}</span>}
|
|
272
|
-
{lines.map(line => (
|
|
273
|
-
<div key={line.id} className={line.role === 'user' ? css.chatBubbleUser : css.chatBubbleAssistant}>
|
|
274
|
-
{line.role === 'user' && <div className={css.chatRole}>你</div>}
|
|
275
|
-
{line.text !== '' && <div style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{line.text}</div>}
|
|
276
|
-
{line.live !== undefined && line.live !== '' && (
|
|
277
|
-
<div className={css.toolLive}>{line.live}</div>
|
|
278
|
-
)}
|
|
279
|
-
{/* 步骤卡片 */}
|
|
280
|
-
{line.tools.length > 0 && (
|
|
281
|
-
<div className={css.toolSteps}>
|
|
282
|
-
{line.tools.map((tool, i) => {
|
|
283
|
-
const meta = TOOL_LABELS[tool.name] ?? { icon: '⚙️', label: tool.name }
|
|
284
|
-
return (
|
|
285
|
-
<div
|
|
286
|
-
key={i}
|
|
287
|
-
className={`${css.toolStep} ${tool.status === 'error' ? css.toolStepError : tool.status === 'start' ? css.toolStepActive : ''}`}
|
|
288
|
-
>
|
|
289
|
-
<span className={css.toolStepIcon}>{meta.icon}</span>
|
|
290
|
-
<span className={css.toolStepName}>{meta.label}</span>
|
|
291
|
-
<span className={css.toolStepStatus}>
|
|
292
|
-
{tool.status === 'start'
|
|
293
|
-
? '⏳ 进行中'
|
|
294
|
-
: tool.status === 'done'
|
|
295
|
-
? `✓ ${tool.elapsedMs !== undefined ? `${(tool.elapsedMs / 1000).toFixed(1)}s` : ''}`
|
|
296
|
-
: '✗ 失败'}
|
|
297
|
-
</span>
|
|
298
|
-
{tool.status === 'error' && tool.detail !== undefined && (
|
|
299
|
-
<span className={css.toolStepDetail}>{tool.detail}</span>
|
|
300
|
-
)}
|
|
301
|
-
</div>
|
|
302
|
-
)
|
|
303
|
-
})}
|
|
304
|
-
</div>
|
|
305
|
-
)}
|
|
306
|
-
{/* 结构化结果卡片(拆书/导演/质检/知识库列表等) */}
|
|
307
|
-
{line.resultCard !== undefined && CARD_TOOLS.has(line.resultCard.name) && (
|
|
308
|
-
<div style={{ marginTop: 6, border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-10, 10px)', background: 'var(--nf-bg-inset)', padding: 'var(--nf-space-8, 8px)' }}>
|
|
309
|
-
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--nf-accent)', marginBottom: 4 }}>
|
|
310
|
-
{TOOL_LABELS[line.resultCard.name]?.icon ?? ''} {TOOL_LABELS[line.resultCard.name]?.label ?? line.resultCard.name}
|
|
311
|
-
</div>
|
|
312
|
-
<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', margin: 0, fontSize: 12, lineHeight: 1.5, color: 'var(--nf-text)', maxHeight: 240, overflowY: 'auto' }}>{line.resultCard.text}</pre>
|
|
313
|
-
</div>
|
|
314
|
-
)}
|
|
315
|
-
</div>
|
|
316
|
-
))}
|
|
317
|
-
{busy && <span className={css.meta} style={{ color: 'var(--nf-accent)' }}>…</span>}
|
|
318
|
-
</div>
|
|
319
|
-
<div className={css.row} style={{ marginTop: 'var(--nf-space-8)' }}>
|
|
320
|
-
<textarea
|
|
321
|
-
className={css.textarea}
|
|
322
|
-
style={{ minHeight: 64, flex: 1 }}
|
|
323
|
-
placeholder={tt('assistant.placeholder')}
|
|
324
|
-
value={input}
|
|
325
|
-
onChange={e => { setInput(e.target.value) }}
|
|
326
|
-
onKeyDown={e => {
|
|
327
|
-
if (e.key === 'Enter' && !e.shiftKey) {
|
|
328
|
-
e.preventDefault()
|
|
329
|
-
void handleSend()
|
|
330
|
-
}
|
|
331
|
-
}}
|
|
332
|
-
/>
|
|
333
|
-
<button type="button" className={`${css.button} ${css.buttonPrimary}`} disabled={busy || input.trim() === ''} onClick={() => { void handleSend() }}>
|
|
334
|
-
{tt('assistant.send')}
|
|
335
|
-
</button>
|
|
336
|
-
</div>
|
|
337
|
-
</div>
|
|
338
|
-
)
|
|
339
|
-
}
|
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 作者资产库/总数据页:跨书可复用的笔法/红线/套路/角色模板/世界观模板。
|
|
3
|
-
* 支持 新增 / 编辑 / 删除;数据持久化到 ~/.dsh/dsh-novel-forge-author-assets.json。
|
|
4
|
-
*/
|
|
5
|
-
import { useEffect, useMemo, useState } from 'react'
|
|
6
|
-
import { Plus, Trash2, Pencil } from 'lucide-react'
|
|
7
|
-
import type { NovelApi } from '../api.ts'
|
|
8
|
-
import type { AuthorStyleAsset } from '../../protocol.ts'
|
|
9
|
-
import css from './panel.module.css'
|
|
10
|
-
|
|
11
|
-
const KIND_LABELS: Record<AuthorStyleAsset['kind'], string> = {
|
|
12
|
-
style: '笔法',
|
|
13
|
-
antiAi: '红线/反AI',
|
|
14
|
-
progression: '推进模式',
|
|
15
|
-
genre: '题材',
|
|
16
|
-
roleTemplate: '角色模板',
|
|
17
|
-
worldTemplate: '世界观模板',
|
|
18
|
-
plotBeat: '剧情桥段',
|
|
19
|
-
custom: '自定义',
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const KIND_OPTIONS = Object.keys(KIND_LABELS) as AuthorStyleAsset['kind'][]
|
|
23
|
-
|
|
24
|
-
const KIND_COLORS: Record<AuthorStyleAsset['kind'], string> = {
|
|
25
|
-
style: 'var(--nf-info)',
|
|
26
|
-
antiAi: 'var(--nf-warn)',
|
|
27
|
-
progression: 'var(--nf-accent)',
|
|
28
|
-
genre: 'var(--nf-info)',
|
|
29
|
-
roleTemplate: 'var(--nf-accent)',
|
|
30
|
-
worldTemplate: 'var(--nf-warn)',
|
|
31
|
-
plotBeat: 'var(--nf-accent)',
|
|
32
|
-
custom: 'var(--nf-muted)',
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** 计算资产的分组(分类):题材按顶层题材名('/' 之前),其余按类型。 */
|
|
36
|
-
function assetCategory(a: AuthorStyleAsset): string {
|
|
37
|
-
if (a.kind === 'genre') {
|
|
38
|
-
const top = a.name.split('/')[0]?.trim()
|
|
39
|
-
return top !== undefined && top !== '' ? top : '题材'
|
|
40
|
-
}
|
|
41
|
-
return KIND_LABELS[a.kind] ?? '其他'
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface EditorState {
|
|
45
|
-
id?: string
|
|
46
|
-
name: string; kind: AuthorStyleAsset['kind']; summary: string; content: string; tags: string; sourceBooks: string
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function emptyEditor(): EditorState {
|
|
50
|
-
return { name: '', kind: 'style', summary: '', content: '', tags: '', sourceBooks: '' }
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function AuthorAssetsView({ api }: { api: NovelApi }) {
|
|
54
|
-
const [assets, setAssets] = useState<AuthorStyleAsset[]>([]);
|
|
55
|
-
const [editing, setEditing] = useState<EditorState | null>(null);
|
|
56
|
-
const [error, setError] = useState('');
|
|
57
|
-
const [busy, setBusy] = useState(false);
|
|
58
|
-
const [kindFilter, setKindFilter] = useState<AuthorStyleAsset['kind'] | 'all'>('all');
|
|
59
|
-
|
|
60
|
-
const load = async (): Promise<void> => {
|
|
61
|
-
try { const res = await api.authorAssets(); setAssets(res.assets.items) } catch (err) { setError((err as Error).message) }
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
/** 导入默认:把书架书的写作资产/角色 + 内置全局库批量沉淀到作者资产库。 */
|
|
65
|
-
const importDefaults = async (): Promise<void> => {
|
|
66
|
-
setBusy(true); setError('');
|
|
67
|
-
try { const res = await api.authorAssetsImportDefault(); setAssets(res.assets.items) } catch (err) { setError((err as Error).message) } finally { setBusy(false) }
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
// 打开即加载;若资产库为空,自动导入默认(书架资产 + 内置全局库)。
|
|
71
|
-
useEffect(() => {
|
|
72
|
-
let cancelled = false
|
|
73
|
-
void (async () => {
|
|
74
|
-
try {
|
|
75
|
-
const res = await api.authorAssets()
|
|
76
|
-
if (cancelled) return
|
|
77
|
-
if (res.assets.items.length === 0) {
|
|
78
|
-
const imp = await api.authorAssetsImportDefault()
|
|
79
|
-
if (!cancelled) setAssets(imp.assets.items)
|
|
80
|
-
} else {
|
|
81
|
-
setAssets(res.assets.items)
|
|
82
|
-
}
|
|
83
|
-
} catch (err) {
|
|
84
|
-
if (!cancelled) setError((err as Error).message)
|
|
85
|
-
}
|
|
86
|
-
})()
|
|
87
|
-
return () => { cancelled = true }
|
|
88
|
-
}, [api]);
|
|
89
|
-
|
|
90
|
-
const startNew = (): void => { setEditing(emptyEditor()); setError('') };
|
|
91
|
-
const startEdit = (a: AuthorStyleAsset): void => {
|
|
92
|
-
setEditing({ id: a.id, name: a.name, kind: a.kind, summary: a.summary, content: a.content, tags: a.tags.join('、'), sourceBooks: a.sourceBooks.join('、') });
|
|
93
|
-
setError('');
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const save = async (): Promise<void> => {
|
|
97
|
-
if (editing === null) return;
|
|
98
|
-
if (editing.name.trim() === '') { setError('请填写资产名'); return }
|
|
99
|
-
setBusy(true); setError('');
|
|
100
|
-
try {
|
|
101
|
-
const asset: AuthorStyleAsset = {
|
|
102
|
-
id: editing.id ?? '', name: editing.name.trim(), kind: editing.kind, summary: editing.summary.trim(), content: editing.content.trim(),
|
|
103
|
-
sourceBooks: editing.sourceBooks.split(/[、,,\s]+/).map(s => s.trim()).filter(Boolean),
|
|
104
|
-
tags: editing.tags.split(/[、,,\s]+/).map(s => s.trim()).filter(Boolean),
|
|
105
|
-
createdAt: '', updatedAt: '',
|
|
106
|
-
};
|
|
107
|
-
await api.authorAssetsUpsert(asset); setEditing(null); await load();
|
|
108
|
-
} catch (err) { setError((err as Error).message) } finally { setBusy(false) }
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const del = async (id: string): Promise<void> => {
|
|
112
|
-
setBusy(true); setError('');
|
|
113
|
-
try { await api.authorAssetsRemove(id); await load() } catch (err) { setError((err as Error).message) } finally { setBusy(false) }
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
const visible = useMemo(() => (kindFilter === 'all' ? assets : assets.filter(a => a.kind === kindFilter)), [assets, kindFilter]);
|
|
117
|
-
|
|
118
|
-
/** 按分类分组展示(题材按顶层题材名,其余按类型)。 */
|
|
119
|
-
const grouped = useMemo(() => {
|
|
120
|
-
const map = new Map<string, AuthorStyleAsset[]>()
|
|
121
|
-
for (const a of visible) {
|
|
122
|
-
const cat = assetCategory(a)
|
|
123
|
-
const arr = map.get(cat) ?? []
|
|
124
|
-
arr.push(a)
|
|
125
|
-
map.set(cat, arr)
|
|
126
|
-
}
|
|
127
|
-
return Array.from(map.entries()).sort((x, y) => x[0].localeCompare(y[0], 'zh'))
|
|
128
|
-
}, [visible]);
|
|
129
|
-
|
|
130
|
-
return (
|
|
131
|
-
<div className={css.authorPageBody}>
|
|
132
|
-
<div className={css.authorPageHero}>
|
|
133
|
-
<div className={css.authorPageHeader}>
|
|
134
|
-
<div>
|
|
135
|
-
<h2 className={css.panelTitle} style={{ margin: 0 }}>🧬 作者资产库 · 总数据</h2>
|
|
136
|
-
<span className={css.meta}>跨书可复用的笔法/红线/套路/角色模板/世界观模板。任意新书与改编都能调用。</span>
|
|
137
|
-
</div>
|
|
138
|
-
<div className={css.rowEnd}>
|
|
139
|
-
<button type="button" className={css.button + ' ' + css.buttonSmall} onClick={() => { void importDefaults() }} disabled={busy} title="把书架里所有书的写作资产/角色批量沉淀到作者资产库(内置库请到全局写作资产库浏览/套用)">导入默认资产</button>
|
|
140
|
-
<button type="button" className={css.button + ' ' + css.buttonPrimary} onClick={startNew}><Plus size={14} style={{ verticalAlign: -2 }} /> 新增资产</button>
|
|
141
|
-
</div>
|
|
142
|
-
</div>
|
|
143
|
-
<div className={css.assetFilterBar}>
|
|
144
|
-
{KIND_OPTIONS.map(k => (
|
|
145
|
-
<button key={k} type="button" className={css.assetFilterChip + (kindFilter === k ? ' ' + css.assetFilterChipActive : '')} onClick={() => setKindFilter(kindFilter === k ? 'all' : k)}>{KIND_LABELS[k]} {assets.filter(a => a.kind === k).length}</button>
|
|
146
|
-
))}
|
|
147
|
-
</div>
|
|
148
|
-
</div>
|
|
149
|
-
|
|
150
|
-
{error !== '' && <div className={css.noticeError}>{error}</div>}
|
|
151
|
-
|
|
152
|
-
{editing !== null ? (
|
|
153
|
-
<div className={css.assetEditor}>
|
|
154
|
-
<h3 style={{ margin: 0 }}>{editing.id !== '' && editing.id !== undefined ? '编辑资产' : '新增资产'}</h3>
|
|
155
|
-
<div className={css.assetEditorRow}>
|
|
156
|
-
<label className={css.assetLabel}>资产名 <input className={css.input} value={editing.name} onChange={e => setEditing({ ...editing, name: e.target.value })} placeholder="如:短句快节奏爽文风" /></label>
|
|
157
|
-
<label className={css.assetLabel}>类型 <select className={css.input} value={editing.kind} onChange={e => setEditing({ ...editing, kind: e.target.value as AuthorStyleAsset['kind'] })}>{KIND_OPTIONS.map(k => <option key={k} value={k}>{KIND_LABELS[k]}</option>)}</select></label>
|
|
158
|
-
</div>
|
|
159
|
-
<label className={css.assetLabel}>一句话摘要 <input className={css.input} value={editing.summary} onChange={e => setEditing({ ...editing, summary: e.target.value })} placeholder="这个资产解决什么问题 / 有什么特点" /></label>
|
|
160
|
-
<label className={css.assetLabel}>内容(按类型约定的描述 / 规则 / 结构化文本)
|
|
161
|
-
<textarea className={`${css.input} ${css.assetTextarea}`} value={editing.content} onChange={e => setEditing({ ...editing, content: e.target.value })} placeholder={'角色模板示例:\n姓名:林越\n定位:痞坏型主角\n口头禅:"有点意思"\n性格:表面懒散、内里狠辣\n成长弧线:从底层游街到一方枭雄'} />
|
|
162
|
-
</label>
|
|
163
|
-
<div className={css.assetEditorRow}>
|
|
164
|
-
<label className={css.assetLabel}>标签(顿号分隔) <input className={css.input} value={editing.tags} onChange={e => setEditing({ ...editing, tags: e.target.value })} placeholder="扮猪吃虎、快节奏、冷面" /></label>
|
|
165
|
-
<label className={css.assetLabel}>来源书(顿号分隔) <input className={css.input} value={editing.sourceBooks} onChange={e => setEditing({ ...editing, sourceBooks: e.target.value })} placeholder="《某某传》《某某录》" /></label>
|
|
166
|
-
</div>
|
|
167
|
-
<div className={css.rowEnd}><button type="button" className={`${css.button} ${css.buttonPrimary}`} onClick={save} disabled={busy}>{busy ? '保存中…' : '保存'}</button>{(editing.id !== '' && editing.id !== undefined) && <button type="button" className={css.button + ' ' + css.buttonSmall} onClick={() => setEditing(null)}>取消</button>}</div>
|
|
168
|
-
</div>
|
|
169
|
-
) : null}
|
|
170
|
-
|
|
171
|
-
{visible.length === 0 ? (
|
|
172
|
-
<div className={css.shelfEmpty} style={{ minHeight: 160 }}>
|
|
173
|
-
<span className={css.shelfEmptyIcon}><Plus size={30} /></span>
|
|
174
|
-
<span className={css.shelfEmptyTitle}>还没有资产</span>
|
|
175
|
-
<span className={css.meta}>点「新增资产」收藏你的笔法/角色模板/套路,跨书调用。</span>
|
|
176
|
-
</div>
|
|
177
|
-
) : (
|
|
178
|
-
<div className={css.assetGroups}>
|
|
179
|
-
{grouped.map(([cat, items]) => (
|
|
180
|
-
<div key={cat} className={css.assetGroup}>
|
|
181
|
-
<div className={css.assetGroupHeader}>{cat} <span className={css.meta}>{items.length}</span></div>
|
|
182
|
-
<div className={css.assetGrid}>
|
|
183
|
-
{items.map(a => (
|
|
184
|
-
<div key={a.id} className={css.assetCard}>
|
|
185
|
-
<div className={css.assetCardTop}>
|
|
186
|
-
<span className={css.assetKind} style={{ color: KIND_COLORS[a.kind] }}>{KIND_LABELS[a.kind]}</span>
|
|
187
|
-
<div className={css.rowEnd}>
|
|
188
|
-
<button type="button" className={css.iconBtn} title="编辑" onClick={() => startEdit(a)}><Pencil size={13} /></button>
|
|
189
|
-
<button type="button" className={css.iconBtn} title="删除" onClick={() => { void del(a.id) }}><Trash2 size={13} /></button>
|
|
190
|
-
</div>
|
|
191
|
-
</div>
|
|
192
|
-
<div className={css.assetCardName}>{a.name}</div>
|
|
193
|
-
{a.summary !== '' && <div className={css.meta}>{a.summary}</div>}
|
|
194
|
-
<div className={css.assetCardContent}>{a.content}</div>
|
|
195
|
-
<div className={css.assetCardFooter}>
|
|
196
|
-
{a.tags.map(t => <span key={t} className={css.badge}>{t}</span>)}
|
|
197
|
-
{a.sourceBooks.length > 0 && <span className={css.meta}>来源:{a.sourceBooks.join(' / ')}</span>}
|
|
198
|
-
</div>
|
|
199
|
-
</div>
|
|
200
|
-
))}
|
|
201
|
-
</div>
|
|
202
|
-
</div>
|
|
203
|
-
))}
|
|
204
|
-
</div>
|
|
205
|
-
)}
|
|
206
|
-
</div>
|
|
207
|
-
);
|
|
208
|
-
}
|