@waterwx/dsh-novel-forge 1.1.1 → 1.1.2
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 +10 -0
- package/lib/client.js +2019 -1842
- package/lib/client.js.map +1 -1
- package/lib/types/client/locales.d.ts +2 -0
- package/package.json +1 -1
- package/src/client/locales.ts +4 -0
- package/src/client/panel/NovelPanel.tsx +102 -17
- package/src/client/panel/panel.module.css +5 -5
|
@@ -107,6 +107,8 @@ export declare const zh: {
|
|
|
107
107
|
readonly 'settings.chapterChars': "每章目标字数";
|
|
108
108
|
readonly 'settings.maxTokens': "单章最大输出 tokens";
|
|
109
109
|
readonly 'settings.reviewPassScore': "审稿通过分数(0-100)";
|
|
110
|
+
readonly 'settings.editorFontSize': "编辑器字号(仅编辑页显示)";
|
|
111
|
+
readonly 'settings.editorFontSizeHint': "存于浏览器本地,不影响其他设备";
|
|
110
112
|
readonly 'settings.autoReview': "生成后自动审稿";
|
|
111
113
|
readonly 'settings.save': "保存设置";
|
|
112
114
|
readonly 'settings.saved': "设置已保存";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waterwx/dsh-novel-forge",
|
|
3
3
|
"description": "AI 编译小说工作台 for the dsh web GUI: import a novel outline (docx/text), plan chapters with LLM, generate chapter by chapter (3000-4000 chars each), and save every chapter as Markdown into your chosen output folder.",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
package/src/client/locales.ts
CHANGED
|
@@ -108,6 +108,8 @@ export const zh = {
|
|
|
108
108
|
'settings.chapterChars': '每章目标字数',
|
|
109
109
|
'settings.maxTokens': '单章最大输出 tokens',
|
|
110
110
|
'settings.reviewPassScore': '审稿通过分数(0-100)',
|
|
111
|
+
'settings.editorFontSize': '编辑器字号(仅编辑页显示)',
|
|
112
|
+
'settings.editorFontSizeHint': '存于浏览器本地,不影响其他设备',
|
|
111
113
|
'settings.autoReview': '生成后自动审稿',
|
|
112
114
|
'settings.save': '保存设置',
|
|
113
115
|
'settings.saved': '设置已保存',
|
|
@@ -240,6 +242,8 @@ export const en: Record<string, string> = {
|
|
|
240
242
|
'settings.chapterChars': 'Chars per chapter',
|
|
241
243
|
'settings.maxTokens': 'Max output tokens',
|
|
242
244
|
'settings.reviewPassScore': 'Review pass score (0-100)',
|
|
245
|
+
'settings.editorFontSize': 'Editor font size (workspace only)',
|
|
246
|
+
'settings.editorFontSizeHint': 'Stored in browser localStorage; display-only',
|
|
243
247
|
'settings.autoReview': 'Auto-review after writing',
|
|
244
248
|
'settings.save': 'Save settings',
|
|
245
249
|
'settings.saved': 'Settings saved',
|
|
@@ -219,7 +219,7 @@ function renderBeats(beats: string): ReactElement {
|
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
/** The diff list of a draft-vs-original comparison (scrollable). */
|
|
222
|
-
function DiffList({ original, draft }: { original: string; draft: string }): ReactElement { const rows = useMemo(() => paragraphDiff(original, draft), [original, draft])
|
|
222
|
+
function DiffList({ original, draft, fontSize }: { original: string; draft: string; fontSize?: number }): ReactElement { const rows = useMemo(() => paragraphDiff(original, draft), [original, draft])
|
|
223
223
|
const changed = rows.filter(r => r.kind === 'change').length
|
|
224
224
|
const added = rows.filter(r => r.kind === 'add').length
|
|
225
225
|
const removed = rows.filter(r => r.kind === 'del').length
|
|
@@ -242,7 +242,7 @@ function DiffList({ original, draft }: { original: string; draft: string }): Rea
|
|
|
242
242
|
只看改动
|
|
243
243
|
</label>
|
|
244
244
|
</div>
|
|
245
|
-
<div className={css.diffList}>
|
|
245
|
+
<div className={css.diffList} style={fontSize !== undefined ? { fontSize } : undefined}>
|
|
246
246
|
{shown.map((row, idx) => {
|
|
247
247
|
if (row.kind === 'same') {
|
|
248
248
|
return (
|
|
@@ -333,6 +333,20 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
333
333
|
const wsEditorRef = useRef<HTMLTextAreaElement | null>(null)
|
|
334
334
|
/** 工作区:手动编辑后的 AI 审查结果(不落盘)。 */
|
|
335
335
|
const [wsCheckReport, setWsCheckReport] = useState<ReviewReport | null>(null)
|
|
336
|
+
/** 手动审查结果中作者勾选要修复的问题(issue 下标)。 */
|
|
337
|
+
const [wsChecked, setWsChecked] = useState<number[]>([])
|
|
338
|
+
/** 编辑页字号(localStorage 记忆,仅影响显示)。 */
|
|
339
|
+
const [editorFontSize, setEditorFontSize] = useState<number>(() => {
|
|
340
|
+
try {
|
|
341
|
+
const v = Number(window.localStorage.getItem('dsh-novel-forge.editor.fontSize'))
|
|
342
|
+
return v >= 12 && v <= 24 ? v : 14
|
|
343
|
+
} catch { return 14 }
|
|
344
|
+
})
|
|
345
|
+
const changeEditorFontSize = (next: number): void => {
|
|
346
|
+
const v = Math.min(24, Math.max(12, next))
|
|
347
|
+
setEditorFontSize(v)
|
|
348
|
+
try { window.localStorage.setItem('dsh-novel-forge.editor.fontSize', String(v)) } catch { /* ignore */ }
|
|
349
|
+
}
|
|
336
350
|
/** 有未采纳草稿的章节号(refresh 后检测到遗留草稿时提示)。 */
|
|
337
351
|
const [draftNo, setDraftNo] = useState<number | null>(null)
|
|
338
352
|
/** 大纲页「更新大纲」编辑区是否展开。 */
|
|
@@ -776,6 +790,10 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
776
790
|
try {
|
|
777
791
|
const result = await api.chapterCheck(workspace.no, workspace.original)
|
|
778
792
|
setWsCheckReport(result.report)
|
|
793
|
+
// 默认勾选 high 问题(无 high 则勾选全部 medium),作者可自行增删。
|
|
794
|
+
const highIdx = result.report.issues.map((it, i) => ({ it, i })).filter(x => x.it.severity === 'high').map(x => x.i)
|
|
795
|
+
const mediumIdx = result.report.issues.map((it, i) => ({ it, i })).filter(x => x.it.severity === 'medium').map(x => x.i)
|
|
796
|
+
setWsChecked(highIdx.length > 0 ? highIdx : mediumIdx)
|
|
779
797
|
pushProgress(`审查完成:${result.report.score} 分 — ${result.report.verdict}`, result.report.passed ? 'done' : 'error')
|
|
780
798
|
} catch (err) {
|
|
781
799
|
setError((err as Error).message)
|
|
@@ -801,6 +819,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
801
819
|
setWorkspace(null)
|
|
802
820
|
setDraftNo(null)
|
|
803
821
|
setWsCheckReport(null)
|
|
822
|
+
setWsChecked([])
|
|
804
823
|
if (report !== undefined) {
|
|
805
824
|
pushProgress(`已保存并审稿:${report.score} 分 — ${report.verdict}(${report.passed ? '通过' : '未通过'})`, report.passed ? 'done' : 'error')
|
|
806
825
|
} else {
|
|
@@ -947,7 +966,9 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
947
966
|
draft: chapter.pendingDraft !== undefined && chapter.pendingDraft !== '' ? chapter.pendingDraft : null,
|
|
948
967
|
})
|
|
949
968
|
setWsSelected('')
|
|
950
|
-
setWsShowDiff(
|
|
969
|
+
setWsShowDiff(true)
|
|
970
|
+
setWsCheckReport(null)
|
|
971
|
+
setWsChecked([])
|
|
951
972
|
} catch { /* best-effort */ }
|
|
952
973
|
}, [api])
|
|
953
974
|
|
|
@@ -980,14 +1001,15 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
980
1001
|
}
|
|
981
1002
|
|
|
982
1003
|
/** 工作区:按指令修订(whole=true 整章,false 仅修订选中片段)。 */
|
|
983
|
-
const handleWsRewrite = async (whole: boolean): Promise<void> => {
|
|
1004
|
+
const handleWsRewrite = async (whole: boolean, overrideInstruction?: string): Promise<void> => {
|
|
984
1005
|
if (workspace === null) return
|
|
985
1006
|
const target = whole ? '' : wsSelected
|
|
1007
|
+
const instruction = overrideInstruction ?? workspace.instruction
|
|
986
1008
|
setBusy(true)
|
|
987
1009
|
setBusyLabel(`${tt('plan.rewrite')} 第${workspace.no}章`)
|
|
988
1010
|
setError('')
|
|
989
1011
|
try {
|
|
990
|
-
await api.rewrite(workspace.no,
|
|
1012
|
+
await api.rewrite(workspace.no, instruction, target, frame => { applyJobFrame(frame, n => tt('progress.rewriting', { no: n })) })
|
|
991
1013
|
} catch (err) {
|
|
992
1014
|
setError((err as Error).message)
|
|
993
1015
|
pushProgress(`第 ${workspace.no} 章修订失败:${(err as Error).message}`, 'error')
|
|
@@ -998,6 +1020,19 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
998
1020
|
}
|
|
999
1021
|
}
|
|
1000
1022
|
|
|
1023
|
+
/** 工作区:按审查报告中勾选的问题一键修订(预填指令 + 立即整章修订到草稿)。 */
|
|
1024
|
+
const handleWsReviseByReport = async (): Promise<void> => {
|
|
1025
|
+
if (workspace === null || wsCheckReport === null) return
|
|
1026
|
+
const picked = wsChecked
|
|
1027
|
+
.map(i => wsCheckReport.issues[i])
|
|
1028
|
+
.filter((it): it is ReviewReport['issues'][number] => it !== undefined)
|
|
1029
|
+
.slice(0, 5)
|
|
1030
|
+
if (picked.length === 0) return
|
|
1031
|
+
const instruction = '按审稿意见修订(优先处理):\n' + picked.map(i => `[${i.severity}] ${i.item} → ${i.suggestion}`).join('\n')
|
|
1032
|
+
setWorkspace({ ...workspace, instruction })
|
|
1033
|
+
await handleWsRewrite(true, instruction)
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1001
1036
|
/** 采纳草稿:覆盖正文文件并刷新。 */
|
|
1002
1037
|
const handleDraftApply = async (no: number): Promise<void> => {
|
|
1003
1038
|
setBusy(true)
|
|
@@ -1665,12 +1700,21 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
1665
1700
|
</div>
|
|
1666
1701
|
)}
|
|
1667
1702
|
|
|
1668
|
-
{/*
|
|
1703
|
+
{/* 章节编辑页(独占整页:点「编辑」进入,返回后回到原页面) */}
|
|
1669
1704
|
{workspace !== null && (
|
|
1670
1705
|
<div className={css.card} style={{ borderColor: 'var(--nf-accent)' }}>
|
|
1671
|
-
<div className={css.busyRow}>
|
|
1672
|
-
<
|
|
1673
|
-
|
|
1706
|
+
<div className={css.busyRow} style={{ flexWrap: 'wrap', gap: 8 }}>
|
|
1707
|
+
<button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setWorkspace(null) }} title="返回章节列表(草稿不丢失)">
|
|
1708
|
+
← 返回
|
|
1709
|
+
</button>
|
|
1710
|
+
<span className={css.cardTitle}>第 {workspace.no} 章《{workspace.title}》</span>
|
|
1711
|
+
<span className={css.meta}>{workspace.original.length} 字</span>
|
|
1712
|
+
<span style={{ display: 'flex', gap: 4, alignItems: 'center', marginLeft: 'auto' }}>
|
|
1713
|
+
<button type="button" className={css.iconButton} title="减小字号" aria-label="减小字号" onClick={() => { changeEditorFontSize(editorFontSize - 1) }}>A−</button>
|
|
1714
|
+
<span className={css.meta}>{editorFontSize}px</span>
|
|
1715
|
+
<button type="button" className={css.iconButton} title="增大字号" aria-label="增大字号" onClick={() => { changeEditorFontSize(editorFontSize + 1) }}>A+</button>
|
|
1716
|
+
<button type="button" className={css.iconButton} title="关闭工作区" aria-label="关闭工作区" onClick={() => { setWorkspace(null) }}>×</button>
|
|
1717
|
+
</span>
|
|
1674
1718
|
</div>
|
|
1675
1719
|
<div className={css.wsColumns}>
|
|
1676
1720
|
<div className={css.wsColumn}>
|
|
@@ -1680,6 +1724,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
1680
1724
|
<textarea
|
|
1681
1725
|
ref={wsEditorRef}
|
|
1682
1726
|
className={`${css.textarea} ${css.wsEditor}`}
|
|
1727
|
+
style={{ fontSize: editorFontSize }}
|
|
1683
1728
|
value={workspace.original}
|
|
1684
1729
|
onChange={e => { setWorkspace({ ...workspace, original: e.target.value }) }}
|
|
1685
1730
|
onMouseUp={captureWsSelection}
|
|
@@ -1706,7 +1751,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
1706
1751
|
<div className={css.meta}>未选中内容时仅支持整章润色/修订。</div>
|
|
1707
1752
|
)}
|
|
1708
1753
|
<div className={css.row} style={{ flexWrap: 'wrap' }}>
|
|
1709
|
-
{workspace.instruction.
|
|
1754
|
+
{workspace.instruction.includes('按审稿意见修订') && (
|
|
1710
1755
|
<button type="button" className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void handleWsRewrite(true) }} title="AI 按已预填的审稿意见自动修订整章(无需自己找问题)">
|
|
1711
1756
|
🔧 按意见修订
|
|
1712
1757
|
</button>
|
|
@@ -1738,16 +1783,40 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
1738
1783
|
</div>
|
|
1739
1784
|
<div className={css.meta}><b>总评:</b>{wsCheckReport.verdict}</div>
|
|
1740
1785
|
{wsCheckReport.issues.length > 0 && (
|
|
1741
|
-
<ul style={{ margin: 0, paddingLeft: 16,
|
|
1786
|
+
<ul style={{ margin: 0, paddingLeft: 16, display: 'flex', flexDirection: 'column', gap: 3, fontSize: editorFontSize - 1, maxHeight: '45vh', overflowY: 'auto' }}>
|
|
1742
1787
|
{wsCheckReport.issues.map((issue, i) => (
|
|
1743
|
-
<li key={i} style={{ color: severityColor(issue.severity) }}>
|
|
1744
|
-
|
|
1745
|
-
|
|
1788
|
+
<li key={i} style={{ color: severityColor(issue.severity), display: 'flex', gap: 6, alignItems: 'flex-start' }}>
|
|
1789
|
+
<input
|
|
1790
|
+
type="checkbox"
|
|
1791
|
+
style={{ marginTop: 1 }}
|
|
1792
|
+
checked={wsChecked.includes(i)}
|
|
1793
|
+
onChange={e => {
|
|
1794
|
+
setWsChecked(prev => e.target.checked ? [...prev, i] : prev.filter(x => x !== i))
|
|
1795
|
+
}}
|
|
1796
|
+
title="勾选后可由「修复所选问题」一起修订"
|
|
1797
|
+
/>
|
|
1798
|
+
<span>
|
|
1799
|
+
[{issue.severity}] {issue.item}
|
|
1800
|
+
{issue.suggestion !== '' && <span style={{ color: 'var(--nf-text-2)' }}> → {issue.suggestion}</span>}
|
|
1801
|
+
</span>
|
|
1746
1802
|
</li>
|
|
1747
1803
|
))}
|
|
1748
1804
|
</ul>
|
|
1749
1805
|
)}
|
|
1750
|
-
<span className={css.meta}
|
|
1806
|
+
<span className={css.meta}>审查只读不落盘;勾选要修的问题,点下方按钮一键修订;改完点「💾 保存为正文」才写入文件(原稿自动备份 .bak)。</span>
|
|
1807
|
+
{wsCheckReport.issues.length > 0 && wsChecked.length > 0 && (
|
|
1808
|
+
<div style={{ marginTop: 8 }}>
|
|
1809
|
+
<button
|
|
1810
|
+
type="button"
|
|
1811
|
+
className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`}
|
|
1812
|
+
disabled={busy}
|
|
1813
|
+
onClick={() => { void handleWsReviseByReport() }}
|
|
1814
|
+
title="自动按勾选的问题修订整章,产出草稿预览(不落盘),对比后应用或放弃"
|
|
1815
|
+
>
|
|
1816
|
+
🔧 修复所选问题({wsChecked.length})
|
|
1817
|
+
</button>
|
|
1818
|
+
</div>
|
|
1819
|
+
)}
|
|
1751
1820
|
</div>
|
|
1752
1821
|
)}
|
|
1753
1822
|
{workspace.draft !== null && (
|
|
@@ -1767,8 +1836,8 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
1767
1836
|
</span>
|
|
1768
1837
|
</div>
|
|
1769
1838
|
{wsShowDiff
|
|
1770
|
-
? <DiffList original={workspace.original} draft={workspace.draft} />
|
|
1771
|
-
: <pre className={css.wsPreviewText}>{workspace.draft}</pre>}
|
|
1839
|
+
? <DiffList original={workspace.original} draft={workspace.draft} fontSize={editorFontSize} />
|
|
1840
|
+
: <pre className={css.wsPreviewText} style={{ fontSize: editorFontSize }}>{workspace.draft}</pre>}
|
|
1772
1841
|
</div>
|
|
1773
1842
|
)}
|
|
1774
1843
|
</div>
|
|
@@ -1776,6 +1845,8 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
1776
1845
|
</div>
|
|
1777
1846
|
)}
|
|
1778
1847
|
|
|
1848
|
+
{/* 编辑页打开时独占整页:隐藏其余所有页面内容 */}
|
|
1849
|
+
{workspace === null && (<>
|
|
1779
1850
|
{activeTab === 'workflow' && (
|
|
1780
1851
|
<>
|
|
1781
1852
|
{/* ⭐ 主行动大卡片 */}
|
|
@@ -2588,6 +2659,19 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
2588
2659
|
<label className={css.fieldLabel}>{tt('settings.reviewPassScore')}</label>
|
|
2589
2660
|
<input className={css.input} type="number" min={0} max={100} value={configDraft.reviewPassScore} onChange={e => { setConfigDraft({ ...configDraft, reviewPassScore: Number(e.target.value) }) }} />
|
|
2590
2661
|
</div>
|
|
2662
|
+
<div className={css.field} style={{ flex: 1 }}>
|
|
2663
|
+
<label className={css.fieldLabel}>{tt('settings.editorFontSize')}</label>
|
|
2664
|
+
<select
|
|
2665
|
+
className={css.input}
|
|
2666
|
+
value={editorFontSize}
|
|
2667
|
+
onChange={e => { changeEditorFontSize(Number(e.target.value)) }}
|
|
2668
|
+
>
|
|
2669
|
+
{[12, 13, 14, 15, 16, 18, 20, 22, 24].map(v => (
|
|
2670
|
+
<option key={v} value={v}>{v}px</option>
|
|
2671
|
+
))}
|
|
2672
|
+
</select>
|
|
2673
|
+
<span className={css.meta}>{tt('settings.editorFontSizeHint')}</span>
|
|
2674
|
+
</div>
|
|
2591
2675
|
<div className={css.field} style={{ flex: 1 }}>
|
|
2592
2676
|
<label className={css.fieldLabel}>{tt('settings.autoReview')}</label>
|
|
2593
2677
|
<select
|
|
@@ -2639,6 +2723,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
2639
2723
|
)}
|
|
2640
2724
|
</div>
|
|
2641
2725
|
)}
|
|
2726
|
+
</>)}
|
|
2642
2727
|
</div>
|
|
2643
2728
|
</div>
|
|
2644
2729
|
</>
|
|
@@ -1070,7 +1070,7 @@ html[data-dsh-novelforge-active]:not([data-dsh-taskboard-active]):not([data-dsh-
|
|
|
1070
1070
|
}
|
|
1071
1071
|
|
|
1072
1072
|
.diffList {
|
|
1073
|
-
max-height:
|
|
1073
|
+
max-height: 70vh;
|
|
1074
1074
|
overflow-y: auto;
|
|
1075
1075
|
border: 1px solid var(--nf-border);
|
|
1076
1076
|
border-radius: 8px;
|
|
@@ -1193,10 +1193,10 @@ html[data-dsh-novelforge-active]:not([data-dsh-taskboard-active]):not([data-dsh-
|
|
|
1193
1193
|
color: var(--nf-success);
|
|
1194
1194
|
}
|
|
1195
1195
|
|
|
1196
|
-
/* Revision workspace: left original editor + right instruction/preview. */
|
|
1196
|
+
/* Revision workspace: left original editor + right instruction/preview, 1:1. */
|
|
1197
1197
|
.wsColumns {
|
|
1198
1198
|
display: grid;
|
|
1199
|
-
grid-template-columns: minmax(0,
|
|
1199
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
1200
1200
|
gap: 14px;
|
|
1201
1201
|
}
|
|
1202
1202
|
|
|
@@ -1209,7 +1209,7 @@ html[data-dsh-novelforge-active]:not([data-dsh-taskboard-active]):not([data-dsh-
|
|
|
1209
1209
|
|
|
1210
1210
|
.wsEditor {
|
|
1211
1211
|
flex: 1;
|
|
1212
|
-
min-height:
|
|
1212
|
+
min-height: 50vh;
|
|
1213
1213
|
resize: vertical;
|
|
1214
1214
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1215
1215
|
font-size: 13px;
|
|
@@ -1245,7 +1245,7 @@ html[data-dsh-novelforge-active]:not([data-dsh-taskboard-active]):not([data-dsh-
|
|
|
1245
1245
|
|
|
1246
1246
|
.wsPreviewText {
|
|
1247
1247
|
margin: 0;
|
|
1248
|
-
max-height:
|
|
1248
|
+
max-height: 55vh;
|
|
1249
1249
|
overflow: auto;
|
|
1250
1250
|
font-size: 13px;
|
|
1251
1251
|
line-height: 1.7;
|