@waterwx/dsh-novel-forge 1.7.3 → 1.9.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.
Files changed (44) hide show
  1. package/lib/client.js +3627 -447
  2. package/lib/client.js.map +1 -1
  3. package/lib/index.js +1127 -141
  4. package/lib/index.js.map +1 -1
  5. package/lib/types/author-assets.d.ts +25 -0
  6. package/lib/types/client/api.d.ts +31 -1
  7. package/lib/types/client/locales.d.ts +16 -0
  8. package/lib/types/client/panel/AdaptModeView.d.ts +4 -0
  9. package/lib/types/client/panel/AuthorAssetsView.d.ts +4 -0
  10. package/lib/types/client/panel/AuthorHome.d.ts +30 -0
  11. package/lib/types/client/panel/ModelManager.d.ts +14 -0
  12. package/lib/types/client/panel/ModelPicker.d.ts +11 -0
  13. package/lib/types/client/panel/ProgressConsole.d.ts +17 -0
  14. package/lib/types/client/panel/ProgressHomeView.d.ts +5 -0
  15. package/lib/types/client/panel/ReasoningSection.d.ts +10 -0
  16. package/lib/types/client/panel/SettingsView.d.ts +14 -0
  17. package/lib/types/client/text.d.ts +9 -0
  18. package/lib/types/engine.d.ts +39 -1
  19. package/lib/types/index.d.ts +8 -0
  20. package/lib/types/protocol.d.ts +339 -0
  21. package/package.json +1 -1
  22. package/src/author-assets.ts +166 -0
  23. package/src/client/api.ts +82 -0
  24. package/src/client/locales.ts +32 -0
  25. package/src/client/mount.tsx._new +97 -0
  26. package/src/client/mount.tsx._nf +97 -0
  27. package/src/client/mount.tsx._tmp +97 -0
  28. package/src/client/panel/AdaptModeView.tsx +202 -0
  29. package/src/client/panel/AuthorAssetsView.tsx +206 -0
  30. package/src/client/panel/AuthorHome.tsx +107 -0
  31. package/src/client/panel/ImportModal.tsx +2 -1
  32. package/src/client/panel/ModelManager.tsx +205 -0
  33. package/src/client/panel/NovelPanel.tsx +73 -79
  34. package/src/client/panel/ProgressConsole.tsx +64 -0
  35. package/src/client/panel/ProgressHomeView.tsx +87 -0
  36. package/src/client/panel/ReasoningSection.tsx +31 -0
  37. package/src/client/panel/SettingsView.tsx +292 -0
  38. package/src/client/panel/panel.module.css +325 -0
  39. package/src/client/sidebar-entry.ts +1 -1
  40. package/src/client/text.ts +28 -0
  41. package/src/engine.ts +442 -0
  42. package/src/index.ts +20 -0
  43. package/src/protocol.ts +356 -0
  44. package/src/routes.ts +323 -0
@@ -9,10 +9,13 @@ import type { ReactElement } from 'react'
9
9
  import type { NovelApi } from '../api.ts'
10
10
  import type { PanelController } from './controller.ts'
11
11
  import { tt } from './helpers.ts'
12
+ import { ModelManager } from './ModelManager.tsx'
13
+ import { ReasoningSection } from './ReasoningSection.tsx'
12
14
  import { BarChart3, Book, BookMarked, BookOpen, Brain, Clapperboard, Factory, FileText, Folder, GitBranch, Library, MessageSquare, Palette, PenLine, PlugZap, RotateCcw, ScrollText, Search, Settings, Sparkles, Wrench } from 'lucide-react'
13
15
  import { AssistantTab } from './AssistantTab.tsx'
14
16
  import { AssetsTab } from './AssetsTab.tsx'
15
17
  import { ShelfView } from './ShelfView.tsx'
18
+ import { AuthorHome } from './AuthorHome.tsx'
16
19
  import { ReaderView } from './ReaderView.tsx'
17
20
  import { RunPanel } from './RunPanel.tsx'
18
21
  import { CreateBookView } from './CreateBookView.tsx'
