@waterwx/dsh-novel-forge 1.4.0 → 1.5.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/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/horror-game-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/shinkai-anime.webp +0 -0
- package/assets/styles/tactical-stealth.webp +0 -0
- package/assets/styles/vaporwave-3d.webp +0 -0
- package/assets/styles/vhs90.webp +0 -0
- package/lib/client.js +2073 -658
- package/lib/client.js.map +1 -1
- package/lib/index.js +919 -64
- package/lib/index.js.map +1 -1
- package/lib/types/client/api.d.ts +15 -1
- package/lib/types/client/locales.d.ts +4 -2
- package/lib/types/client/panel/MangaWorkspace.d.ts +9 -0
- package/lib/types/client/panel/NovelPanel.d.ts +1 -1
- package/lib/types/client/panel/StoryboardTab.d.ts +13 -0
- package/lib/types/client/panel/StyleGallery.d.ts +1 -0
- package/lib/types/engine.d.ts +46 -4
- package/lib/types/index.d.ts +2 -0
- package/lib/types/protocol.d.ts +194 -3
- package/lib/types/style-library.d.ts +33 -0
- package/package.json +2 -1
- package/src/client/api.ts +36 -1
- package/src/client/locales.ts +7 -3
- package/src/client/panel/ImportModal.tsx +123 -21
- package/src/client/panel/MangaWorkspace.tsx +185 -0
- package/src/client/panel/NovelPanel.tsx +239 -150
- package/src/client/panel/StoryboardTab.tsx +287 -0
- package/src/client/panel/StyleGallery.tsx +109 -0
- package/src/client/panel/panel.module.css +77 -0
- package/src/engine.ts +457 -39
- package/src/index.ts +6 -0
- package/src/protocol.ts +200 -3
- package/src/routes.ts +280 -13
- package/src/style-library.ts +78 -0
|
@@ -17,6 +17,9 @@ import { RunPanel } from './RunPanel.tsx'
|
|
|
17
17
|
import { CreateBookView } from './CreateBookView.tsx'
|
|
18
18
|
import { ImportModal } from './ImportModal.tsx'
|
|
19
19
|
import { WorldTab } from './WorldTab.tsx'
|
|
20
|
+
import { StoryboardTab } from './StoryboardTab.tsx'
|
|
21
|
+
import { MangaWorkspace } from './MangaWorkspace.tsx'
|
|
22
|
+
import { StyleGallery } from './StyleGallery.tsx'
|
|
20
23
|
import { AuditIssueRow, PlotlineCard, PlotlineHealthPanel, PlotlinePlanPanel, PlotlineSuggestionPanel, RoleCandidateRow, RoleCard, StatCell, TodoRow } from './views.tsx'
|
|
21
24
|
import { extractDocxTextFromBuffer } from '../docx.ts'
|
|
22
25
|
import type {
|
|
@@ -43,7 +46,7 @@ export type NovelTab =
|
|
|
43
46
|
| 'workflow' | 'overview' | 'blurb' | 'plan' | 'bible' | 'world' | 'foreshadow' | 'assistant' | 'settings'
|
|
44
47
|
| 'characters' | 'roles' | 'facts' | 'plotlines' | 'reviews' | 'progress' | 'breakdown'
|
|
45
48
|
| 'roleImage' | 'scenes'
|
|
46
|
-
| 'assetsGenre' | 'assetsProgression' | 'assetsTemplates' | 'assetsRules' | 'assetsStyle' | 'run'
|
|
49
|
+
| 'assetsGenre' | 'assetsProgression' | 'assetsTemplates' | 'assetsRules' | 'assetsStyle' | 'run' | 'manhua' | 'styleLib'
|
|
47
50
|
| 'book' | 'assets'
|
|
48
51
|
|
|
49
52
|
/** Panel shell props. */
|
|
@@ -88,6 +91,8 @@ const NAV_GROUPS: ReadonlyArray<{ id: string; label: string; items: ReadonlyArra
|
|
|
88
91
|
{ id: 'breakdown', label: '拆书分析', icon: '🔍' },
|
|
89
92
|
{ id: 'roleImage', label: '角色形象', icon: '🖼️' },
|
|
90
93
|
{ id: 'scenes', label: '场景库', icon: '🏞️' },
|
|
94
|
+
{ id: 'styleLib', label: '风格库', icon: '🎨' },
|
|
95
|
+
{ id: 'manhua', label: '漫剧', icon: '🎬' },
|
|
91
96
|
{ id: 'run', label: '生产单', icon: '🏭' },
|
|
92
97
|
],
|
|
93
98
|
},
|
|
@@ -791,6 +796,30 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
791
796
|
}
|
|
792
797
|
}
|
|
793
798
|
|
|
799
|
+
/** 反推大纲:从已写章节正文反向生成全书总纲(流式进度)。 */
|
|
800
|
+
const handleReverseOutline = async (): Promise<void> => {
|
|
801
|
+
if (project === null) return
|
|
802
|
+
if (!window.confirm('将从已写章节正文反推出全书总纲(覆盖当前大纲文本,不影响章节/设定/进度)。确定继续?')) return
|
|
803
|
+
setBusy(true)
|
|
804
|
+
setError('')
|
|
805
|
+
try {
|
|
806
|
+
await api.outlineReverse(frame => {
|
|
807
|
+
if (frame.type === 'outline-progress') {
|
|
808
|
+
pushProgress(`反推大纲:${frame.phase}(${frame.done}/${frame.total})`, 'info')
|
|
809
|
+
} else if (frame.type === 'outline-done') {
|
|
810
|
+
setOutlineText(frame.outline)
|
|
811
|
+
pushProgress(`反推大纲完成(${frame.chars} 字),已保存为当前总纲`, 'done')
|
|
812
|
+
}
|
|
813
|
+
})
|
|
814
|
+
await refresh(false)
|
|
815
|
+
} catch (err) {
|
|
816
|
+
setError((err as Error).message)
|
|
817
|
+
pushProgress(`反推大纲失败:${(err as Error).message}`, 'error')
|
|
818
|
+
} finally {
|
|
819
|
+
setBusy(false)
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
794
823
|
/** 大纲页「更新大纲」展开/收起(展开时预填当前大纲文本)。 */
|
|
795
824
|
const handleToggleUpdateOutline = (): void => {
|
|
796
825
|
if (!updatingOutline && project !== null) setOutlineText(project.outline)
|
|
@@ -1150,7 +1179,8 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
1150
1179
|
setBusyLabel(`生成「${name}」形象锚点…`)
|
|
1151
1180
|
setError('')
|
|
1152
1181
|
try {
|
|
1153
|
-
|
|
1182
|
+
const activePlan = (project?.mangaPlans ?? []).find(pl => pl.active)
|
|
1183
|
+
await api.roles({ op: 'visual', name, styleId: activePlan?.styleId, filterId: activePlan?.filterId })
|
|
1154
1184
|
await refresh(false)
|
|
1155
1185
|
pushProgress(`已生成「${name}」形象锚点`, 'done')
|
|
1156
1186
|
} catch (err) {
|
|
@@ -3001,6 +3031,15 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
3001
3031
|
{tt('overview.bookName')}: {project.bookName} · {project.outline.length} 字
|
|
3002
3032
|
{project.outlinePath !== undefined && <span> · {project.outlinePath}</span>}
|
|
3003
3033
|
</span>
|
|
3034
|
+
<button
|
|
3035
|
+
type="button"
|
|
3036
|
+
className={`${css.button} ${css.buttonPrimary}`}
|
|
3037
|
+
disabled={busy || !(project.chapters ?? []).some(c => c.status === 'written' || c.status === 'approved' || c.status === 'rejected')}
|
|
3038
|
+
onClick={() => { void handleReverseOutline() }}
|
|
3039
|
+
title="从已写章节正文反向生成全书总纲(覆盖当前大纲文本)"
|
|
3040
|
+
>
|
|
3041
|
+
🔄 反推大纲
|
|
3042
|
+
</button>
|
|
3004
3043
|
<button type="button" className={css.button} disabled={busy} onClick={() => { handleToggleUpdateOutline() }}>
|
|
3005
3044
|
{updatingOutline ? '收起' : '更新大纲'}
|
|
3006
3045
|
</button>
|
|
@@ -3076,7 +3115,12 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
3076
3115
|
</>
|
|
3077
3116
|
) : (
|
|
3078
3117
|
/* 只读展示:大纲是开书时的出生证明 */
|
|
3118
|
+
<>
|
|
3119
|
+
<div className={css.meta} style={{ marginBottom: 6 }}>
|
|
3120
|
+
大纲是本书的「出生证明」;已写章节可点右上角「🔄 反推大纲」从正文反推,或「更新大纲」手动修订。
|
|
3121
|
+
</div>
|
|
3079
3122
|
<pre className={css.outlineReadonly}>{project.outline}</pre>
|
|
3123
|
+
</>
|
|
3080
3124
|
)}
|
|
3081
3125
|
</div>
|
|
3082
3126
|
<div className={css.card}>
|
|
@@ -3729,170 +3773,206 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
3729
3773
|
{activeTab === 'assets' && <AssetsTab api={api} />}
|
|
3730
3774
|
|
|
3731
3775
|
{activeTab === 'settings' && configDraft !== null && (
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
<div className={css.
|
|
3735
|
-
<
|
|
3736
|
-
<
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
<label className={css.fieldLabel}>{tt('settings.outputDir')}</label>
|
|
3740
|
-
<input className={css.input} value={configDraft.outputDir} onChange={e => { setConfigDraft({ ...configDraft, outputDir: e.target.value }) }} />
|
|
3741
|
-
</div>
|
|
3742
|
-
<div className={css.row} style={{ alignItems: 'flex-start' }}>
|
|
3743
|
-
<div className={css.field} style={{ flex: 1 }}>
|
|
3744
|
-
<label className={css.fieldLabel}>{tt('settings.provider')}</label>
|
|
3745
|
-
<input className={css.input} value={configDraft.provider} onChange={e => { setConfigDraft({ ...configDraft, provider: e.target.value }) }} />
|
|
3776
|
+
<>
|
|
3777
|
+
{/* ① 路径与输出 */}
|
|
3778
|
+
<div className={`${css.card} ${css.settingsCard}`}>
|
|
3779
|
+
<span className={css.cardTitle}>📁 路径与输出</span>
|
|
3780
|
+
<div className={css.field}>
|
|
3781
|
+
<label className={css.fieldLabel}>{tt('settings.outlinePath')}</label>
|
|
3782
|
+
<input className={css.input} value={configDraft.outlinePath} onChange={e => { setConfigDraft({ ...configDraft, outlinePath: e.target.value }) }} />
|
|
3746
3783
|
</div>
|
|
3747
|
-
<div className={css.field}
|
|
3748
|
-
<label className={css.fieldLabel}>{tt('settings.
|
|
3749
|
-
<
|
|
3750
|
-
className={css.input}
|
|
3751
|
-
value={modelCustomMode ? '__custom__' : configDraft.model}
|
|
3752
|
-
onChange={e => {
|
|
3753
|
-
const v = e.target.value
|
|
3754
|
-
if (v === '__custom__') {
|
|
3755
|
-
setModelCustomMode(true)
|
|
3756
|
-
} else {
|
|
3757
|
-
setModelCustomMode(false)
|
|
3758
|
-
setConfigDraft({ ...configDraft, model: v })
|
|
3759
|
-
}
|
|
3760
|
-
}}
|
|
3761
|
-
>
|
|
3762
|
-
{MODEL_PRESETS.map(m => <option key={m} value={m}>{m}</option>)}
|
|
3763
|
-
<option value="__custom__">{tt('settings.modelCustom')}</option>
|
|
3764
|
-
</select>
|
|
3765
|
-
{modelCustomMode && (
|
|
3766
|
-
<input
|
|
3767
|
-
className={css.input}
|
|
3768
|
-
style={{ marginTop: 6 }}
|
|
3769
|
-
value={configDraft.model}
|
|
3770
|
-
placeholder={tt('settings.modelCustomPlaceholder')}
|
|
3771
|
-
onChange={e => { setConfigDraft({ ...configDraft, model: e.target.value }) }}
|
|
3772
|
-
/>
|
|
3773
|
-
)}
|
|
3784
|
+
<div className={css.field}>
|
|
3785
|
+
<label className={css.fieldLabel}>{tt('settings.outputDir')}</label>
|
|
3786
|
+
<input className={css.input} value={configDraft.outputDir} onChange={e => { setConfigDraft({ ...configDraft, outputDir: e.target.value }) }} />
|
|
3774
3787
|
</div>
|
|
3775
|
-
<div className={css.
|
|
3776
|
-
<
|
|
3777
|
-
<select
|
|
3778
|
-
className={css.input}
|
|
3779
|
-
value={configDraft.reasoningEffort ?? 'off'}
|
|
3780
|
-
onChange={e => { setConfigDraft({ ...configDraft, reasoningEffort: e.target.value as NovelConfig['reasoningEffort'] }) }}
|
|
3781
|
-
>
|
|
3782
|
-
{REASONING_OPTIONS.map(v => <option key={v} value={v}>{tt(`settings.reasoning.${v}`)}</option>)}
|
|
3783
|
-
</select>
|
|
3784
|
-
<span className={css.meta}>{tt('settings.reasoningHint')}</span>
|
|
3788
|
+
<div className={css.row}>
|
|
3789
|
+
<button type="button" className={css.button} onClick={() => { void api.openFolder() }}>{tt('settings.openFolder')}</button>
|
|
3785
3790
|
</div>
|
|
3786
3791
|
</div>
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3792
|
+
|
|
3793
|
+
{/* ② 模型与推理 */}
|
|
3794
|
+
<div className={`${css.card} ${css.settingsCard}`}>
|
|
3795
|
+
<span className={css.cardTitle}>🤖 模型与推理</span>
|
|
3796
|
+
<div className={css.row} style={{ alignItems: 'flex-start' }}>
|
|
3797
|
+
<div className={css.field} style={{ flex: 1 }}>
|
|
3798
|
+
<label className={css.fieldLabel}>{tt('settings.provider')}</label>
|
|
3799
|
+
<input className={css.input} value={configDraft.provider} onChange={e => { setConfigDraft({ ...configDraft, provider: e.target.value }) }} />
|
|
3800
|
+
</div>
|
|
3801
|
+
<div className={css.field} style={{ flex: 1.4 }}>
|
|
3802
|
+
<label className={css.fieldLabel}>{tt('settings.model')}</label>
|
|
3803
|
+
<select
|
|
3804
|
+
className={css.input}
|
|
3805
|
+
value={modelCustomMode ? '__custom__' : configDraft.model}
|
|
3806
|
+
onChange={e => {
|
|
3807
|
+
const v = e.target.value
|
|
3808
|
+
if (v === '__custom__') {
|
|
3809
|
+
setModelCustomMode(true)
|
|
3810
|
+
} else {
|
|
3811
|
+
setModelCustomMode(false)
|
|
3812
|
+
setConfigDraft({ ...configDraft, model: v })
|
|
3813
|
+
}
|
|
3814
|
+
}}
|
|
3815
|
+
>
|
|
3816
|
+
{MODEL_PRESETS.map(m => <option key={m} value={m}>{m}</option>)}
|
|
3817
|
+
<option value="__custom__">{tt('settings.modelCustom')}</option>
|
|
3818
|
+
</select>
|
|
3819
|
+
{modelCustomMode && (
|
|
3820
|
+
<input
|
|
3821
|
+
className={css.input}
|
|
3822
|
+
style={{ marginTop: 6 }}
|
|
3823
|
+
value={configDraft.model}
|
|
3824
|
+
placeholder={tt('settings.modelCustomPlaceholder')}
|
|
3825
|
+
onChange={e => { setConfigDraft({ ...configDraft, model: e.target.value }) }}
|
|
3826
|
+
/>
|
|
3827
|
+
)}
|
|
3828
|
+
</div>
|
|
3795
3829
|
</div>
|
|
3796
|
-
<div className={css.
|
|
3797
|
-
<
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3830
|
+
<div className={css.row} style={{ alignItems: 'flex-start' }}>
|
|
3831
|
+
<div className={css.field} style={{ flex: 1 }}>
|
|
3832
|
+
<label className={css.fieldLabel}>{tt('settings.reasoningEffort')}</label>
|
|
3833
|
+
<select
|
|
3834
|
+
className={css.input}
|
|
3835
|
+
value={configDraft.reasoningEffort ?? 'off'}
|
|
3836
|
+
onChange={e => { setConfigDraft({ ...configDraft, reasoningEffort: e.target.value as NovelConfig['reasoningEffort'] }) }}
|
|
3837
|
+
>
|
|
3838
|
+
{REASONING_OPTIONS.map(v => <option key={v} value={v}>{tt(`settings.reasoning.${v}`)}</option>)}
|
|
3839
|
+
</select>
|
|
3840
|
+
<span className={css.meta}>{tt('settings.reasoningHint')}</span>
|
|
3841
|
+
</div>
|
|
3842
|
+
<div className={css.field} style={{ flex: 1 }}>
|
|
3843
|
+
<label className={css.fieldLabel}>{tt('settings.analysisReasoning')}</label>
|
|
3844
|
+
<select
|
|
3845
|
+
className={css.input}
|
|
3846
|
+
value={configDraft.analysisReasoning ?? 'low'}
|
|
3847
|
+
onChange={e => { setConfigDraft({ ...configDraft, analysisReasoning: e.target.value as NovelConfig['analysisReasoning'] }) }}
|
|
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.analysisReasoningHint')}</span>
|
|
3852
|
+
</div>
|
|
3807
3853
|
</div>
|
|
3808
3854
|
</div>
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3855
|
+
|
|
3856
|
+
{/* ③ 生图(豆包) */}
|
|
3857
|
+
<div className={`${css.card} ${css.settingsCard}`}>
|
|
3858
|
+
<span className={css.cardTitle}>🎨 生图(豆包)</span>
|
|
3859
|
+
<div className={css.row}>
|
|
3860
|
+
<div className={css.field} style={{ flex: 1 }}>
|
|
3861
|
+
<label className={css.fieldLabel}>豆包 API Key(生图)</label>
|
|
3862
|
+
<input className={css.input} type="password" placeholder="ark-..." value={configDraft.imageApiKey ?? ''} onChange={e => { setConfigDraft({ ...configDraft, imageApiKey: e.target.value }) }} />
|
|
3863
|
+
</div>
|
|
3864
|
+
<div className={css.field} style={{ flex: 1 }}>
|
|
3865
|
+
<label className={css.fieldLabel}>豆包生图模型</label>
|
|
3866
|
+
<input className={css.input} placeholder="doubao-seedream-5-0-pro-260628" value={configDraft.imageApiModel ?? ''} onChange={e => { setConfigDraft({ ...configDraft, imageApiModel: e.target.value }) }} />
|
|
3867
|
+
</div>
|
|
3868
|
+
<div className={css.field} style={{ flex: 1 }}>
|
|
3869
|
+
<label className={css.fieldLabel}>启用豆包生图</label>
|
|
3870
|
+
<select
|
|
3871
|
+
className={css.input}
|
|
3872
|
+
value={configDraft.imageApiEnabled ? '1' : '0'}
|
|
3873
|
+
onChange={e => { setConfigDraft({ ...configDraft, imageApiEnabled: e.target.value === '1' }) }}
|
|
3874
|
+
>
|
|
3875
|
+
<option value="0">关(默认)</option>
|
|
3876
|
+
<option value="1">开</option>
|
|
3877
|
+
</select>
|
|
3878
|
+
<span className={css.meta}>开启后角色详情显示「豆包生成」参考图按钮</span>
|
|
3879
|
+
</div>
|
|
3817
3880
|
</div>
|
|
3818
3881
|
</div>
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
)
|
|
3834
|
-
|
|
3835
|
-
|
|
3882
|
+
|
|
3883
|
+
{/* ④ 写作与审稿参数 */}
|
|
3884
|
+
<div className={`${css.card} ${css.settingsCard}`}>
|
|
3885
|
+
<span className={css.cardTitle}>✍️ 写作与审稿参数</span>
|
|
3886
|
+
<div className={css.row}>
|
|
3887
|
+
<div className={css.field} style={{ flex: 1 }}>
|
|
3888
|
+
<label className={css.fieldLabel}>{tt('settings.chapterChars')}</label>
|
|
3889
|
+
<input className={css.input} type="number" min={1000} max={20000} value={configDraft.chapterChars} onChange={e => { setConfigDraft({ ...configDraft, chapterChars: Number(e.target.value) }) }} />
|
|
3890
|
+
</div>
|
|
3891
|
+
<div className={css.field} style={{ flex: 1 }}>
|
|
3892
|
+
<label className={css.fieldLabel}>{tt('settings.maxTokens')}</label>
|
|
3893
|
+
<input className={css.input} type="number" min={2000} max={64000} value={configDraft.maxTokens} onChange={e => { setConfigDraft({ ...configDraft, maxTokens: Number(e.target.value) }) }} />
|
|
3894
|
+
</div>
|
|
3895
|
+
<div className={css.field} style={{ flex: 1 }}>
|
|
3896
|
+
<label className={css.fieldLabel}>{tt('settings.editorFontSize')}</label>
|
|
3897
|
+
<select
|
|
3898
|
+
className={css.input}
|
|
3899
|
+
value={editorFontSize}
|
|
3900
|
+
onChange={e => { changeEditorFontSize(Number(e.target.value)) }}
|
|
3901
|
+
>
|
|
3902
|
+
{[12, 13, 14, 15, 16, 18, 20, 22, 24].map(v => (
|
|
3903
|
+
<option key={v} value={v}>{v}px</option>
|
|
3904
|
+
))}
|
|
3905
|
+
</select>
|
|
3906
|
+
<span className={css.meta}>{tt('settings.editorFontSizeHint')}</span>
|
|
3907
|
+
</div>
|
|
3836
3908
|
</div>
|
|
3837
|
-
<div className={css.
|
|
3838
|
-
<
|
|
3839
|
-
|
|
3840
|
-
className={css.input}
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
>
|
|
3844
|
-
<
|
|
3845
|
-
<
|
|
3846
|
-
|
|
3909
|
+
<div className={css.row}>
|
|
3910
|
+
<div className={css.field} style={{ flex: 1 }}>
|
|
3911
|
+
<label className={css.fieldLabel}>{tt('settings.reviewPassScore')}</label>
|
|
3912
|
+
<input className={css.input} type="number" min={0} max={100} value={configDraft.reviewPassScore} onChange={e => { setConfigDraft({ ...configDraft, reviewPassScore: Number(e.target.value) }) }} />
|
|
3913
|
+
<span className={css.meta}>通过判定:综合评分 ≥ 此分数(默认 70),或无 high 级问题且评分 ≥ 60;建议设 60-80。</span>
|
|
3914
|
+
</div>
|
|
3915
|
+
<div className={css.field} style={{ flex: 1 }}>
|
|
3916
|
+
<label className={css.fieldLabel}>{tt('settings.autoReview')}</label>
|
|
3917
|
+
<select
|
|
3918
|
+
className={css.input}
|
|
3919
|
+
value={configDraft.autoReview ? '1' : '0'}
|
|
3920
|
+
onChange={e => { setConfigDraft({ ...configDraft, autoReview: e.target.value === '1' }) }}
|
|
3921
|
+
>
|
|
3922
|
+
<option value="1">✓ 是</option>
|
|
3923
|
+
<option value="0">✗ 否</option>
|
|
3924
|
+
</select>
|
|
3925
|
+
</div>
|
|
3926
|
+
<div className={css.field} style={{ flex: 1 }}>
|
|
3927
|
+
<label className={css.fieldLabel}>{tt('settings.autoAuthorReview')}</label>
|
|
3928
|
+
<select
|
|
3929
|
+
className={css.input}
|
|
3930
|
+
value={configDraft.autoAuthorReview ? '1' : '0'}
|
|
3931
|
+
onChange={e => { setConfigDraft({ ...configDraft, autoAuthorReview: e.target.value === '1' }) }}
|
|
3932
|
+
>
|
|
3933
|
+
<option value="1">✓ 是</option>
|
|
3934
|
+
<option value="0">✗ 否</option>
|
|
3935
|
+
</select>
|
|
3936
|
+
<span className={css.meta}>{tt('settings.autoAuthorReviewHint')}</span>
|
|
3937
|
+
</div>
|
|
3938
|
+
<div className={css.field} style={{ flex: 1 }}>
|
|
3939
|
+
<label className={css.fieldLabel}>{tt('settings.autoReviewAfterRevise')}</label>
|
|
3940
|
+
<select
|
|
3941
|
+
className={css.input}
|
|
3942
|
+
value={configDraft.autoReviewAfterRevise ? '1' : '0'}
|
|
3943
|
+
onChange={e => { setConfigDraft({ ...configDraft, autoReviewAfterRevise: e.target.value === '1' }) }}
|
|
3944
|
+
>
|
|
3945
|
+
<option value="1">✓ 是</option>
|
|
3946
|
+
<option value="0">✗ 否</option>
|
|
3947
|
+
</select>
|
|
3948
|
+
<span className={css.meta}>{tt('settings.autoReviewAfterReviseHint')}</span>
|
|
3949
|
+
</div>
|
|
3847
3950
|
</div>
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
</select>
|
|
3858
|
-
<span className={css.meta}>{tt('settings.autoAuthorReviewHint')}</span>
|
|
3951
|
+
</div>
|
|
3952
|
+
|
|
3953
|
+
{/* ⑤ 保存与导出 */}
|
|
3954
|
+
<div className={`${css.card} ${css.settingsCard}`}>
|
|
3955
|
+
<div className={css.row}>
|
|
3956
|
+
<button type="button" className={`${css.button} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void handleSaveConfig() }}>
|
|
3957
|
+
{tt('settings.save')}
|
|
3958
|
+
</button>
|
|
3959
|
+
<span className={css.meta}>当前:{config?.provider} / {config?.model} · {config?.outputDir}</span>
|
|
3859
3960
|
</div>
|
|
3860
|
-
<div className={css.
|
|
3861
|
-
<
|
|
3862
|
-
<
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
<option value="0">✗ 否</option>
|
|
3869
|
-
</select>
|
|
3870
|
-
<span className={css.meta}>{tt('settings.autoReviewAfterReviseHint')}</span>
|
|
3961
|
+
<div className={css.row}>
|
|
3962
|
+
<span className={css.cardTitle}>{tt('settings.export')}</span>
|
|
3963
|
+
<button type="button" className={css.button} disabled={busy || chapters.length === 0} onClick={() => { void handleExport('txt') }}>
|
|
3964
|
+
{tt('settings.exportTxt')}
|
|
3965
|
+
</button>
|
|
3966
|
+
<button type="button" className={css.button} disabled={busy || chapters.length === 0} onClick={() => { void handleExport('md') }}>
|
|
3967
|
+
{tt('settings.exportMd')}
|
|
3968
|
+
</button>
|
|
3871
3969
|
</div>
|
|
3872
3970
|
</div>
|
|
3873
|
-
|
|
3874
|
-
<button type="button" className={`${css.button} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void handleSaveConfig() }}>
|
|
3875
|
-
{tt('settings.save')}
|
|
3876
|
-
</button>
|
|
3877
|
-
<button type="button" className={css.button} onClick={() => { void api.openFolder() }}>
|
|
3878
|
-
{tt('settings.openFolder')}
|
|
3879
|
-
</button>
|
|
3880
|
-
<span className={css.meta}>当前:{config?.provider} / {config?.model} · {config?.outputDir}</span>
|
|
3881
|
-
</div>
|
|
3882
|
-
<div className={css.row}>
|
|
3883
|
-
<span className={css.cardTitle}>{tt('settings.export')}</span>
|
|
3884
|
-
<button type="button" className={css.button} disabled={busy || chapters.length === 0} onClick={() => { void handleExport('txt') }}>
|
|
3885
|
-
{tt('settings.exportTxt')}
|
|
3886
|
-
</button>
|
|
3887
|
-
<button type="button" className={css.button} disabled={busy || chapters.length === 0} onClick={() => { void handleExport('md') }}>
|
|
3888
|
-
{tt('settings.exportMd')}
|
|
3889
|
-
</button>
|
|
3890
|
-
</div>
|
|
3891
|
-
</div>
|
|
3971
|
+
</>
|
|
3892
3972
|
)}
|
|
3893
3973
|
|
|
3894
3974
|
{activeTab === 'settings' && (
|
|
3895
|
-
<div className={css.card}>
|
|
3975
|
+
<div className={`${css.card} ${css.settingsCard}`}>
|
|
3896
3976
|
<span className={css.cardTitle}>{tt('settings.theme')}</span>
|
|
3897
3977
|
<div className={css.row} style={{ gap: 8, flexWrap: 'wrap' }}>
|
|
3898
3978
|
<button
|
|
@@ -4310,6 +4390,14 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
4310
4390
|
<RunPanel api={api} totalChapters={chapters.length} />
|
|
4311
4391
|
)}
|
|
4312
4392
|
|
|
4393
|
+
{activeTab === 'styleLib' && (
|
|
4394
|
+
<StyleGallery />
|
|
4395
|
+
)}
|
|
4396
|
+
|
|
4397
|
+
{activeTab === 'manhua' && (
|
|
4398
|
+
<MangaWorkspace api={api} project={project} chapters={chapters} onProjectChanged={() => refresh(false)} />
|
|
4399
|
+
)}
|
|
4400
|
+
|
|
4313
4401
|
{activeTab === 'breakdown' && (
|
|
4314
4402
|
<div className={css.card}>
|
|
4315
4403
|
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
@@ -4619,7 +4707,8 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
4619
4707
|
setBusyLabel(`精修「${detailRole.name}」提示词…`)
|
|
4620
4708
|
setError('')
|
|
4621
4709
|
try {
|
|
4622
|
-
|
|
4710
|
+
const activePlan = (project?.mangaPlans ?? []).find(pl => pl.active)
|
|
4711
|
+
await api.roles({ op: 'promptKit', name: detailRole.name, styleId: activePlan?.styleId, filterId: activePlan?.filterId })
|
|
4623
4712
|
await refresh(false)
|
|
4624
4713
|
pushProgress(`已生成「${detailRole.name}」四类精修提示词`, 'done')
|
|
4625
4714
|
} catch (err) {
|