@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.
@@ -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.1",
4
+ "version": "1.1.2",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -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(false)
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, workspace.instruction, target, frame => { applyJobFrame(frame, n => tt('progress.rewriting', { no: n })) })
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
- <span className={css.cardTitle}>第 {workspace.no} 章《{workspace.title}》润色/修订</span>
1673
- <button type="button" className={css.iconButton} title="关闭工作区" aria-label="关闭工作区" onClick={() => { setWorkspace(null) }}>×</button>
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.startsWith('按审稿意见修订') && (
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, fontSize: 12, display: 'flex', flexDirection: 'column', gap: 3 }}>
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
- [{issue.severity}] {issue.item}
1745
- {issue.suggestion !== '' && <span style={{ color: 'var(--nf-text-2)' }}> → {issue.suggestion}</span>}
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}>审查只读不落盘;改完点「💾 保存为正文」才写入文件(原稿自动备份 .bak)。</span>
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: 460px;
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, 3fr) minmax(0, 2fr);
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: 340px;
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: 320px;
1248
+ max-height: 55vh;
1249
1249
  overflow: auto;
1250
1250
  font-size: 13px;
1251
1251
  line-height: 1.7;