@@ -73,40 +76,37 @@ const NAV_GROUPS: ReadonlyArray<{ id: string; label: string; items: ReadonlyArra
73
76
  id: 'create',
74
77
  label: '创作',
75
78
  items: [
76
- { id: 'workflow', label: tt('tab.workflow'), icon: <Wrench size={16} /> },
77
- { id: 'overview', label: tt('tab.overview'), icon: <FileText size={16} /> },
78
- { id: 'blurb', label: '简介 / 封面', icon: <BookOpen size={16} /> },
79
- { id: 'plan', label: tt('tab.plan'), icon: <BookMarked size={16} /> },
80
- { id: 'plotlines', label: '长线管理', icon: <ScrollText size={16} /> },
81
- { id: 'book', label: '本书设定', icon: <Library size={16} /> },
79
+ { id: 'workflow', label: tt('tab.workflow'), icon: <Wrench size={18} /> },
80
+ { id: 'overview', label: tt('tab.overview'), icon: <FileText size={18} /> },
81
+ { id: 'blurb', label: '简介 / 封面', icon: <BookOpen size={18} /> },
82
+ { id: 'plan', label: tt('tab.plan'), icon: <BookMarked size={18} /> },
83
+ { id: 'plotlines', label: '长线管理', icon: <ScrollText size={18} /> },
84
+ { id: 'book', label: '本书设定', icon: <Library size={18} /> },
82
85
  ],
83
86
  },
84
87
  {
85
88
  id: 'tools',
86
89
  label: '工具',
87
90
  items: [
88
- { id: 'assistant', label: tt('tab.assistant'), icon: <MessageSquare size={16} /> },
89
- { id: 'progress', label: 'AI 进度', icon: <BarChart3 size={16} /> },
90
- { id: 'breakdown', label: '拆书分析', icon: <Search size={16} /> },
91
- { id: 'manhua', label: '漫剧工作台', icon: <Clapperboard size={16} /> },
92
- { id: 'run', label: '生产单', icon: <Factory size={16} /> },
91
+ { id: 'assistant', label: tt('tab.assistant'), icon: <MessageSquare size={18} /> },
92
+ { id: 'progress', label: 'AI 进度', icon: <BarChart3 size={18} /> },
93
+ { id: 'breakdown', label: '拆书分析', icon: <Search size={18} /> },
94
+ { id: 'manhua', label: '漫剧工作台', icon: <Clapperboard size={18} /> },
95
+ { id: 'run', label: '生产单', icon: <Factory size={18} /> },
93
96
  ],
94
97
  },
95
98
  {
96
99
  id: 'assets',
97
100
  label: '资产',
98
101
  items: [
99
- { id: 'assets', label: '创作资产', icon: <Wrench size={16} /> },
102
+ { id: 'assets', label: '创作资产', icon: <Wrench size={18} /> },
100
103
  ],
101
104
  },
102
105
  ]
103
106
 
104
107
  /** Settings tab — pinned to the bottom of the nav rail. */
105
- const SETTINGS_TAB: { id: NovelTab; label: string; icon: ReactElement } = { id: 'settings', label: tt('tab.settings'), icon: <Settings size={16} /> }
108
+ const SETTINGS_TAB: { id: NovelTab; label: string; icon: ReactElement } = { id: 'settings', label: tt('tab.settings'), icon: <Settings size={18} /> }
106
109
 
107
- /** Common DeepSeek model presets shown in settings; users can also type any model id. */
108
- const MODEL_PRESETS = ['deepseek-v4-flash', 'deepseek-v4-pro', 'deepseek-chat', 'deepseek-reasoner'] as const
109
- const REASONING_OPTIONS = ['off', 'low', 'high', 'max'] as const
110
110
 
111
111
  /** 设置页内子导航分组。 */
