@waterwx/dsh-novel-forge 1.7.0 → 1.7.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/lib/client.js +2537 -1858
- package/lib/client.js.map +1 -1
- package/lib/index.js +657 -45
- package/lib/index.js.map +1 -1
- package/lib/types/client/api.d.ts +2 -0
- package/lib/types/client/locales.d.ts +0 -1
- package/lib/types/client/panel/FlowGuide.d.ts +21 -0
- package/lib/types/client/panel/MangaRoleLibrary.d.ts +18 -0
- package/lib/types/client/panel/MangaWorkspace.d.ts +2 -2
- package/lib/types/client/panel/StoryboardTab.d.ts +5 -1
- package/lib/types/engine.d.ts +15 -8
- package/lib/types/protocol.d.ts +124 -0
- package/package.json +2 -1
- package/src/client/api.ts +5 -0
- package/src/client/locales.ts +0 -1
- package/src/client/panel/AssetsTab.tsx +22 -22
- package/src/client/panel/AssistantTab.tsx +3 -3
- package/src/client/panel/BookshelfBar.tsx +1 -1
- package/src/client/panel/CreateBookView.tsx +10 -10
- package/src/client/panel/FlowGuide.tsx +83 -0
- package/src/client/panel/MangaRoleLibrary.tsx +810 -0
- package/src/client/panel/MangaWorkspace.tsx +359 -297
- package/src/client/panel/NovelPanel.tsx +107 -107
- package/src/client/panel/ReaderView.tsx +4 -4
- package/src/client/panel/RunPanel.tsx +8 -8
- package/src/client/panel/SceneLibrary.tsx +68 -67
- package/src/client/panel/StoryboardTab.tsx +105 -41
- package/src/client/panel/StyleCard.tsx +13 -13
- package/src/client/panel/StyleGallery.tsx +2 -2
- package/src/client/panel/WorldTab.tsx +5 -5
- package/src/client/panel/panel.module.css +861 -510
- package/src/client/panel/views.tsx +24 -24
- package/src/engine.ts +482 -51
- package/src/protocol.ts +125 -0
- package/src/routes.ts +203 -0
- package/src/client/panel/RoleVisualPanel.tsx +0 -485
|
@@ -100,6 +100,8 @@ export declare class NovelApi {
|
|
|
100
100
|
/** 角色库:AI 提炼 / 采纳 / 更新 / 删除。 */
|
|
101
101
|
imageTest(req: import('../protocol.ts').ImageTestRequest): Promise<import('../protocol.ts').ImageTestResponse>;
|
|
102
102
|
roles(req: import('../protocol.ts').RolesRequest): Promise<import('../protocol.ts').RolesResponse>;
|
|
103
|
+
/** 漫剧角色库:从分镜提名 / 建卡 / 更新 / 删除 / 形象锚点 / 精修提示词。 */
|
|
104
|
+
mangaRoles(req: import('../protocol.ts').MangaRolesRequest): Promise<import('../protocol.ts').MangaRolesResponse>;
|
|
103
105
|
/** 场景库:AI 提炼 / 采纳 / 更新 / 删除 / 图集。 */
|
|
104
106
|
scenes(req: import('../protocol.ts').ScenesRequest): Promise<import('../protocol.ts').ScenesResponse>;
|
|
105
107
|
/** 视觉世界观规则:提炼 / 保存。 */
|
|
@@ -153,7 +153,6 @@ export declare const zh: {
|
|
|
153
153
|
readonly 'settings.reasoning.max': "max · 最高思考(质检/推演)";
|
|
154
154
|
readonly 'settings.theme': "主题";
|
|
155
155
|
readonly 'settings.themeLiquid': "iOS 液态玻璃(绿)";
|
|
156
|
-
readonly 'settings.themeClassic': "经典毛玻璃(蓝)";
|
|
157
156
|
readonly 'settings.themeNeumorph': "新拟物(双阴影)";
|
|
158
157
|
readonly 'settings.themeHint': "存于浏览器本地,即时生效";
|
|
159
158
|
readonly 'settings.chapterChars': "每章目标字数";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 漫剧工作台·全流程步骤条(唯一导航,方案X):
|
|
3
|
+
* ①创建方案 → ②视觉规则 → ③剧情骨架 → ④分镜表 → ⑤导入角色 → ⑥角色定妆 → ⑦视频提示词 → ⑧场景库 → ⑨导出使用。
|
|
4
|
+
* 每个步骤对应一个独立页面主体;完成度自动判定(读 project 现有字段)。
|
|
5
|
+
*/
|
|
6
|
+
import type { ProjectState } from '../../protocol.ts';
|
|
7
|
+
/** 步骤 → 页面主体的一一对应目标。 */
|
|
8
|
+
export type FlowTarget = 'plan' | 'rules' | 'skeleton' | 'table' | 'import' | 'makeup' | 'prompts' | 'scenes' | 'export';
|
|
9
|
+
export interface FlowStep {
|
|
10
|
+
no: number;
|
|
11
|
+
label: string;
|
|
12
|
+
hint: string;
|
|
13
|
+
done: boolean;
|
|
14
|
+
target: FlowTarget;
|
|
15
|
+
}
|
|
16
|
+
/** 依据 project 数据计算 9 步完成度(与存储顺序无关,按生产顺序排列)。 */
|
|
17
|
+
export declare function computeFlowSteps(project: ProjectState | null): FlowStep[];
|
|
18
|
+
export declare function FlowGuide({ project, onNavigate, }: {
|
|
19
|
+
project: ProjectState | null;
|
|
20
|
+
onNavigate: (target: FlowTarget) => void;
|
|
21
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { NovelApi } from '../api.ts';
|
|
2
|
+
import type { ImageModelConfig, ProjectState } from '../../protocol.ts';
|
|
3
|
+
export declare function MangaRoleLibrary({ api, project, refresh, styleId, filterId, imageApiEnabled, imageModels, focus, showCards, onProgress, }: {
|
|
4
|
+
api: NovelApi;
|
|
5
|
+
project: ProjectState | null;
|
|
6
|
+
refresh: () => void | Promise<void>;
|
|
7
|
+
styleId?: string;
|
|
8
|
+
filterId?: string;
|
|
9
|
+
/** 是否启用生图(豆包等出定妆图)。 */
|
|
10
|
+
imageApiEnabled?: boolean;
|
|
11
|
+
/** 生图模型库(出定妆图时可选择用哪条)。 */
|
|
12
|
+
imageModels?: ImageModelConfig[];
|
|
13
|
+
/** 步骤页聚焦:import=展开并滚动到导入区;cards=滚动到已建漫剧卡列表。 */
|
|
14
|
+
focus?: 'import' | 'cards';
|
|
15
|
+
/** 是否显示「已建漫剧卡」列表(第⑤步导入页可隐藏,只留导入区)。 */
|
|
16
|
+
showCards?: boolean;
|
|
17
|
+
onProgress?: (text: string, kind?: 'info' | 'done' | 'error') => void;
|
|
18
|
+
}): import("react").JSX.Element;
|
|
@@ -6,9 +6,9 @@ export declare function MangaWorkspace({ api, project, chapters, onProjectChange
|
|
|
6
6
|
chapters: ChapterPlan[];
|
|
7
7
|
/** 方案/资产变更已持久化后触发(刷新项目)。 */
|
|
8
8
|
onProjectChanged?: () => void | Promise<void>;
|
|
9
|
-
/**
|
|
9
|
+
/** 是否启用生图(漫剧卡出定妆图)。 */
|
|
10
10
|
imageApiEnabled?: boolean;
|
|
11
|
-
/**
|
|
11
|
+
/** 生图模型库(出定妆图时可选择模型)。 */
|
|
12
12
|
imageModels?: ImageModelConfig[];
|
|
13
13
|
/** 上报到「AI进度」控制台(漫剧工作台内所有 LLM/方案操作)。 */
|
|
14
14
|
onProgress?: (text: string, kind?: 'info' | 'done' | 'error') => void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { NovelApi } from '../api.ts';
|
|
2
2
|
import type { ChapterPlan, ProjectState } from '../../protocol.ts';
|
|
3
|
-
export declare function StoryboardTab({ api, project, chapters, onProjectChanged, styleId, filterId, onProgress, }: {
|
|
3
|
+
export declare function StoryboardTab({ api, project, chapters, onProjectChanged, styleId, filterId, mode, onGoStep, onProgress, }: {
|
|
4
4
|
api: NovelApi;
|
|
5
5
|
project: ProjectState | null;
|
|
6
6
|
chapters: ChapterPlan[];
|
|
@@ -10,6 +10,10 @@ export declare function StoryboardTab({ api, project, chapters, onProjectChanged
|
|
|
10
10
|
styleId?: string;
|
|
11
11
|
/** 可选滤镜风格 id。 */
|
|
12
12
|
filterId?: string;
|
|
13
|
+
/** 页面模式:auto=按本章实际进度显示;skeleton/table/prompts=固定显示该步骤(前置不足显示提示,不降级)。 */
|
|
14
|
+
mode?: 'auto' | 'skeleton' | 'table' | 'prompts';
|
|
15
|
+
/** 「下一步」按钮回调(1=骨架→2=分镜表→3=提示词),供外层切步骤页。 */
|
|
16
|
+
onGoStep?: (n: 1 | 2 | 3) => void;
|
|
13
17
|
/** 上报到「AI进度」控制台(分镜三步生成)。 */
|
|
14
18
|
onProgress?: (text: string, kind?: 'info' | 'done' | 'error') => void;
|
|
15
19
|
}): import("react").JSX.Element;
|
package/lib/types/engine.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
export declare const COMPLIANCE_REDLINES: ReadonlyArray<string>;
|
|
14
14
|
import type { Context } from '@deepseek-ai/cordis';
|
|
15
|
-
import type { AuditIssue, AuthorReview, BreakdownResponse, ChapterPlan, Foreshadow, NovelConfig, OutlineCandidate, Plotline, PlotlineHealthReport, PlotlinePlan, ProjectState, ReviewReport, RoleRecord, RoleStatusCard, SceneCard, StoryBible, StoryboardSkeleton, StoryboardTable, StoryboardPrompt, Volume, WorldState } from './protocol.ts';
|
|
15
|
+
import type { AuditIssue, AuthorReview, BreakdownResponse, ChapterPlan, Foreshadow, NovelConfig, OutlineCandidate, Plotline, PlotlineHealthReport, PlotlinePlan, ProjectState, ReviewReport, RoleRecord, RoleStatusCard, SceneCard, StoryBible, StoryboardSkeleton, StoryboardTable, StoryboardPrompt, MangaRoleCandidate, Volume, WorldState } from './protocol.ts';
|
|
16
16
|
/** Project state file name inside the output dir. */
|
|
17
17
|
export declare const PROJECT_FILE = "novel-project.json";
|
|
18
18
|
/** Chapter output file name, e.g. 第001章_开篇.md */
|
|
@@ -109,16 +109,20 @@ export interface RoleVisualPrompt {
|
|
|
109
109
|
/** 四类精修提示词(立绘/四视图/表情/细节,一次提炼直接产出)。 */
|
|
110
110
|
promptKit?: RoleRecord['promptKit'];
|
|
111
111
|
}
|
|
112
|
-
/**
|
|
113
|
-
* 为单个角色提炼「动漫形象描述词」:扫描该角色出场的已写章节正文,
|
|
114
|
-
* 截取含外貌描写的段落,交给 LLM 提炼中文描述 + 英文绘图标签。
|
|
115
|
-
*/
|
|
112
|
+
/** 小说角色库:提炼单个角色的形象锚点并写回角色卡。 */
|
|
116
113
|
export declare function extractRoleVisual(ctx: Context, config: NovelConfig, project: ProjectState, outputDir: string, roleName: string, styleId?: string, filterId?: string): Promise<RoleVisualPrompt>;
|
|
114
|
+
/** 漫剧角色卡:提炼形象锚点并写回漫剧卡(status → anchored)。 */
|
|
115
|
+
export declare function extractMangaRoleVisual(ctx: Context, config: NovelConfig, project: ProjectState, outputDir: string, cardId: string, styleId?: string, filterId?: string): Promise<RoleVisualPrompt>;
|
|
116
|
+
/** 小说角色库:角色四类生图提示词精修包(写回角色卡)。 */
|
|
117
|
+
export declare function generateRolePromptKit(ctx: Context, config: NovelConfig, project: ProjectState, roleName: string, styleId?: string, filterId?: string): Promise<RoleRecord['promptKit']>;
|
|
118
|
+
/** 漫剧角色卡:四类生图提示词精修包(写回漫剧卡,status → anchored)。 */
|
|
119
|
+
export declare function generateMangaRolePromptKit(ctx: Context, config: NovelConfig, project: ProjectState, cardId: string, styleId?: string, filterId?: string): Promise<RoleRecord['promptKit']>;
|
|
117
120
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
121
|
+
* 漫剧角色库·提名(两段式):从某章分镜的 characters 提名候选角色名 →
|
|
122
|
+
* 规则过滤(精确名 + 身份/简称匹配,短名单 ≤5)→ LLM 确认(是/否 + 选哪个,不做开放检索)→
|
|
123
|
+
* 返回带漫剧卡建议的候选(未匹配时给出「回小说库补提炼 / 漫剧直接创建」判定)。
|
|
120
124
|
*/
|
|
121
|
-
export declare function
|
|
125
|
+
export declare function nominateMangaRoles(ctx: Context, config: NovelConfig, project: ProjectState, outputDir: string, chapterNo: number): Promise<MangaRoleCandidate[]>;
|
|
122
126
|
/** ✨ 从道藏/红线提炼「视觉世界观规则」:生图/生视频必须遵守的设定纠偏(如"商品=人,禁止常规超市商品")。 */
|
|
123
127
|
export declare function extractVisualRules(ctx: Context, config: NovelConfig, project: ProjectState): Promise<string[]>;
|
|
124
128
|
/**
|
|
@@ -143,7 +147,10 @@ export declare function testImageEndpoint(baseURL: string, apiKey: string, model
|
|
|
143
147
|
message?: string;
|
|
144
148
|
modelFound?: boolean;
|
|
145
149
|
}>;
|
|
150
|
+
/** 小说角色库:生成角色定妆图并写回角色卡(imageUrl)。 */
|
|
146
151
|
export declare function generateRoleReferenceImage(ctx: Context, config: NovelConfig, project: ProjectState, outputDir: string, name: string, style?: string, modelId?: string): Promise<string>;
|
|
152
|
+
/** 漫剧角色卡:生成定妆图并写回漫剧卡(imageUrl)。 */
|
|
153
|
+
export declare function generateMangaRoleReferenceImage(ctx: Context, config: NovelConfig, project: ProjectState, outputDir: string, cardId: string, style?: string, modelId?: string): Promise<string>;
|
|
147
154
|
/** 🩺 剧情健康检查:基于已写章节数/各线状态/编年录,判断是否需要新线及添加时机。 */
|
|
148
155
|
export declare function analyzePlotlineHealth(ctx: Context, config: NovelConfig, project: ProjectState): Promise<PlotlineHealthReport>;
|
|
149
156
|
/** ✨ AI 剧情方案:基于健康检查结果设计下一阶段方向与建议新线。 */
|
package/lib/types/protocol.d.ts
CHANGED
|
@@ -43,6 +43,8 @@ export declare const NOVEL_API: {
|
|
|
43
43
|
readonly bookshelfImportTextPreview: "/api/dsh-novel-forge/bookshelf/import-text/preview";
|
|
44
44
|
/** 漫剧方案管理:create/remove/activate。 */
|
|
45
45
|
readonly manhuaPlans: "/api/dsh-novel-forge/manhua/plans";
|
|
46
|
+
/** 漫剧角色库:从分镜提名(规则+LLM两段式)/ 建卡 / 更新 / 删除 / 形象锚点 / 精修提示词。 */
|
|
47
|
+
readonly mangaRoles: "/api/dsh-novel-forge/manga/roles";
|
|
46
48
|
/** 分镜·编剧级:单章 → 剧情骨架(节拍链)。 */
|
|
47
49
|
readonly storyboardSkeleton: "/api/dsh-novel-forge/storyboard/skeleton";
|
|
48
50
|
/** 分镜·导演级:骨架 → 分镜表(镜头级)。 */
|
|
@@ -220,6 +222,8 @@ export interface StoryboardSkeleton {
|
|
|
220
222
|
arc: string;
|
|
221
223
|
/** 剧情节拍链(时间顺序,因果连贯)。 */
|
|
222
224
|
beats: StoryboardBeat[];
|
|
225
|
+
/** 本章全部出镜角色(正文中的确切称谓,去重;漫剧角色库提名地基)。 */
|
|
226
|
+
characters?: string[];
|
|
223
227
|
}
|
|
224
228
|
/** POST /storyboard/skeleton 请求:生成单章剧情骨架。 */
|
|
225
229
|
export interface StoryboardSkeletonRequest {
|
|
@@ -253,6 +257,10 @@ export interface StoryboardShot {
|
|
|
253
257
|
prevState: string;
|
|
254
258
|
/** 本镜头结束状态。 */
|
|
255
259
|
nextState: string;
|
|
260
|
+
/** 本镜头出镜角色(引用小说库规范名/漫剧卡名,1-4 个;漫剧角色库提名地基)。 */
|
|
261
|
+
characters?: string[];
|
|
262
|
+
/** 本镜头已绑定漫剧卡的 id(定妆图引用,供出图/生视频时按卡取参考图)。 */
|
|
263
|
+
mangaRoleIds?: string[];
|
|
256
264
|
}
|
|
257
265
|
/** 分镜·导演级:单章分镜表。 */
|
|
258
266
|
export interface StoryboardTable {
|
|
@@ -260,6 +268,10 @@ export interface StoryboardTable {
|
|
|
260
268
|
shots: StoryboardShot[];
|
|
261
269
|
/** 生成时注入的场景卡名称(前端标注消费关系)。 */
|
|
262
270
|
usedScenes?: string[];
|
|
271
|
+
/** 本章全部出镜角色(各镜头 characters 去重汇总;骨架有则兜底)。 */
|
|
272
|
+
characters?: string[];
|
|
273
|
+
/** 本章已绑定漫剧卡的 id(去重汇总,定妆图引用)。 */
|
|
274
|
+
mangaRoleIds?: string[];
|
|
263
275
|
}
|
|
264
276
|
/** POST /storyboard/table 请求:骨架 → 分镜表。 */
|
|
265
277
|
export interface StoryboardTableRequest {
|
|
@@ -281,6 +293,8 @@ export interface StoryboardPrompt {
|
|
|
281
293
|
shotId: string;
|
|
282
294
|
/** 即梦/视频模型可粘贴的中文提示词(画面+运镜+光效+风格词块)。 */
|
|
283
295
|
text: string;
|
|
296
|
+
/** 本镜头绑定的漫剧卡 id(定妆图引用:出图/生视频时按卡取 imageUrl/立绘)。 */
|
|
297
|
+
mangaRoleIds?: string[];
|
|
284
298
|
}
|
|
285
299
|
/** 分镜持久化:单章的分镜产出(骨架 + 分镜表 + 视频提示词,可分别存在)。 */
|
|
286
300
|
export interface ChapterStoryboard {
|
|
@@ -335,6 +349,112 @@ export interface MangaPlansRequest {
|
|
|
335
349
|
export interface MangaPlansResponse {
|
|
336
350
|
plans: MangaPlan[];
|
|
337
351
|
}
|
|
352
|
+
/** 漫剧角色卡:制作角色(要建模/出图/锁一致性的「上镜角色」),与小说角色库弱关联。 */
|
|
353
|
+
export interface MangaRoleCard {
|
|
354
|
+
/** 稳定 id。 */
|
|
355
|
+
id: string;
|
|
356
|
+
/** 来源小说角色名(只读追溯,非强外键;禁止反向写回小说库)。 */
|
|
357
|
+
sourceRoleName?: string;
|
|
358
|
+
/** 漫剧用名(可短剧化改名)。 */
|
|
359
|
+
name: string;
|
|
360
|
+
/** 身份一句话。 */
|
|
361
|
+
identity: string;
|
|
362
|
+
/** 核心功能:主角/导师/感情线/反派/搭档/线人/功能性。 */
|
|
363
|
+
coreFunction: 'protagonist' | 'mentor' | 'love_interest' | 'antagonist' | 'sidekick' | 'informant' | 'functional';
|
|
364
|
+
/** 与主角的关系。 */
|
|
365
|
+
protagonistRelation: 'enemy' | 'friend' | 'mentor' | 'lover' | 'exploit' | 'neutral';
|
|
366
|
+
/** 口头禅/说话方式(配音一致性)。 */
|
|
367
|
+
speechStyle: string;
|
|
368
|
+
/** ≤3 个极致性格标签。 */
|
|
369
|
+
traits: string[];
|
|
370
|
+
/** 1-2 个辨识度外貌点。 */
|
|
371
|
+
appearance: string;
|
|
372
|
+
/** 3-5 个关键剧情节点。 */
|
|
373
|
+
keyScenes: string[];
|
|
374
|
+
/** 上场集数(分镜提名导入时记录)。 */
|
|
375
|
+
appearsInEpisodes: number[];
|
|
376
|
+
/** 待匹配 → 待确认 → 已导入 → 已定妆 → 已归档。 */
|
|
377
|
+
status: 'pending_match' | 'pending_confirm' | 'imported' | 'anchored' | 'archived';
|
|
378
|
+
/** 形象资产(沿用小说角色库同款结构)。 */
|
|
379
|
+
imagePrompt?: RoleRecord['imagePrompt'];
|
|
380
|
+
/** 情绪表情清单(锚点提炼时产出)。 */
|
|
381
|
+
expressions?: string[];
|
|
382
|
+
/** 四类精修生图提示词(立绘/四视图/表情/细节)。 */
|
|
383
|
+
promptKit?: RoleRecord['promptKit'];
|
|
384
|
+
/** 角色参考图。 */
|
|
385
|
+
imageUrl?: string;
|
|
386
|
+
/** 图集(图像锚点)。 */
|
|
387
|
+
gallery?: RoleImage[];
|
|
388
|
+
/** 生成锚点/精修时的方案风格 id(旧风格检测用)。 */
|
|
389
|
+
promptStyleId?: string;
|
|
390
|
+
createdAt: string;
|
|
391
|
+
updatedAt: string;
|
|
392
|
+
}
|
|
393
|
+
/** 分镜提名 → 小说角色库匹配的一条候选。 */
|
|
394
|
+
export interface MangaRoleCandidate {
|
|
395
|
+
/** 分镜里的原始称谓(正文确切用法)。 */
|
|
396
|
+
rawName: string;
|
|
397
|
+
/** matched=命中小说角色;ambiguous=多个候选待定;not_in_library=小说库没有;already_imported=已建漫剧卡(跳过)。 */
|
|
398
|
+
verdict: 'matched' | 'ambiguous' | 'not_in_library' | 'already_imported';
|
|
399
|
+
/** 规则阶段候选短名单(≤5,精确名优先 + 身份/简称匹配)。 */
|
|
400
|
+
matches: Array<{
|
|
401
|
+
roleName: string;
|
|
402
|
+
reason: string;
|
|
403
|
+
}>;
|
|
404
|
+
/** LLM/规则确认的命中角色名(ambiguous 时可能为空)。 */
|
|
405
|
+
matchedRoleName?: string;
|
|
406
|
+
/** not_in_library 时给出建议:backfill=正文有该称谓但小说库漏提炼(回小说库补);manga_new=漫剧新增角色(直接建卡)。 */
|
|
407
|
+
novelHint?: 'backfill' | 'manga_new';
|
|
408
|
+
/** 采纳时预填的漫剧卡建议。 */
|
|
409
|
+
suggested: {
|
|
410
|
+
name: string;
|
|
411
|
+
identity: string;
|
|
412
|
+
coreFunction: MangaRoleCard['coreFunction'];
|
|
413
|
+
protagonistRelation: MangaRoleCard['protagonistRelation'];
|
|
414
|
+
speechStyle: string;
|
|
415
|
+
traits: string[];
|
|
416
|
+
appearance: string;
|
|
417
|
+
keyScenes: string[];
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
/** POST /manga/roles 请求:漫剧角色库操作。 */
|
|
421
|
+
export interface MangaRolesRequest {
|
|
422
|
+
op: 'nominate' | 'adopt' | 'update' | 'remove' | 'visual' | 'promptKit' | 'image' | 'removeImage' | 'imageGenerate' | 'mode';
|
|
423
|
+
/** op='mode':短剧精简模式开关。 */
|
|
424
|
+
shortDramaMode?: boolean;
|
|
425
|
+
/** op='nominate':从哪章分镜提名角色。 */
|
|
426
|
+
chapterNo?: number;
|
|
427
|
+
/** op='adopt' / op='update':漫剧角色卡。 */
|
|
428
|
+
card?: MangaRoleCard;
|
|
429
|
+
/** op='remove' / 'visual' / 'promptKit' / 'image' / 'removeImage' / 'imageGenerate':漫剧卡 id。 */
|
|
430
|
+
id?: string;
|
|
431
|
+
/** op='visual' / 'promptKit':漫剧方案基底风格 id。 */
|
|
432
|
+
styleId?: string;
|
|
433
|
+
/** 可选滤镜风格 id。 */
|
|
434
|
+
filterId?: string;
|
|
435
|
+
/** op='image':定妆图 dataURL。 */
|
|
436
|
+
dataUrl?: string;
|
|
437
|
+
/** op='image':图集用途标签(立绘/四视图/表情-x/场景/细节);缺省视为角色参考图(imageUrl)。 */
|
|
438
|
+
label?: string;
|
|
439
|
+
/** op='imageGenerate':漫画风格预设 id(可选)。 */
|
|
440
|
+
style?: string;
|
|
441
|
+
/** op='imageGenerate':生图模型库条目 id(缺省用启用条目)。 */
|
|
442
|
+
modelId?: string;
|
|
443
|
+
}
|
|
444
|
+
/** POST /manga/roles 响应。 */
|
|
445
|
+
export interface MangaRolesResponse {
|
|
446
|
+
cards: MangaRoleCard[];
|
|
447
|
+
/** op='nominate' 时的 AI 提名候选。 */
|
|
448
|
+
candidates?: MangaRoleCandidate[];
|
|
449
|
+
/** op='visual' 时的形象锚点(已写入漫剧卡)。 */
|
|
450
|
+
visual?: RoleRecord['imagePrompt'];
|
|
451
|
+
/** op='promptKit' 时的四类精修提示词包(已写入漫剧卡)。 */
|
|
452
|
+
promptKit?: RoleRecord['promptKit'];
|
|
453
|
+
/** op='image' / 'imageGenerate' 时的定妆图 dataURL(已写入漫剧卡)。 */
|
|
454
|
+
imageUrl?: string;
|
|
455
|
+
/** op='mode' 时的短剧精简模式状态。 */
|
|
456
|
+
shortDramaMode?: boolean;
|
|
457
|
+
}
|
|
338
458
|
/** Chapter lifecycle states (the writing pipeline's state machine). */
|
|
339
459
|
export type ChapterStatus = 'pending' | 'generating' | 'written' | 'reviewing' | 'approved' | 'rejected' | 'error';
|
|
340
460
|
/** One chapter in the plan. */
|
|
@@ -979,6 +1099,10 @@ export interface ProjectState {
|
|
|
979
1099
|
plotlines?: Plotline[];
|
|
980
1100
|
/** 角色库(作者维护 + AI 提炼的主表)。 */
|
|
981
1101
|
roles?: RoleRecord[];
|
|
1102
|
+
/** 漫剧角色库(制作角色投影:从分镜提名导入,独立于小说角色库)。 */
|
|
1103
|
+
mangaRoles?: MangaRoleCard[];
|
|
1104
|
+
/** 短剧精简模式:漫剧角色库按 5-8 上镜角色 / 功能标签 / 关系闭环 / 人设极致化约束。 */
|
|
1105
|
+
shortDramaMode?: boolean;
|
|
982
1106
|
/** 场景库(作者维护 + AI 提炼的主表)。 */
|
|
983
1107
|
scenes?: SceneCard[];
|
|
984
1108
|
/** 视觉世界观规则(生图/生视频必须遵守,从道藏提炼,注入所有提示词)。 */
|
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.7.
|
|
4
|
+
"version": "1.7.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -94,6 +94,7 @@
|
|
|
94
94
|
"bundle": "tsdown",
|
|
95
95
|
"watch": "tsdown --watch",
|
|
96
96
|
"typecheck": "tsc --noEmit",
|
|
97
|
+
"check:styles": "node scripts/check-theme-sizes.mjs",
|
|
97
98
|
"test": "vitest run"
|
|
98
99
|
}
|
|
99
100
|
}
|
package/src/client/api.ts
CHANGED
|
@@ -236,6 +236,11 @@ export class NovelApi {
|
|
|
236
236
|
return postJson<import('../protocol.ts').RolesResponse>(NOVEL_API.roles, req)
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
/** 漫剧角色库:从分镜提名 / 建卡 / 更新 / 删除 / 形象锚点 / 精修提示词。 */
|
|
240
|
+
async mangaRoles(req: import('../protocol.ts').MangaRolesRequest): Promise<import('../protocol.ts').MangaRolesResponse> {
|
|
241
|
+
return postJson<import('../protocol.ts').MangaRolesResponse>(NOVEL_API.mangaRoles, req)
|
|
242
|
+
}
|
|
243
|
+
|
|
239
244
|
/** 场景库:AI 提炼 / 采纳 / 更新 / 删除 / 图集。 */
|
|
240
245
|
async scenes(req: import('../protocol.ts').ScenesRequest): Promise<import('../protocol.ts').ScenesResponse> {
|
|
241
246
|
return postJson<import('../protocol.ts').ScenesResponse>(NOVEL_API.scenes, req)
|
package/src/client/locales.ts
CHANGED
|
@@ -154,7 +154,6 @@ export const zh = {
|
|
|
154
154
|
'settings.reasoning.max': 'max · 最高思考(质检/推演)',
|
|
155
155
|
'settings.theme': '主题',
|
|
156
156
|
'settings.themeLiquid': 'iOS 液态玻璃(绿)',
|
|
157
|
-
'settings.themeClassic': '经典毛玻璃(蓝)',
|
|
158
157
|
'settings.themeNeumorph': '新拟物(双阴影)',
|
|
159
158
|
'settings.themeHint': '存于浏览器本地,即时生效',
|
|
160
159
|
'settings.chapterChars': '每章目标字数',
|
|
@@ -19,7 +19,7 @@ export interface AssetsTabProps {
|
|
|
19
19
|
function GenreTree({ node, selected, onSelect }: { node: GenreNode; selected: string; onSelect: (node: GenreNode) => void }) {
|
|
20
20
|
return (
|
|
21
21
|
<div>
|
|
22
|
-
<label style={{ display: 'flex', alignItems: 'flex-start', gap: 6, cursor: 'pointer' }}>
|
|
22
|
+
<label style={{ display: 'flex', alignItems: 'flex-start', gap: 'var(--nf-space-6)', cursor: 'pointer' }}>
|
|
23
23
|
<input type="radio" name="genre" checked={selected === node.name} onChange={() => { onSelect(node) }} />
|
|
24
24
|
<span>
|
|
25
25
|
<b>{node.name}</b>
|
|
@@ -34,7 +34,7 @@ function GenreTree({ node, selected, onSelect }: { node: GenreNode; selected: st
|
|
|
34
34
|
</span>
|
|
35
35
|
</label>
|
|
36
36
|
{node.children.length > 0 && (
|
|
37
|
-
<div style={{ marginLeft:
|
|
37
|
+
<div style={{ marginLeft: 'var(--nf-space-24)', display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-4)' }}>
|
|
38
38
|
{node.children.map(child => (
|
|
39
39
|
<GenreTree key={child.name} node={child} selected={selected} onSelect={onSelect} />
|
|
40
40
|
))}
|
|
@@ -205,9 +205,9 @@ export function AssetsTab({ api, initialTab = 'genre' }: AssetsTabProps) {
|
|
|
205
205
|
const genreLibrary = data.genreLibrary
|
|
206
206
|
|
|
207
207
|
return (
|
|
208
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, flex: 1, minHeight: 0 }}>
|
|
208
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-12)', flex: 1, minHeight: 0 }}>
|
|
209
209
|
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
210
|
-
<span className={css.cardTitle} style={{ fontSize:
|
|
210
|
+
<span className={css.cardTitle} style={{ fontSize: 'var(--nf-fs-16)', fontWeight: 700 }}>🧰 创作资产</span>
|
|
211
211
|
<span className={css.meta}>题材基底 / 推进模式 / 笔法帖 / 文戒 / 心法</span>
|
|
212
212
|
</div>
|
|
213
213
|
{error !== '' && <div className={css.card} style={{ borderColor: 'var(--nf-error)' }}><span style={{ color: 'var(--nf-error)' }}>{tt('common.error')}: {error}</span></div>}
|
|
@@ -240,13 +240,13 @@ export function AssetsTab({ api, initialTab = 'genre' }: AssetsTabProps) {
|
|
|
240
240
|
</div>
|
|
241
241
|
|
|
242
242
|
{/* 子页签:题材基底 / 推进模式 / 笔法帖 / 文戒 / 心法 */}
|
|
243
|
-
<div className={css.row} style={{ flexWrap: 'wrap', gap: 6 }}>
|
|
243
|
+
<div className={css.row} style={{ flexWrap: 'wrap', gap: 'var(--nf-space-6)' }}>
|
|
244
244
|
{SUB_TABS.map(t => (
|
|
245
245
|
<button
|
|
246
246
|
key={t.id}
|
|
247
247
|
type="button"
|
|
248
248
|
className={`${css.button} ${assetTab === t.id ? css.buttonPrimary : ''}`}
|
|
249
|
-
style={{ fontSize: 14, flex: 1 }}
|
|
249
|
+
style={{ fontSize: 'var(--nf-fs-14)', flex: 1 }}
|
|
250
250
|
onClick={() => { setAssetTab(t.id) }}
|
|
251
251
|
>
|
|
252
252
|
{t.label}
|
|
@@ -260,12 +260,12 @@ export function AssetsTab({ api, initialTab = 'genre' }: AssetsTabProps) {
|
|
|
260
260
|
<span className={css.cardTitle}>题材基底库</span>
|
|
261
261
|
<span className={css.meta}>这本书属于哪个阅读市场?题材定位会注入章节生成与审稿提示词。</span>
|
|
262
262
|
{assets.genre !== undefined && (
|
|
263
|
-
<div style={{ border: '1px solid var(--nf-border)', borderRadius: 6, padding: '
|
|
263
|
+
<div style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-6)', padding: 'var(--nf-space-6) var(--nf-space-10)', fontSize: 'var(--nf-fs-12)' }}>
|
|
264
264
|
<b>当前题材:{assets.genre.name}</b> — {assets.genre.description}
|
|
265
265
|
</div>
|
|
266
266
|
)}
|
|
267
267
|
{/* 题材基底库:自然展开,不套内部滚动条(描述两行截断,悬停看全文) */}
|
|
268
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
268
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-8)' }}>
|
|
269
269
|
{genreLibrary.map(root => <GenreTree key={root.name} node={root} selected={assets.genre?.name ?? ''} onSelect={handleSelectGenre} />)}
|
|
270
270
|
</div>
|
|
271
271
|
</div>
|
|
@@ -277,17 +277,17 @@ export function AssetsTab({ api, initialTab = 'genre' }: AssetsTabProps) {
|
|
|
277
277
|
<span className={css.cardTitle}>推进模式库</span>
|
|
278
278
|
<span className={css.meta}>读者为什么继续看下一章?主模式 + 辅助模式注入卷规划与章节生成。</span>
|
|
279
279
|
{assets.primaryProgression !== undefined && (
|
|
280
|
-
<div style={{ border: '1px solid var(--nf-accent)', borderRadius: 6, padding: '
|
|
280
|
+
<div style={{ border: '1px solid var(--nf-accent)', borderRadius: 'var(--nf-radius-6)', padding: 'var(--nf-space-6) var(--nf-space-10)', fontSize: 'var(--nf-fs-12)', color: 'var(--nf-accent)' }}>
|
|
281
281
|
<b>主推进:{assets.primaryProgression.name}</b> — {assets.primaryProgression.driver}
|
|
282
282
|
</div>
|
|
283
283
|
)}
|
|
284
284
|
{assets.auxiliaryProgressions.map(mode => (
|
|
285
|
-
<div key={mode.name} style={{ border: '1px solid var(--nf-border)', borderRadius: 6, padding: '
|
|
285
|
+
<div key={mode.name} style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-6)', padding: 'var(--nf-space-6) var(--nf-space-10)', fontSize: 'var(--nf-fs-12)' }}>
|
|
286
286
|
<b>{mode.name}</b> — {mode.driver}
|
|
287
287
|
</div>
|
|
288
288
|
))}
|
|
289
289
|
<span className={css.meta}>从内置推进模式库选择添加(第一个设为主推进):</span>
|
|
290
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, flex: 1, minHeight: 0, overflowY: 'auto' }}>
|
|
290
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)', flex: 1, minHeight: 0, overflowY: 'auto' }}>
|
|
291
291
|
{data.progressionLibrary.map(mode => {
|
|
292
292
|
const alreadyPrimary = assets.primaryProgression?.name === mode.name
|
|
293
293
|
const alreadyAux = assets.auxiliaryProgressions.some(m => m.name === mode.name)
|
|
@@ -307,12 +307,12 @@ export function AssetsTab({ api, initialTab = 'genre' }: AssetsTabProps) {
|
|
|
307
307
|
<div className={css.card} style={{ flex: 1, minHeight: 0 }}>
|
|
308
308
|
<span className={css.cardTitle}>笔法帖</span>
|
|
309
309
|
<span className={css.meta}>从内置 8 套叙事风格模板中一键选用(来自 AI-Novel-Writing-Assistant 写法引擎),无需样本文本;绑定后生成与润色都遵循该风格。</span>
|
|
310
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, flex: 1, minHeight: 0, overflowY: 'auto' }}>
|
|
310
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-8)', flex: 1, minHeight: 0, overflowY: 'auto' }}>
|
|
311
311
|
{data.styleTemplates.map(template => {
|
|
312
312
|
const bound = assets.styleAssets.some(s => s.name === template.name)
|
|
313
313
|
return (
|
|
314
|
-
<div key={template.key} style={{ border: `1px solid ${bound ? 'var(--nf-accent)' : 'var(--nf-border)'}`, borderRadius: 6, padding: '
|
|
315
|
-
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
|
314
|
+
<div key={template.key} style={{ border: `1px solid ${bound ? 'var(--nf-accent)' : 'var(--nf-border)'}`, borderRadius: 'var(--nf-radius-6)', padding: 'var(--nf-space-8) var(--nf-space-10)', fontSize: 'var(--nf-fs-12)' }}>
|
|
315
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--nf-space-8)' }}>
|
|
316
316
|
<span><b>{template.name}</b> <span className={css.badge} style={{ borderColor: 'var(--nf-text-3)', color: 'var(--nf-text-3)' }}>{template.category}</span></span>
|
|
317
317
|
<button
|
|
318
318
|
className={`${css.button} ${css.buttonSmall} ${bound ? '' : css.buttonPrimary}`}
|
|
@@ -347,12 +347,12 @@ export function AssetsTab({ api, initialTab = 'genre' }: AssetsTabProps) {
|
|
|
347
347
|
<div className={css.card} style={{ flex: 1, minHeight: 0 }}>
|
|
348
348
|
<span className={css.cardTitle}>文戒</span>
|
|
349
349
|
<span className={css.meta}>写作时必须遵守的表达边界(内置全局 + 项目自定义),生成与审稿都会检查。内置规则可用「覆盖编辑」复制为自定义版本调整。</span>
|
|
350
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, flex: 1, minHeight: 0, overflowY: 'auto' }}>
|
|
350
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)', flex: 1, minHeight: 0, overflowY: 'auto' }}>
|
|
351
351
|
{builtinRules.map(rule => {
|
|
352
352
|
const overridden = customRules.some(r => r.name === rule.name)
|
|
353
353
|
return (
|
|
354
|
-
<div key={rule.name} style={{ border: '1px solid var(--nf-border)', borderRadius: 6, padding: '
|
|
355
|
-
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
|
354
|
+
<div key={rule.name} style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-6)', padding: 'var(--nf-space-6) var(--nf-space-10)', fontSize: 'var(--nf-fs-12)' }}>
|
|
355
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--nf-space-8)' }}>
|
|
356
356
|
<span>
|
|
357
357
|
<b>{rule.name}</b>{' '}
|
|
358
358
|
<span className={css.badge} style={{ borderColor: overridden ? 'var(--nf-accent)' : 'var(--nf-text-3)', color: overridden ? 'var(--nf-accent)' : 'var(--nf-text-3)' }}>
|
|
@@ -369,13 +369,13 @@ export function AssetsTab({ api, initialTab = 'genre' }: AssetsTabProps) {
|
|
|
369
369
|
)
|
|
370
370
|
})}
|
|
371
371
|
{customRules.map((rule, index) => (
|
|
372
|
-
<div key={`${rule.name}-${index}`} style={{ border: '1px solid var(--nf-accent)', borderRadius: 6, padding: '
|
|
373
|
-
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
|
372
|
+
<div key={`${rule.name}-${index}`} style={{ border: '1px solid var(--nf-accent)', borderRadius: 'var(--nf-radius-6)', padding: 'var(--nf-space-6) var(--nf-space-10)', fontSize: 'var(--nf-fs-12)' }}>
|
|
373
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--nf-space-8)' }}>
|
|
374
374
|
<span>
|
|
375
375
|
<b>{rule.name}</b>{' '}
|
|
376
376
|
<span className={css.badge} style={{ borderColor: 'var(--nf-accent)', color: 'var(--nf-accent)' }}>自定义</span>
|
|
377
377
|
</span>
|
|
378
|
-
<span style={{ display: 'flex', gap: 6 }}>
|
|
378
|
+
<span style={{ display: 'flex', gap: 'var(--nf-space-6)' }}>
|
|
379
379
|
<button className={`${css.button} ${css.buttonSmall}`} disabled={busy} onClick={() => { setEditingRule({ index, name: rule.name, avoid: rule.avoid, fix: rule.fix ?? '' }) }}>
|
|
380
380
|
编辑
|
|
381
381
|
</button>
|
|
@@ -385,7 +385,7 @@ export function AssetsTab({ api, initialTab = 'genre' }: AssetsTabProps) {
|
|
|
385
385
|
</span>
|
|
386
386
|
</div>
|
|
387
387
|
{editingRule !== null && editingRule.index === index ? (
|
|
388
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 6 }}>
|
|
388
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)', marginTop: 'var(--nf-space-6)' }}>
|
|
389
389
|
<input className={css.input} placeholder="规则名" value={editingRule.name} onChange={e => { setEditingRule({ ...editingRule, name: e.target.value }) }} />
|
|
390
390
|
<input className={css.input} placeholder="避免(要杜绝的表达)" value={editingRule.avoid} onChange={e => { setEditingRule({ ...editingRule, avoid: e.target.value }) }} />
|
|
391
391
|
<input className={css.input} placeholder="修正(改写方向,可留空)" value={editingRule.fix} onChange={e => { setEditingRule({ ...editingRule, fix: e.target.value }) }} />
|
|
@@ -426,7 +426,7 @@ export function AssetsTab({ api, initialTab = 'genre' }: AssetsTabProps) {
|
|
|
426
426
|
<span className={css.cardTitle}>心法</span>
|
|
427
427
|
<span className={css.meta}>粘贴一段你喜欢的样本文本,AI 提取叙事风格规则并绑定到本书,后续章节保持同一味道。</span>
|
|
428
428
|
{assets.styleAssets.map(style => (
|
|
429
|
-
<div key={style.name} style={{ border: '1px solid var(--nf-border)', borderRadius: 6, padding: '
|
|
429
|
+
<div key={style.name} style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-6)', padding: 'var(--nf-space-6) var(--nf-space-10)', fontSize: 'var(--nf-fs-12)' }}>
|
|
430
430
|
<b>{style.name}</b>
|
|
431
431
|
<div className={css.meta}>叙述:{style.proseRules.slice(0, 3).join(';')}</div>
|
|
432
432
|
{style.dialogueRules.length > 0 && <div className={css.meta}>台词:{style.dialogueRules.slice(0, 2).join(';')}</div>}
|
|
@@ -227,8 +227,8 @@ export function AssistantTab({ api }: AssistantTabProps) {
|
|
|
227
227
|
</button>
|
|
228
228
|
</div>
|
|
229
229
|
<span className={css.meta}>{tt('assistant.hint')}</span>
|
|
230
|
-
{notice !== '' && <span style={{ color: 'var(--nf-success)', fontSize: 12 }}>{notice}</span>}
|
|
231
|
-
{error !== '' && <span style={{ color: 'var(--nf-error)', fontSize: 12 }}>{tt('common.error')}: {error}</span>}
|
|
230
|
+
{notice !== '' && <span style={{ color: 'var(--nf-success)', fontSize: 'var(--nf-fs-12)' }}>{notice}</span>}
|
|
231
|
+
{error !== '' && <span style={{ color: 'var(--nf-error)', fontSize: 'var(--nf-fs-12)' }}>{tt('common.error')}: {error}</span>}
|
|
232
232
|
{/* 状态条:编辑老师正在做什么 */}
|
|
233
233
|
<div className={`${css.assistantStatus} ${busy ? css.assistantStatusBusy : ''}`}>
|
|
234
234
|
{busy
|
|
@@ -278,7 +278,7 @@ export function AssistantTab({ api }: AssistantTabProps) {
|
|
|
278
278
|
))}
|
|
279
279
|
{busy && <span className={css.meta} style={{ color: 'var(--nf-accent)' }}>…</span>}
|
|
280
280
|
</div>
|
|
281
|
-
<div className={css.row} style={{ marginTop: 8 }}>
|
|
281
|
+
<div className={css.row} style={{ marginTop: 'var(--nf-space-8)' }}>
|
|
282
282
|
<textarea
|
|
283
283
|
className={css.textarea}
|
|
284
284
|
style={{ minHeight: 64, flex: 1 }}
|
|
@@ -164,7 +164,7 @@ export function BookshelfBar({ api, shelf, onSwitch }: BookshelfBarProps) {
|
|
|
164
164
|
</div>
|
|
165
165
|
)}
|
|
166
166
|
</div>
|
|
167
|
-
{error !== '' && <span style={{ color: 'var(--nf-error)', fontSize: 12 }}>{error}</span>}
|
|
167
|
+
{error !== '' && <span style={{ color: 'var(--nf-error)', fontSize: 'var(--nf-fs-12)' }}>{error}</span>}
|
|
168
168
|
</div>
|
|
169
169
|
)
|
|
170
170
|
}
|
|
@@ -161,14 +161,14 @@ export function CreateBookView({
|
|
|
161
161
|
<span className={css.meta}>把一份大纲「编译」成一本完整的小说</span>
|
|
162
162
|
|
|
163
163
|
{error !== '' && (
|
|
164
|
-
<div className={css.card} style={{ borderColor: 'var(--nf-error)', padding: '
|
|
165
|
-
<span style={{ color: 'var(--nf-error)', fontSize: 12 }}>{error}</span>
|
|
164
|
+
<div className={css.card} style={{ borderColor: 'var(--nf-error)', padding: 'var(--nf-space-8) var(--nf-space-12)' }}>
|
|
165
|
+
<span style={{ color: 'var(--nf-error)', fontSize: 'var(--nf-fs-12)' }}>{error}</span>
|
|
166
166
|
</div>
|
|
167
167
|
)}
|
|
168
168
|
|
|
169
169
|
{fillNotice !== '' && (
|
|
170
|
-
<div className={css.card} style={{ borderColor: 'var(--nf-success)', padding: '
|
|
171
|
-
<span style={{ color: 'var(--nf-success)', fontSize: 12 }}>✅ {fillNotice}</span>
|
|
170
|
+
<div className={css.card} style={{ borderColor: 'var(--nf-success)', padding: 'var(--nf-space-8) var(--nf-space-12)' }}>
|
|
171
|
+
<span style={{ color: 'var(--nf-success)', fontSize: 'var(--nf-fs-12)' }}>✅ {fillNotice}</span>
|
|
172
172
|
</div>
|
|
173
173
|
)}
|
|
174
174
|
|
|
@@ -240,7 +240,7 @@ export function CreateBookView({
|
|
|
240
240
|
✨ {ideaOpen ? '▾' : '▸'} 没有大纲?用一句话想法让 AI 生成
|
|
241
241
|
</button>
|
|
242
242
|
{ideaOpen && (
|
|
243
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
243
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-8)' }}>
|
|
244
244
|
<textarea
|
|
245
245
|
className={css.textarea}
|
|
246
246
|
style={{ minHeight: 60 }}
|
|
@@ -268,7 +268,7 @@ export function CreateBookView({
|
|
|
268
268
|
|
|
269
269
|
{/* 方案卡 */}
|
|
270
270
|
{candidates.length > 0 && (
|
|
271
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
271
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-8)' }}>
|
|
272
272
|
{candidates.map(candidate => {
|
|
273
273
|
const isPinned = pinned.includes(candidate.id)
|
|
274
274
|
return (
|
|
@@ -277,13 +277,13 @@ export function CreateBookView({
|
|
|
277
277
|
className={css.ideaCandidate}
|
|
278
278
|
style={isPinned ? { borderColor: 'var(--nf-accent)', boxShadow: '0 0 0 2px var(--nf-accent-soft)' } : undefined}
|
|
279
279
|
>
|
|
280
|
-
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap', gap: 6 }}>
|
|
281
|
-
<span style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
|
280
|
+
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap', gap: 'var(--nf-space-6)' }}>
|
|
281
|
+
<span style={{ display: 'flex', alignItems: 'center', gap: 'var(--nf-space-8)', flexWrap: 'wrap' }}>
|
|
282
282
|
<b>《{candidate.bookName}》</b>
|
|
283
283
|
{candidate.genre !== '' && <span className={css.badge} style={{ borderColor: 'var(--nf-accent)', color: 'var(--nf-accent)' }}>{candidate.genre}</span>}
|
|
284
284
|
{isPinned && <span className={css.badge} style={{ borderColor: 'var(--nf-warn)', color: 'var(--nf-warn)' }}>★ 已暂留</span>}
|
|
285
285
|
</span>
|
|
286
|
-
<span style={{ display: 'flex', gap: 6 }}>
|
|
286
|
+
<span style={{ display: 'flex', gap: 'var(--nf-space-6)' }}>
|
|
287
287
|
<button
|
|
288
288
|
type="button"
|
|
289
289
|
className={`${css.button} ${css.buttonSmall} ${isPinned ? '' : css.buttonPrimary}`}
|
|
@@ -333,7 +333,7 @@ export function CreateBookView({
|
|
|
333
333
|
<button
|
|
334
334
|
type="button"
|
|
335
335
|
className={`${css.button} ${css.buttonPrimary}`}
|
|
336
|
-
style={{ width: '100%', padding: '
|
|
336
|
+
style={{ width: '100%', padding: 'var(--nf-space-10) 0', fontSize: 'var(--nf-fs-14)' }}
|
|
337
337
|
disabled={busy || effectiveName === ''}
|
|
338
338
|
onClick={() => { void handleCreate() }}
|
|
339
339
|
>
|