@waterwx/dsh-novel-forge 2.0.1-alpha → 2.1.0-alpha
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 +97 -2
- package/lib/client.js +3291 -752
- package/lib/index.js +3557 -237
- package/lib/types/assets.d.ts +16 -3
- package/lib/types/assistant.d.ts +4 -0
- package/lib/types/client/api.d.ts +89 -0
- package/lib/types/client/llmLive.d.ts +33 -0
- package/lib/types/client/locales.d.ts +3 -3
- package/lib/types/client/panel/AdaptModeView.d.ts +1 -1
- package/lib/types/client/panel/AssetsTab.d.ts +1 -1
- package/lib/types/client/panel/AssistantTab.d.ts +1 -1
- package/lib/types/client/panel/AuthorAssetsView.d.ts +1 -1
- package/lib/types/client/panel/AuthorHome.d.ts +4 -2
- package/lib/types/client/panel/BookAnalysisView.d.ts +4 -0
- package/lib/types/client/panel/BookshelfBar.d.ts +1 -1
- package/lib/types/client/panel/CreateBookView.d.ts +6 -2
- package/lib/types/client/panel/DirectorView.d.ts +7 -0
- package/lib/types/client/panel/FlowGuide.d.ts +1 -1
- package/lib/types/client/panel/GlobalAssetLibraryView.d.ts +1 -1
- package/lib/types/client/panel/IdeaInspirationView.d.ts +6 -0
- package/lib/types/client/panel/ImportModal.d.ts +1 -1
- package/lib/types/client/panel/KnowledgeBaseView.d.ts +4 -0
- package/lib/types/client/panel/LiveFeedLog.d.ts +5 -0
- package/lib/types/client/panel/MangaRoleLibrary.d.ts +1 -1
- package/lib/types/client/panel/MangaWorkspace.d.ts +1 -1
- package/lib/types/client/panel/MarketRadarView.d.ts +5 -0
- package/lib/types/client/panel/NovelPanel.d.ts +2 -2
- package/lib/types/client/panel/ProgressConsole.d.ts +1 -1
- package/lib/types/client/panel/ProgressHomeView.d.ts +1 -1
- package/lib/types/client/panel/PropLibrary.d.ts +1 -1
- 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/SceneLibrary.d.ts +1 -1
- package/lib/types/client/panel/SettingsView.d.ts +1 -1
- package/lib/types/client/panel/ShelfView.d.ts +1 -1
- package/lib/types/client/panel/StoryboardTab.d.ts +1 -1
- package/lib/types/client/panel/StyleCard.d.ts +1 -1
- package/lib/types/client/panel/StyleGallery.d.ts +1 -1
- package/lib/types/client/panel/WorldTab.d.ts +1 -1
- package/lib/types/engine.d.ts +29 -1
- package/lib/types/global-assets.d.ts +6 -0
- package/lib/types/index.d.ts +6 -0
- package/lib/types/llm-live.d.ts +44 -0
- package/lib/types/market-radar-scan.d.ts +19 -0
- package/lib/types/market-radar-sources.d.ts +31 -0
- package/lib/types/prompting.d.ts +35 -0
- package/lib/types/protocol.d.ts +364 -8
- package/lib/types/run.d.ts +1 -1
- package/lib/types/src/ai-scan.d.ts +36 -0
- package/lib/types/src/assets.d.ts +37 -0
- package/lib/types/src/assistant.d.ts +45 -0
- package/lib/types/src/author-assets.d.ts +25 -0
- package/lib/types/src/bookshelf.d.ts +42 -0
- package/lib/types/src/client/api.d.ts +234 -0
- package/lib/types/src/client/docx.d.ts +15 -0
- package/lib/types/src/client/index.d.ts +14 -0
- package/lib/types/src/client/locales.d.ts +212 -0
- package/lib/types/src/client/mount.d.ts +9 -0
- package/lib/types/src/client/panel/AdaptModeView.d.ts +5 -0
- package/lib/types/src/client/panel/AssetsTab.d.ts +12 -0
- package/lib/types/src/client/panel/AssistantTab.d.ts +7 -0
- package/lib/types/src/client/panel/AuthorAssetsView.d.ts +4 -0
- package/lib/types/src/client/panel/AuthorHome.d.ts +32 -0
- package/lib/types/src/client/panel/BookshelfBar.d.ts +11 -0
- package/lib/types/src/client/panel/CreateBookView.d.ts +9 -0
- package/lib/types/src/client/panel/FlowGuide.d.ts +22 -0
- package/lib/types/src/client/panel/GlobalAssetLibraryView.d.ts +4 -0
- package/lib/types/src/client/panel/ImportModal.d.ts +7 -0
- package/lib/types/src/client/panel/MangaRoleLibrary.d.ts +20 -0
- package/lib/types/src/client/panel/MangaWorkspace.d.ts +12 -0
- package/lib/types/src/client/panel/ModelManager.d.ts +14 -0
- package/lib/types/src/client/panel/NovelPanel.d.ts +13 -0
- package/lib/types/src/client/panel/ProgressConsole.d.ts +17 -0
- package/lib/types/src/client/panel/ProgressHomeView.d.ts +5 -0
- package/lib/types/src/client/panel/PropLibrary.d.ts +8 -0
- package/lib/types/src/client/panel/ReaderView.d.ts +8 -0
- package/lib/types/src/client/panel/ReasoningSection.d.ts +10 -0
- package/lib/types/src/client/panel/RunPanel.d.ts +5 -0
- package/lib/types/src/client/panel/SceneLibrary.d.ts +16 -0
- package/lib/types/src/client/panel/SettingsView.d.ts +16 -0
- package/lib/types/src/client/panel/ShelfView.d.ts +15 -0
- package/lib/types/src/client/panel/StoryboardTab.d.ts +19 -0
- package/lib/types/src/client/panel/StyleCard.d.ts +8 -0
- package/lib/types/src/client/panel/StyleGallery.d.ts +1 -0
- package/lib/types/src/client/panel/WorldTab.d.ts +10 -0
- package/lib/types/src/client/panel/controller.d.ts +19 -0
- package/lib/types/src/client/panel/helpers.d.ts +18 -0
- package/lib/types/src/client/panel/views.d.ts +77 -0
- package/lib/types/src/client/sidebar-entry.d.ts +13 -0
- package/lib/types/src/client/text.d.ts +9 -0
- package/lib/types/src/comic-presets.d.ts +12 -0
- package/lib/types/src/docx.d.ts +19 -0
- package/lib/types/src/engine.d.ts +402 -0
- package/lib/types/src/index.d.ts +79 -0
- package/lib/types/src/manga-genre-rules.d.ts +22 -0
- package/lib/types/src/novel-context.d.ts +80 -0
- package/lib/types/src/prompt-utils.d.ts +35 -0
- package/lib/types/src/protocol.d.ts +2176 -0
- package/lib/types/src/review-policy.d.ts +97 -0
- package/lib/types/src/routes.d.ts +29 -0
- package/lib/types/src/run.d.ts +32 -0
- package/lib/types/src/shot-language.d.ts +46 -0
- package/lib/types/src/story-beat-language.d.ts +35 -0
- package/lib/types/src/style-library.d.ts +33 -0
- package/package.json +1 -1
- package/src/assets.ts +1774 -879
- package/src/assistant.ts +154 -9
- package/src/client/api.ts +105 -6
- package/src/client/llmLive.ts +101 -0
- package/src/client/locales.ts +4 -4
- package/src/client/panel/AdaptModeView.tsx +18 -3
- package/src/client/panel/AssistantTab.tsx +41 -3
- package/src/client/panel/AuthorHome.tsx +51 -20
- package/src/client/panel/BookAnalysisView.tsx +60 -0
- package/src/client/panel/CreateBookView.tsx +22 -3
- package/src/client/panel/DirectorView.tsx +200 -0
- package/src/client/panel/IdeaInspirationView.tsx +71 -0
- package/src/client/panel/KnowledgeBaseView.tsx +73 -0
- package/src/client/panel/LiveFeedLog.tsx +76 -0
- package/src/client/panel/MarketRadarView.tsx +374 -0
- package/src/client/panel/NovelPanel.tsx +117 -16
- package/src/client/panel/ProgressConsole.tsx +5 -6
- package/src/client/panel/SettingsView.tsx +3 -0
- package/src/client/panel/panel.module.css +2 -3
- package/src/engine.ts +459 -23
- package/src/global-assets.ts +59 -0
- package/src/index.ts +17 -1
- package/src/llm-live.ts +68 -0
- package/src/market-radar-scan.ts +41 -0
- package/src/market-radar-sources.ts +173 -0
- package/src/prompting.ts +159 -0
- package/src/protocol.ts +340 -8
- package/src/routes.ts +579 -59
- package/src/run.ts +15 -8
- package/skills/chapter-batch/SKILL.md +0 -81
|
@@ -28,9 +28,24 @@ const TOOL_LABELS: Record<string, { icon: string; label: string }> = {
|
|
|
28
28
|
assets_set_genre: { icon: '🏷️', label: '设置题材' },
|
|
29
29
|
assets_set_progression: { icon: '📈', label: '设置推进模式' },
|
|
30
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: '生成简介' },
|
|
31
43
|
error: { icon: '⚠️', label: '出错了' },
|
|
32
44
|
}
|
|
33
45
|
|
|
46
|
+
/** 需要渲染成结构化结果卡片的工具(其余工具结果仍走文字回复,避免大段章节/总纲刷屏)。 */
|
|
47
|
+
const CARD_TOOLS = new Set(['director_advice', 'book_analysis', 'breakdown', 'audit', 'knowledge_list', 'plotline_list', 'director_todo_list'])
|
|
48
|
+
|
|
34
49
|
/** One chat bubble (either side). */
|
|
35
50
|
interface ChatLine {
|
|
36
51
|
id: number
|
|
@@ -40,6 +55,8 @@ interface ChatLine {
|
|
|
40
55
|
tools: Array<{ name: string; status: 'start' | 'done' | 'error'; detail?: string; startedAt?: number; elapsedMs?: number }>
|
|
41
56
|
/** Live output while a tool runs (generated text streamed into the bubble). */
|
|
42
57
|
live?: string
|
|
58
|
+
/** 结构化工具完整结果(用于渲染成卡片,如拆书/导演/质检/知识库列表)。 */
|
|
59
|
+
resultCard?: { name: string; text: string }
|
|
43
60
|
}
|
|
44
61
|
|
|
45
62
|
/** Props. */
|
|
@@ -127,6 +144,17 @@ export function AssistantTab({ api }: AssistantTabProps) {
|
|
|
127
144
|
})
|
|
128
145
|
}, [])
|
|
129
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
|
+
|
|
130
158
|
/** 当前进行中的工具(状态条显示)。 */
|
|
131
159
|
const activeTool = (() => {
|
|
132
160
|
if (!busy) return null
|
|
@@ -207,6 +235,8 @@ export function AssistantTab({ api }: AssistantTabProps) {
|
|
|
207
235
|
})
|
|
208
236
|
} else if (frame.type === 'toolDelta') {
|
|
209
237
|
pushToolDelta(frame.text)
|
|
238
|
+
} else if (frame.type === 'toolResult') {
|
|
239
|
+
pushResultCard({ name: frame.name, text: frame.text })
|
|
210
240
|
} else if (frame.type === 'error') {
|
|
211
241
|
pushTool({ name: 'error', status: 'error', detail: frame.message })
|
|
212
242
|
}
|
|
@@ -220,13 +250,12 @@ export function AssistantTab({ api }: AssistantTabProps) {
|
|
|
220
250
|
|
|
221
251
|
return (
|
|
222
252
|
<div className={css.card} style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
|
|
223
|
-
<div className={css.row} style={{ justifyContent: 'space-between' }}>
|
|
224
|
-
<span className={css.
|
|
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>
|
|
225
255
|
<button type="button" className={css.iconButton} title="清空聊天记录" aria-label="清空聊天记录" onClick={() => { void handleClear() }}>
|
|
226
256
|
🗑️
|
|
227
257
|
</button>
|
|
228
258
|
</div>
|
|
229
|
-
<span className={css.meta}>{tt('assistant.hint')}</span>
|
|
230
259
|
{notice !== '' && <span style={{ color: 'var(--nf-success)', fontSize: 'var(--nf-fs-12)' }}>{notice}</span>}
|
|
231
260
|
{error !== '' && <span style={{ color: 'var(--nf-error)', fontSize: 'var(--nf-fs-12)' }}>{tt('common.error')}: {error}</span>}
|
|
232
261
|
{/* 状态条:编辑老师正在做什么 */}
|
|
@@ -274,6 +303,15 @@ export function AssistantTab({ api }: AssistantTabProps) {
|
|
|
274
303
|
})}
|
|
275
304
|
</div>
|
|
276
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
|
+
)}
|
|
277
315
|
</div>
|
|
278
316
|
))}
|
|
279
317
|
{busy && <span className={css.meta} style={{ color: 'var(--nf-accent)' }}>…</span>}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* 导航复用工作区 .panelNav 玻璃卡片样式(同款),导航项为作者级:书架/改编/资产库/全局资产库/AI 进度/设置。
|
|
4
4
|
*/
|
|
5
5
|
import { useState, useRef, useEffect } from 'react'
|
|
6
|
-
import { Library, Wand2, Boxes, Brush,
|
|
6
|
+
import { Library, Wand2, Boxes, Brush, Radar, FileSearch, Lightbulb, Settings } from 'lucide-react'
|
|
7
7
|
import type { NovelApi } from '../api.ts'
|
|
8
8
|
import type { BookshelfSnapshot } from '../../protocol.ts'
|
|
9
9
|
import { ShelfView } from './ShelfView.tsx'
|
|
@@ -12,6 +12,10 @@ import { AuthorAssetsView } from './AuthorAssetsView.tsx'
|
|
|
12
12
|
import { GlobalAssetLibraryView } from './GlobalAssetLibraryView.tsx'
|
|
13
13
|
import { ProgressConsole, type ProgressLine } from './ProgressConsole.tsx'
|
|
14
14
|
import { SettingsView } from './SettingsView.tsx'
|
|
15
|
+
import LiveFeedLog from './LiveFeedLog.tsx'
|
|
16
|
+
import MarketRadarView from './MarketRadarView.tsx'
|
|
17
|
+
import BookAnalysisView from './BookAnalysisView.tsx'
|
|
18
|
+
import IdeaInspirationView from './IdeaInspirationView.tsx'
|
|
15
19
|
import css from './panel.module.css'
|
|
16
20
|
|
|
17
21
|
/** 构建时注入的插件版本(tsdown define 替换为字符串字面量)。 */
|
|
@@ -20,20 +24,41 @@ const PLUGIN_VERSION: string = typeof __NOVEL_FORGE_VERSION__ !== 'undefined' ?
|
|
|
20
24
|
/** GitHub 仓库地址(关于区块点击跳转)。 */
|
|
21
25
|
const REPO_URL = 'https://github.com/watersxya/dsh-novel-forge'
|
|
22
26
|
|
|
23
|
-
type AuthorNav = 'shelf' | 'adapt' | 'assets' | 'library' | '
|
|
27
|
+
type AuthorNav = 'shelf' | 'adapt' | 'assets' | 'library' | 'marketRadar' | 'bookAnalysis' | 'ideaInspiration' | 'settings';
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
{
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
type AuthorNavGroup = { label: string; items: Array<{ id: Exclude<AuthorNav, 'settings'>; label: string; icon: JSX.Element; hint: string }> };
|
|
30
|
+
|
|
31
|
+
const NAV_GROUPS: AuthorNavGroup[] = [
|
|
32
|
+
{
|
|
33
|
+
label: '创作',
|
|
34
|
+
items: [
|
|
35
|
+
{ id: 'shelf', label: '书架', icon: <Library size={18} />, hint: '我的书' },
|
|
36
|
+
{ id: 'adapt', label: '改编模式', icon: <Wand2 size={18} />, hint: '上传全文→可改范围' },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
label: '灵感 · 策划',
|
|
41
|
+
items: [
|
|
42
|
+
{ id: 'marketRadar', label: '题材雷达', icon: <Radar size={18} />, hint: '热门题材雷达' },
|
|
43
|
+
{ id: 'ideaInspiration', label: '创意灵感', icon: <Lightbulb size={18} />, hint: '多方向开书灵感' },
|
|
44
|
+
{ id: 'bookAnalysis', label: '书分析', icon: <FileSearch size={18} />, hint: '拆书/卖点/可借鉴' },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
label: '资产',
|
|
49
|
+
items: [
|
|
50
|
+
{ id: 'assets', label: '作者资产库', icon: <Boxes size={18} />, hint: '跨书总数据' },
|
|
51
|
+
{ id: 'library', label: '全局写作资产库', icon: <Brush size={18} />, hint: '内置题材/规则/模板' },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
31
54
|
];
|
|
32
55
|
|
|
33
56
|
|
|
34
|
-
export function AuthorHome({ api, shelf, onOpenBook, onReadBook, onAddBook, onImportBook, onOpenSettings, onTheme, onBackground, onOpacity, onEndfieldAccent, adaptEnabled, progress, busy, busyLabel, liveBar, onClearProgress }: {
|
|
57
|
+
export function AuthorHome({ api, shelf, onOpenBook, onReadBook, onAddBook, onImportBook, onOpenSettings, onTheme, onBackground, onOpacity, onEndfieldAccent, adaptEnabled, progress, busy, busyLabel, liveBar, onClearProgress, onUseIdea }: {
|
|
35
58
|
api: NovelApi; shelf: BookshelfSnapshot;
|
|
36
59
|
onOpenBook: (id: string) => void; onReadBook: (id: string) => void; onAddBook: () => void; onImportBook: () => void;
|
|
60
|
+
/** 创意灵感 → 采纳某个灵感带入开书向导。 */
|
|
61
|
+
onUseIdea?: (idea: import('../../protocol.ts').IdeaInspirationResult['ideas'][number]) => void;
|
|
37
62
|
/** 兼容旧入口:首页设置现为独立设置页,此回调保留但不再使用。 */
|
|
38
63
|
onOpenSettings?: () => void;
|
|
39
64
|
/** 主题/模式/密度变化回调(供面板根容器实时生效)。 */
|
|
@@ -132,16 +157,18 @@ export function AuthorHome({ api, shelf, onOpenBook, onReadBook, onAddBook, onIm
|
|
|
132
157
|
<div className={css.navTitleBook}>作者级 · 跨书</div>
|
|
133
158
|
</div>
|
|
134
159
|
</div>
|
|
135
|
-
|
|
136
|
-
<div className={css.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
<
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
160
|
+
{NAV_GROUPS.map(group => (
|
|
161
|
+
<div className={css.navGroup} key={group.label}>
|
|
162
|
+
<div className={css.navGroupLabel}>{group.label}</div>
|
|
163
|
+
{group.items.filter(item => item.id !== 'adapt' || adaptEnabled).map(item => (
|
|
164
|
+
<button key={item.id} type="button" role="tab" aria-selected={nav === item.id} data-active={nav === item.id ? '' : undefined} className={css.navTab} title={item.hint} onClick={() => { if (item.id === 'progress') { setProgressOpen(o => !o); return } setNav(item.id) }}>
|
|
165
|
+
<span className={css.navTabIcon}>{item.icon}</span>
|
|
166
|
+
<span className={css.navTabLabel}>{item.label}</span>
|
|
167
|
+
{item.id === 'progress' && busy === true && <span className={css.navTabDot} />}
|
|
168
|
+
</button>
|
|
169
|
+
))}
|
|
170
|
+
</div>
|
|
171
|
+
))}
|
|
145
172
|
<div className={css.navSpacer} />
|
|
146
173
|
<button type="button" role="tab" aria-selected={nav === 'settings'} data-active={nav === 'settings' ? '' : undefined} className={css.navTab} title="设置" onClick={() => setNav('settings')}>
|
|
147
174
|
<span className={css.navTabIcon}><Settings size={18} /></span>
|
|
@@ -164,6 +191,9 @@ export function AuthorHome({ api, shelf, onOpenBook, onReadBook, onAddBook, onIm
|
|
|
164
191
|
{nav === 'adapt' && <AdaptModeView api={api} onOpenBook={onOpenBook} />}
|
|
165
192
|
{nav === 'assets' && <AuthorAssetsView api={api} />}
|
|
166
193
|
{nav === 'library' && <GlobalAssetLibraryView api={api} />}
|
|
194
|
+
{nav === 'marketRadar' && <MarketRadarView api={api} bookId={shelf?.activeBookId ?? undefined} />}
|
|
195
|
+
{nav === 'bookAnalysis' && <BookAnalysisView api={api} />}
|
|
196
|
+
{nav === 'ideaInspiration' && <IdeaInspirationView api={api} onUseIdea={onUseIdea} />}
|
|
167
197
|
{nav === 'settings' && <SettingsView api={api} onTheme={onTheme} onBackground={onBackground} onOpacity={onOpacity} onEndfieldAccent={onEndfieldAccent} />}
|
|
168
198
|
</div>
|
|
169
199
|
{progressOpen && (
|
|
@@ -186,9 +216,10 @@ export function AuthorHome({ api, shelf, onOpenBook, onReadBook, onAddBook, onIm
|
|
|
186
216
|
</span>
|
|
187
217
|
</div>
|
|
188
218
|
<div className={css.assistantFloatBody} style={{ padding: 'var(--nf-space-10)', display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-8)', overflow: 'hidden', flex: 1, minHeight: 0 }}>
|
|
189
|
-
<div style={{ flex:
|
|
219
|
+
<div style={{ flex: '0 2 auto', minHeight: 0, maxHeight: '42%', display: 'flex', flexDirection: 'column' }}>
|
|
190
220
|
<ProgressConsole progress={progress ?? []} busy={busy ?? false} busyLabel={busyLabel ?? ''} liveBar={liveBar ?? null} onClear={() => onClearProgress?.()} />
|
|
191
221
|
</div>
|
|
222
|
+
<LiveFeedLog />
|
|
192
223
|
</div>
|
|
193
224
|
<div
|
|
194
225
|
style={{ width: 14, height: 14, position: 'absolute', right: 0, bottom: 0, cursor: 'nwse-resize' }}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 书分析/拆书:输入一本书/章节文本 → 卖点/结构/可借鉴点/风险。
|
|
3
|
+
*/
|
|
4
|
+
import { useState } from 'react'
|
|
5
|
+
import type { NovelApi } from '../api.ts'
|
|
6
|
+
import type { BookAnalysisResult } from '../../protocol.ts'
|
|
7
|
+
import css from './panel.module.css'
|
|
8
|
+
|
|
9
|
+
const SECTIONS: Array<{ key: keyof BookAnalysisResult; label: string; color: string }> = [
|
|
10
|
+
{ key: 'sellingPoints', label: '卖点', color: 'var(--nf-success)' },
|
|
11
|
+
{ key: 'structure', label: '结构', color: 'var(--nf-info)' },
|
|
12
|
+
{ key: 'lessons', label: '可借鉴', color: 'var(--nf-accent)' },
|
|
13
|
+
{ key: 'risks', label: '风险', color: 'var(--nf-error)' },
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
export default function BookAnalysisView({ api }: { api: NovelApi }): JSX.Element {
|
|
17
|
+
const [text, setText] = useState('')
|
|
18
|
+
const [busy, setBusy] = useState(false)
|
|
19
|
+
const [error, setError] = useState('')
|
|
20
|
+
const [result, setResult] = useState<BookAnalysisResult | null>(null)
|
|
21
|
+
|
|
22
|
+
const run = async (): Promise<void> => {
|
|
23
|
+
if (text.trim().length < 50) { setError('文本过短(<50 字符),请粘贴一段书/章节内容'); return }
|
|
24
|
+
setBusy(true); setError('')
|
|
25
|
+
try {
|
|
26
|
+
const r = await api.bookAnalysis(text.trim())
|
|
27
|
+
setResult(r.result)
|
|
28
|
+
} catch (e) { setError((e as Error).message) } finally { setBusy(false) }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className={css.authorPageBody}>
|
|
33
|
+
<div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-10)' }}>
|
|
34
|
+
<span className={css.cardTitle}>🔍 书分析 / 拆书</span>
|
|
35
|
+
<span className={css.meta}>粘贴一本书/章节文本 → 提炼卖点、结构、可借鉴点与风险(不照搬具体作品)。</span>
|
|
36
|
+
<textarea className={css.input} style={{ minHeight: 180, resize: 'vertical' }} value={text} onChange={e => setText(e.target.value)} placeholder="粘贴要分析的书/章节正文…" />
|
|
37
|
+
<div>
|
|
38
|
+
<button type="button" className={`${css.button} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void run() }}>
|
|
39
|
+
{busy ? '分析中…' : '开始分析'}
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
{error !== '' && <div style={{ color: 'var(--nf-error)', fontSize: 13 }}>⚠ {error}</div>}
|
|
45
|
+
|
|
46
|
+
{result !== null && (
|
|
47
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', gap: 'var(--nf-space-10)' }}>
|
|
48
|
+
{SECTIONS.map(s => (
|
|
49
|
+
<div key={s.key} className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-8)' }}>
|
|
50
|
+
<div style={{ fontSize: 13, fontWeight: 600, color: s.color }}>{s.label}({(result[s.key] ?? []).length})</div>
|
|
51
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
52
|
+
{(result[s.key] ?? []).length === 0 ? <span style={{ color: 'var(--nf-text-2)', fontSize: 12 }}>无</span> : (result[s.key] ?? []).map((x, i) => <div key={i} style={{ fontSize: 12, borderLeft: '3px solid ' + s.color, paddingLeft: 6 }}>{x}</div>)}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { useRef, useState } from 'react'
|
|
7
7
|
import type { NovelApi } from '../api.ts'
|
|
8
|
+
import { setCurrentBook } from '../api.ts'
|
|
8
9
|
import type { OutlineCandidate } from '../../protocol.ts'
|
|
9
10
|
import { extractDocxTextFromBuffer } from '../docx.ts'
|
|
10
11
|
import css from './panel.module.css'
|
|
@@ -38,14 +39,20 @@ export function CreateBookView({
|
|
|
38
39
|
api,
|
|
39
40
|
onBack,
|
|
40
41
|
onCreated,
|
|
42
|
+
initialIdea,
|
|
43
|
+
initialName,
|
|
41
44
|
}: {
|
|
42
45
|
api: NovelApi
|
|
43
46
|
/** 返回书架。 */
|
|
44
47
|
onBack: () => void
|
|
45
48
|
/** 开书成功:进入新书工作台。 */
|
|
46
49
|
onCreated: (id: string) => void
|
|
50
|
+
/** 从「创意灵感」带过来的起步想法(预填到「一句话想法」输入框)。 */
|
|
51
|
+
initialIdea?: string
|
|
52
|
+
/** 从「创意灵感」带过来的初始书名。 */
|
|
53
|
+
initialName?: string
|
|
47
54
|
}) {
|
|
48
|
-
const [name, setName] = useState('')
|
|
55
|
+
const [name, setName] = useState(initialName ?? '')
|
|
49
56
|
const [outlineText, setOutlineText] = useState('')
|
|
50
57
|
const [outlineName, setOutlineName] = useState('')
|
|
51
58
|
const [dragActive, setDragActive] = useState(false)
|
|
@@ -54,13 +61,16 @@ export function CreateBookView({
|
|
|
54
61
|
const outlineFileRef = useRef<HTMLInputElement | null>(null)
|
|
55
62
|
|
|
56
63
|
// ---- 想法 → AI 大纲状态 ----
|
|
57
|
-
const
|
|
58
|
-
const [
|
|
64
|
+
const hasPrefill = initialIdea !== undefined && initialIdea.trim() !== ''
|
|
65
|
+
const [ideaOpen, setIdeaOpen] = useState(hasPrefill)
|
|
66
|
+
const [idea, setIdea] = useState(initialIdea ?? '')
|
|
59
67
|
const [suggesting, setSuggesting] = useState(false)
|
|
60
68
|
const [candidates, setCandidates] = useState<OutlineCandidate[]>([])
|
|
61
69
|
const [pinned, setPinned] = useState<string[]>([])
|
|
62
70
|
/** 回填后提示(如「已填入方案A,可继续修改」)。 */
|
|
63
71
|
const [fillNotice, setFillNotice] = useState('')
|
|
72
|
+
/** 选定方案自带的题材(创建后自动预填到本书创作资产)。 */
|
|
73
|
+
const [selectedGenre, setSelectedGenre] = useState('')
|
|
64
74
|
|
|
65
75
|
const autoName = inferBookNamePreview(outlineText)
|
|
66
76
|
const effectiveName = name.trim() !== '' ? name.trim() : autoName
|
|
@@ -120,6 +130,7 @@ export function CreateBookView({
|
|
|
120
130
|
const handlePick = (candidate: OutlineCandidate): void => {
|
|
121
131
|
setOutlineText(candidate.outline)
|
|
122
132
|
setName(candidate.bookName)
|
|
133
|
+
setSelectedGenre(candidate.genre ?? '')
|
|
123
134
|
setFillNotice(`已填入方案《${candidate.bookName}》(${candidate.genre}),可继续修改后开书`)
|
|
124
135
|
setIdeaOpen(false)
|
|
125
136
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
@@ -136,6 +147,14 @@ export function CreateBookView({
|
|
|
136
147
|
const snapshot = await api.bookCreate(effectiveName, undefined, outlineText.trim() !== '' ? outlineText.trim() : undefined)
|
|
137
148
|
const created = snapshot.books.find(b => b.id === snapshot.activeBookId)
|
|
138
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: '' } })
|
|
156
|
+
} catch { /* ignore */ }
|
|
157
|
+
}
|
|
139
158
|
onCreated(created.id)
|
|
140
159
|
} else {
|
|
141
160
|
setError('开书失败:未找到新书')
|
|
@@ -0,0 +1,200 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 创意灵感:一句话/题材 → 多方向开书灵感。
|
|
3
|
+
*/
|
|
4
|
+
import { useState } from 'react'
|
|
5
|
+
import type { NovelApi } from '../api.ts'
|
|
6
|
+
import type { IdeaInspirationResult } from '../../protocol.ts'
|
|
7
|
+
import css from './panel.module.css'
|
|
8
|
+
|
|
9
|
+
export default function IdeaInspirationView({ api, onUseIdea }: { api: NovelApi; onUseIdea?: (idea: IdeaInspirationResult['ideas'][number]) => void }): JSX.Element {
|
|
10
|
+
const [idea, setIdea] = useState('')
|
|
11
|
+
const [count, setCount] = useState(5)
|
|
12
|
+
const [busy, setBusy] = useState(false)
|
|
13
|
+
const [error, setError] = useState('')
|
|
14
|
+
const [result, setResult] = useState<IdeaInspirationResult | null>(null)
|
|
15
|
+
|
|
16
|
+
const copyText = (it: IdeaInspirationResult['ideas'][number]): string =>
|
|
17
|
+
`《${it.title}》\n题材:${it.genre}\n视角:${it.pov}\n钩子:${it.hook}\n长期兑现:${it.payoff}`
|
|
18
|
+
|
|
19
|
+
const copy = async (it: IdeaInspirationResult['ideas'][number]): Promise<void> => {
|
|
20
|
+
try { await navigator.clipboard.writeText(copyText(it)); setError('') } catch (e) { setError('复制失败:' + (e as Error).message) }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const run = async (): Promise<void> => {
|
|
24
|
+
if (idea.trim().length < 2) { setError('请填写你的方向/一句话想法'); return }
|
|
25
|
+
setBusy(true); setError('')
|
|
26
|
+
try {
|
|
27
|
+
const r = await api.ideaInspiration(idea.trim(), count)
|
|
28
|
+
setResult(r.result)
|
|
29
|
+
} catch (e) { setError((e as Error).message) } finally { setBusy(false) }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className={css.authorPageBody}>
|
|
34
|
+
<div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-10)' }}>
|
|
35
|
+
<span className={css.cardTitle}>💡 创意灵感</span>
|
|
36
|
+
<span className={css.meta}>输入一句话/题材方向 → 给出多个可开书的差异化创意(不照搬具体作品)。</span>
|
|
37
|
+
<textarea className={css.input} style={{ minHeight: 120, resize: 'vertical' }} value={idea} onChange={e => setIdea(e.target.value)} placeholder="例:重生后我在城隍庙摆摊;反派每天都被打脸却越来越强…" />
|
|
38
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
|
39
|
+
<label style={{ fontSize: 12 }}>数量</label>
|
|
40
|
+
<select className={css.input} style={{ width: 'auto', padding: '4px 8px' }} value={count} onChange={e => setCount(Number(e.target.value))}>
|
|
41
|
+
{[3, 5, 8].map(n => <option key={n} value={n}>{n}</option>)}
|
|
42
|
+
</select>
|
|
43
|
+
<button type="button" className={`${css.button} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void run() }}>
|
|
44
|
+
{busy ? '生成中…' : '生成灵感'}
|
|
45
|
+
</button>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
{error !== '' && <div style={{ color: 'var(--nf-error)', fontSize: 13 }}>⚠ {error}</div>}
|
|
50
|
+
|
|
51
|
+
{result !== null && (
|
|
52
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))', gap: 'var(--nf-space-10)' }}>
|
|
53
|
+
{(result.ideas ?? []).map((it, i) => (
|
|
54
|
+
<div key={i} className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-6)' }}>
|
|
55
|
+
<div style={{ fontSize: 15, fontWeight: 600 }}>{it.title}</div>
|
|
56
|
+
<div style={{ fontSize: 12, color: 'var(--nf-success)' }}>{it.genre} · {it.pov}</div>
|
|
57
|
+
<div style={{ fontSize: 13 }}>钩子:{it.hook}</div>
|
|
58
|
+
<div style={{ fontSize: 12, color: 'var(--nf-text-2)' }}>兑现:{it.payoff}</div>
|
|
59
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
|
60
|
+
<button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { void copy(it) }} title="复制给开书向导/其他用到">复制</button>
|
|
61
|
+
{onUseIdea !== undefined && (
|
|
62
|
+
<button type="button" className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`} onClick={() => onUseIdea(it)} title="带着这个灵感去开书(预填书名与想法)">以此方向开书</button>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|