112
112
  const SETTINGS_SECTIONS: ReadonlyArray<{ id: 'model' | 'writing' | 'image' | 'files' | 'appearance'; label: string; icon: ReactElement }> = [
@@ -316,6 +316,12 @@ function DiffList({ original, draft, fontSize }: { original: string; draft: stri
316
316
  export function NovelPanel({ controller, api }: NovelPanelProps) {
317
317
  const [activeTab, setActiveTab] = useState<NovelTab>('workflow')
318
318
  const [config, setConfig] = useState<NovelConfig | null>(null)
319
+ /** 自定义背景(URL / dataURL),覆盖主题默认背景。 */
320
+ const [themeBg, setThemeBg] = useState<string | undefined>(undefined)
321
+ /** 自定义背景遮罩/模糊强度 0-80。 */
322
+ const [themeBgBlur, setThemeBgBlur] = useState(0)
323
+ /** 玻璃透明度 0-100(100=主题原样)。 */
324
+ const [themeOpacity, setThemeOpacity] = useState(100)
319
325
  const [project, setProject] = useState<ProjectState | null>(null)
320
326
  const [generatedFiles, setGeneratedFiles] = useState<string[]>([])
321
327
  const [outlineText, setOutlineText] = useState('')
@@ -330,7 +336,6 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
330
336
  const [notice, setNotice] = useState('')
331
337
  const [progress, setProgress] = useState<ProgressLine[]>([])
332
338
  const [configDraft, setConfigDraft] = useState<NovelConfig | null>(null)
333
- const [modelCustomMode, setModelCustomMode] = useState(false)
334
339
  const [expandedChapter, setExpandedChapter] = useState<number | null>(null)
335
340
  /** 复盘记录页:当前展开的章节号。 */
336
341
  const [expandedReviewChapter, setExpandedReviewChapter] = useState<number | null>(null)
@@ -699,7 +704,6 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
699
704
  const status = await api.status()
700
705
  setConfig(status.config)
701
706
  setConfigDraft(status.config)
702
- setModelCustomMode(!(MODEL_PRESETS as readonly string[]).includes(status.config.model))
703
707
  setAuditStatus(status.audit ?? null)
704
708
  setProject(status.project ?? null)
705
709
  // 人物志存档同步:有 roleStatus 存档直接显示,无需重新刷新计算。
@@ -748,6 +752,17 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
748
752
  }
749
753
  }, [api, refresh, refreshShelf])
750
754
 
755
+ /** 首页「设置」与书内设置同入口:激活当前/首选书并跳到设置页。 */
756
+ const openSettingsFromHome = async (): Promise<void> => {
757
+ const targetId = shelf?.activeBookId ?? shelf?.books[0]?.id ?? null
758
+ if (targetId === null || targetId === undefined) {
759
+ setError('请先在书架开一本书;设置页以书籍工作台为入口')
760
+ return
761
+ }
762
+ await activateBook(targetId, 'workspace')
763
+ setActiveTab('settings')
764
+ }
765
+
751
766
  /** Handle a docx file (pick or drag): parse locally, save outline. */
752
767
  const handleDocxFile = useCallback(async (file: File) => {
753
768
  setBusy(true)
@@ -2066,6 +2081,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
2066
2081
  imageApiModel: configDraft.imageApiModel,
2067
2082
  imageApiEnabled: configDraft.imageApiEnabled ?? false,
2068
2083
  imageModels: configDraft.imageModels ?? [],
2084
+ savedModels: configDraft.savedModels ?? [],
2069
2085
  })
2070
2086
  setConfig(result.config)
2071
2087
  setConfigDraft(result.config)
@@ -2364,12 +2380,20 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
2364
2380
  n += assets?.antiAiRules?.length ?? 0
2365
2381
  return n
2366
2382
  })()
2383
+ /** 自定义背景:优先用户显式选择(themeBg),否则用 config 里的持久化值。 */
2384
+ const effectiveBg = themeBg !== undefined && themeBg !== '' ? themeBg : (config?.themeBackground ?? '')
2385
+ const effectiveBgBlur = themeBgBlur > 0 ? themeBgBlur : (config?.themeBackgroundBlur ?? 0)
2386
+ const effectiveOpacity = themeOpacity > 0 ? themeOpacity : (config?.themeOpacity ?? 100)
2387
+ const panelBgStyle: Record<string, string> = {}
2388
+ if (effectiveBg !== '') panelBgStyle['--nf-glass-bg-image' as string] = 'url(' + effectiveBg + ')'
2389
+ if (effectiveBgBlur > 0) panelBgStyle['--nf-glass-bg-dim' as string] = String(effectiveBgBlur)
2390
+ panelBgStyle['--nf-glass-opacity' as string] = String(effectiveOpacity) + '%'
2367
2391
 
2368
2392
  return (
2369
- <div className={css.panel} data-nf-theme={panelTheme} data-nf-mode={themeMode === 'system' ? undefined : themeMode} data-nf-density={themeDensity}>
2393
+ <div className={css.panel} data-nf-theme={panelTheme} data-nf-mode={themeMode === 'system' ? undefined : themeMode} data-nf-density={themeDensity} style={panelBgStyle}>
2370
2394
  {viewMode === 'shelf' ? (
2371
- /* 书架首页:默认视图,选择一本书进入工作台或阅读页 */
2372
- <ShelfView
2395
+ /* 书架首页:作者级左侧导航 + 书架/改编/资产库/设置 */
2396
+ <AuthorHome
2373
2397
  api={api}
2374
2398
  shelf={shelf ?? { books: [], activeBookId: null }}
2375
2399
  onOpenBook={async (id) => {
@@ -2380,6 +2404,16 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
2380
2404
  }}
2381
2405
  onAddBook={() => { setViewMode('create') }}
2382
2406
  onImportBook={() => { setShowImport(true) }}
2407
+ onOpenSettings={() => { void openSettingsFromHome() }}
2408
+ onTheme={(t, m, d) => { changePanelTheme(t); changeThemeMode(m); changeThemeDensity(d) }}
2409
+ onBackground={(bg, blur) => { setThemeBg(bg); setThemeBgBlur(blur) }}
2410
+ onOpacity={(n) => { setThemeOpacity(n) }}
2411
+ adaptEnabled={config?.enableAdaptMode === true}
2412
+ progress={progress}
2413
+ busy={busy}
2414
+ busyLabel={busyLabel}
2415
+ liveBar={liveBar}
2416
+ onClearProgress={() => { setProgress([]) }}
2383
2417
  />
2384
2418
  ) : viewMode === 'create' ? (
2385
2419
  /* 开书向导:独立页面 */
@@ -3804,65 +3838,25 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
3804
3838
  </div>
3805
3839
 
3806
3840
  {settingsTab === 'model' && (
3807
- <div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-24)' }}>
3808
- <span className={css.cardTitle}><Brain size={18} style={{ verticalAlign: -3 }} /> 模型与推理</span>
3809
- <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-24)' }}>
3810
- <div className={css.field}>
3811
- <label className={css.fieldLabel}>{tt('settings.provider')}</label>
3812
- <input className={css.input} value={configDraft.provider} onChange={e => { setConfigDraft({ ...configDraft, provider: e.target.value }) }} />
3813
- </div>
3814
- <div className={css.field}>
3815
- <label className={css.fieldLabel}>{tt('settings.model')}</label>
3816
- <select
3817
- className={css.input}
3818
- value={modelCustomMode ? '__custom__' : configDraft.model}
3819
- onChange={e => {
3820
- const v = e.target.value
3821
- if (v === '__custom__') {
3822
- setModelCustomMode(true)
3823
- } else {
3824
- setModelCustomMode(false)
3825
- setConfigDraft({ ...configDraft, model: v })
3826
- }
3827
- }}
3828
- >
3829
- {MODEL_PRESETS.map(m => <option key={m} value={m}>{m}</option>)}
3830
- <option value="__custom__">{tt('settings.modelCustom')}</option>
3831
- </select>
3832
- {modelCustomMode && (
3833
- <input
3834
- className={css.input}
3835
- style={{ marginTop: 'var(--nf-space-6)' }}
3836
- value={configDraft.model}
3837
- placeholder={tt('settings.modelCustomPlaceholder')}
3838
- onChange={e => { setConfigDraft({ ...configDraft, model: e.target.value }) }}
3839
- />
3840
- )}
3841
- </div>
3842
- <div className={css.field}>
3843
- <label className={css.fieldLabel}>{tt('settings.reasoningEffort')}</label>
3844
- <select
3845
- className={css.input}
3846
- value={configDraft.reasoningEffort ?? 'off'}
3847
- onChange={e => { setConfigDraft({ ...configDraft, reasoningEffort: e.target.value as NovelConfig['reasoningEffort'] }) }}
3848
- >
3849
- {REASONING_OPTIONS.map(v => <option key={v} value={v}>{tt(`settings.reasoning.${v}`)}</option>)}
3850
- </select>
3851
- <span className={css.meta}>{tt('settings.reasoningHint')}</span>
3852
- </div>
3853
- <div className={css.field}>
3854
- <label className={css.fieldLabel}>{tt('settings.analysisReasoning')}</label>
3855
- <select
3856
- className={css.input}
3857
- value={configDraft.analysisReasoning ?? 'low'}
3858
- onChange={e => { setConfigDraft({ ...configDraft, analysisReasoning: e.target.value as NovelConfig['analysisReasoning'] }) }}
3859
- >
3860
- {REASONING_OPTIONS.map(v => <option key={v} value={v}>{tt(`settings.reasoning.${v}`)}</option>)}
3861
- </select>
3862
- <span className={css.meta}>{tt('settings.analysisReasoningHint')}</span>
3863
- </div>
3841
+ <>
3842
+ <div className={`${css.card} ${css.settingsCard}`} style={{ gap: 'var(--nf-space-24)' }}>
3843
+ <span className={css.cardTitle}><Brain size={18} style={{ verticalAlign: -3 }} /> 模型与推理</span>
3844
+ <ModelManager
3845
+ api={api}
3846
+ provider={configDraft.provider}
3847
+ model={configDraft.model}
3848
+ savedModels={configDraft.savedModels ?? []}
3849
+ onProvider={v => { setConfigDraft({ ...configDraft, provider: v }) }}
3850
+ onModel={v => { setConfigDraft({ ...configDraft, model: v }) }}
3851
+ onSavedModels={models => setConfigDraft({ ...configDraft, savedModels: models })}
3852
+ />
3864
3853
  </div>
3865
- </div>
3854
+ <ReasoningSection
3855
+ reasoningEffort={configDraft.reasoningEffort ?? 'off'}
3856
+ analysisReasoning={configDraft.analysisReasoning ?? 'low'}
3857
+ onChange={patch => setConfigDraft({ ...configDraft, ...patch })}
3858
+ />
3859
+ </>
3866
3860
  )}
3867
3861
 
3868
3862
  {settingsTab === 'writing' && (
@@ -0,0 +1,64 @@
1
+ /**
2
+ * 共享「AI 进度」实时控制台:当前任务大进度条 + 全量活动日志。
3
+ * 书内与首页共用一个表现,内容来自同一份进度状态。
4
+ */
5
+ import { useEffect, useRef } from 'react'
6
+ import css from './panel.module.css'
7
+
8
+ export interface ProgressLine {
9
+ id: number;
10
+ text: string;
11
+ kind: 'info' | 'done' | 'error';
12
+ live?: boolean;
13
+ ratio?: number;
14
+ }
15
+
16
+ export function ProgressConsole({ progress, busy, busyLabel, liveBar, onClear }: {
17
+ progress: ProgressLine[];
18
+ busy: boolean;
19
+ busyLabel: string;
20
+ liveBar: { text: string; ratio?: number } | null;
21
+ onClear: () => void;
22
+ }) {
23
+ const endRef = useRef<HTMLDivElement | null>(null);
24
+ useEffect(() => {
25
+ endRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
26
+ }, [progress]);
27
+
28
+ return (
29
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-10)', height: '100%', minHeight: 0 }}>
30
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
31
+ <span style={{ fontWeight: 600 }}>📊 AI 进度</span>
32
+ <button type="button" className={css.button + ' ' + css.buttonSmall} onClick={onClear} title="清空活动记录">清空</button>
33
+ </div>
34
+ {(busy && (busyLabel !== '' || liveBar !== null)) && (
35
+ <div style={{ border: '1px solid var(--nf-accent)', borderRadius: 'var(--nf-radius-10)', padding: 'var(--nf-space-8) var(--nf-space-12)', display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)', background: 'color-mix(in srgb, var(--nf-accent) 6%, transparent)' }}>
36
+ <span style={{ fontSize: 'var(--nf-fs-12)', fontWeight: 600, color: 'var(--nf-accent)' }}>✍ {busyLabel !== '' ? busyLabel : (liveBar?.text ?? '任务进行中')}…</span>
37
+ {liveBar?.ratio !== undefined && (
38
+ <div className={css.bigProgressBar}><div className={css.bigProgressBarFill} style={{ width: Math.round(liveBar.ratio * 100) + '%' }} /></div>
39
+ )}
40
+ {liveBar?.text !== undefined && <span className={css.liveText}>{liveBar.text}</span>}
41
+ </div>
42
+ )}
43
+ <div className={css.progress} style={{ flex: 1, minHeight: 0, overflowY: 'auto', border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-10)', background: 'var(--nf-bg-inset)', padding: 'var(--nf-space-8)', display: 'flex', flexDirection: 'column' }}>
44
+ {progress.length === 0 ? (
45
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 'var(--nf-space-4)', color: 'var(--nf-text-3)', fontSize: 'var(--nf-fs-12)', minHeight: 120 }}>
46
+ <span style={{ fontSize: 'var(--nf-fs-22)' }}>📭</span>
47
+ <span>暂无活动记录</span>
48
+ <span>生成、审稿等操作会显示在这里</span>
49
+ </div>
50
+ ) : (
51
+ progress.map(line => (
52
+ <div key={line.id} className={line.kind === 'done' ? css.progressLineDone : line.kind === 'error' ? css.progressLineError : line.live === true ? css.progressLineLive : css.progressLine}>
53
+ {line.live === true && (
54
+ <span className={css.progressBar}><span className={css.progressBarFill} style={{ width: Math.round((line.ratio ?? 0) * 100) + '%' }} /></span>
55
+ )}
56
+ {line.text}
57
+ </div>
58
+ ))
59
+ )}
60
+ <div ref={endRef} />
61
+ </div>
62
+ </div>
63
+ );
64
+ }
@@ -0,0 +1,87 @@
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
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 推理强度(思考强度)独立容器:写作类/分析类两个 reasoningEffort 下拉。
3
+ */
4
+ import { Brain } from 'lucide-react'
5
+ import type { NovelConfig } from '../../protocol.ts'
6
+ import { tt } from './helpers.ts'
7
+ import css from './panel.module.css'
8
+
9
+ const REASONING_OPTIONS = ['off', 'low', 'high', 'max'] as const
10
+ const REASONING_LABEL: Record<string, string> = {
11
+ off: tt('settings.reasoning.off'),
12
+ low: tt('settings.reasoning.low'),
13
+ high: tt('settings.reasoning.high'),
14
+ max: tt('settings.reasoning.max'),
15
+ }
16
+
17
+ export interface ReasoningSectionProps {
18
+ reasoningEffort: NovelConfig['reasoningEffort']
19
+ analysisReasoning: NovelConfig['analysisReasoning']
20
+ onChange: (patch: { reasoningEffort?: NovelConfig['reasoningEffort']; analysisReasoning?: NovelConfig['analysisReasoning'] }) => void
21
+ }
22
+
23
+ export function ReasoningSection({ reasoningEffort, analysisReasoning, onChange }: ReasoningSectionProps): JSX.Element {
24
+ return (
25
+ <div className={css.card + ' ' + css.settingsCard} style={{ gap: 'var(--nf-space-24)' }}>
26
+ <span className={css.cardTitle}><Brain size={18} style={{ verticalAlign: -3 }} /> {tt('settings.reasoningSection')}</span>
27
+ <div className={css.field}><label className={css.fieldLabel}>{tt('settings.reasoningEffort')}</label><select className={css.input} value={reasoningEffort ?? 'off'} onChange={e => onChange({ reasoningEffort: e.target.value as NovelConfig['reasoningEffort'] })}>{REASONING_OPTIONS.map(v => <option key={v} value={v}>{REASONING_LABEL[v]}</option>)}</select><span className={css.meta}>{tt('settings.reasoningHint')}</span></div>
28
+ <div className={css.field}><label className={css.fieldLabel}>{tt('settings.analysisReasoning')}</label><select className={css.input} value={analysisReasoning ?? 'low'} onChange={e => onChange({ analysisReasoning: e.target.value as NovelConfig['analysisReasoning'] })}>{REASONING_OPTIONS.map(v => <option key={v} value={v}>{REASONING_LABEL[v]}</option>)}</select><span className={css.meta}>{tt('settings.analysisReasoningHint')}</span></div>
29
+ </div>
30
+ )
31
+ }