|
|
@@ -57,14 +57,15 @@ const NOVEL_TYPE_OPTIONS: { value: NovelType; label: string; description: string
|
|
|
{ value: 'custom', label: '自定义', description: '自定义世界观,不限类型' },
|
|
|
];
|
|
|
|
|
|
-const STYLE_PRESETS_WITH_MODULES = [
|
|
|
- { label: '轻松幽默', description: '节奏轻快,对话风趣,充满喜剧感', styleId: 'humor' as const },
|
|
|
- { label: '悬疑紧张', description: '信息差强烈,节奏张弛有度,悬念迭起', styleId: 'suspense' as const },
|
|
|
- { label: '热血爽文', description: '节奏紧凑,打脸升级,情绪高亢', styleId: '' as const },
|
|
|
- { label: '细腻写实', description: '心理描写丰富,细节真实,情感深沉', styleId: 'literary' as const },
|
|
|
- { label: '温馨治愈', description: '温暖日常,情感细腻,治愈人心', styleId: 'romance' as const },
|
|
|
- { label: '黑暗沉重', description: '世界观压抑,人性复杂,命运无常', styleId: 'horror' as const },
|
|
|
-];
|
|
|
+const STYLE_MODULE_DESCRIPTIONS: Record<string, string> = {
|
|
|
+ humor: '笑点来自角色间信息差、性格差和预期落差。callback回响,喜剧作压力释放阀',
|
|
|
+ suspense: '信息差制造预测压力,倒计时收紧节奏。每章留下可操作的未知',
|
|
|
+ mystery: '线索公平性,多层误导,揭示阶梯。读者与侦探信息同步',
|
|
|
+ romance: '情感债务驱动,亚文本大于表白,障碍来自角色内部',
|
|
|
+ horror: '有限POV,日常异化,恐惧等级递增。不依赖血腥惊吓',
|
|
|
+ fantasy: '规则代价一致性,日常厚度支撑奇观。魔法体系自洽',
|
|
|
+ literary: '语言节制,主题通过选择戏剧化,意象系统一致',
|
|
|
+};
|
|
|
|
|
|
const DISABLABLE_WORLD_STATE_FILES: { key: string; label: string }[] = [
|
|
|
{ key: 'plot_summary.md', label: '剧情梗概' },
|
|
|
@@ -607,8 +608,6 @@ function Step5StyleAndGenerate({
|
|
|
const [allDone, setAllDone] = useState(false);
|
|
|
const [globalError, setGlobalError] = useState<string | null>(null);
|
|
|
|
|
|
- // Only local state: preset selection (cosmetic) and free-text description
|
|
|
- const [selectedPreset, setSelectedPreset] = useState<string | null>(null);
|
|
|
const [styleDescription, setStyleDescription] = useState('');
|
|
|
|
|
|
const runGeneration = useCallback(async () => {
|
|
|
@@ -633,7 +632,7 @@ function Step5StyleAndGenerate({
|
|
|
projectPath, novelType,
|
|
|
worldDescription: worldDescription || undefined,
|
|
|
apiSettings: apiSettings || undefined,
|
|
|
- writingStylePreset: selectedPreset || undefined,
|
|
|
+ writingStylePreset: primaryStyleId || undefined,
|
|
|
writingStyleDescription: enrichedDescription || undefined,
|
|
|
disabledFiles,
|
|
|
generateIronRules,
|
|
|
@@ -667,7 +666,7 @@ function Step5StyleAndGenerate({
|
|
|
} finally {
|
|
|
setIsRunning(false);
|
|
|
}
|
|
|
- }, [projectPath, novelType, worldDescription, apiSettings, selectedPreset, styleDescription, disabledFiles, interviewData, primaryStyleId, supportStyleId, styleIntensity, paragraphMode, useChapterTitles, generateIronRules, generateStyleAnchor]);
|
|
|
+ }, [projectPath, novelType, worldDescription, apiSettings, styleDescription, disabledFiles, interviewData, primaryStyleId, supportStyleId, styleIntensity, paragraphMode, useChapterTitles, generateIronRules, generateStyleAnchor]);
|
|
|
|
|
|
return (
|
|
|
<div>
|
|
|
@@ -675,41 +674,30 @@ function Step5StyleAndGenerate({
|
|
|
<p className="text-sm text-gray-500 mb-5">选择文风预设或自由描述,AI 将据此生成文风档案。</p>
|
|
|
|
|
|
<div className="mb-5 border border-gray-200 rounded-xl p-4 space-y-4">
|
|
|
- {/* Presets */}
|
|
|
+ {/* Style module cards — click to select primary, secondary via dropdown */}
|
|
|
<div>
|
|
|
- <p className="text-xs text-gray-500 mb-2">预设文风模板</p>
|
|
|
- <div className="grid grid-cols-2 gap-2">
|
|
|
- {STYLE_PRESETS_WITH_MODULES.map((preset) => (
|
|
|
- <button key={preset.label} onClick={() => {
|
|
|
- setSelectedPreset((prev) => prev === preset.label ? null : preset.label);
|
|
|
- if (preset.styleId && primaryStyleId !== preset.styleId) setPrimaryStyleId(preset.styleId);
|
|
|
- }}
|
|
|
- className={`text-left p-3 rounded-lg border text-xs transition-all ${
|
|
|
- selectedPreset === preset.label ? 'border-blue-500 bg-blue-50 ring-1 ring-blue-200' : 'border-gray-200 hover:border-gray-300'
|
|
|
+ <p className="text-xs text-gray-500 mb-2">
|
|
|
+ 风格模块
|
|
|
+ <span className="text-gray-400 ml-2">单击选择主风格(控制 AI 生成的基调),辅助风格通过下方下拉框选择</span>
|
|
|
+ </p>
|
|
|
+ <div className="grid grid-cols-2 gap-2 mb-3">
|
|
|
+ {STYLE_MODULES.map((m) => (
|
|
|
+ <button key={m.id} onClick={() => setPrimaryStyleId(m.id === primaryStyleId ? '' : m.id)}
|
|
|
+ className={`text-left p-2.5 rounded-lg border text-xs transition-all ${
|
|
|
+ primaryStyleId === m.id
|
|
|
+ ? 'border-blue-500 bg-blue-50 ring-1 ring-blue-200'
|
|
|
+ : 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'
|
|
|
}`}>
|
|
|
- <div className="font-medium text-gray-800">{preset.label}</div>
|
|
|
- <div className="text-gray-500 mt-0.5">{preset.description}</div>
|
|
|
+ <div className="font-medium text-gray-800 text-xs">{m.name}</div>
|
|
|
+ <div className="text-gray-500 mt-0.5 leading-relaxed">{STYLE_MODULE_DESCRIPTIONS[m.id] || m.description}</div>
|
|
|
</button>
|
|
|
))}
|
|
|
</div>
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* Style module selection */}
|
|
|
- <div>
|
|
|
- <p className="text-xs text-gray-500 mb-2">风格模块(对 AI 生成起控制作用)</p>
|
|
|
- <div className="flex items-center gap-3 mb-2">
|
|
|
- <label className="text-xs text-gray-600 w-14">主风格</label>
|
|
|
- <select value={primaryStyleId} onChange={(e) => setPrimaryStyleId(e.target.value)}
|
|
|
- className="flex-1 px-2 py-1.5 text-xs border border-gray-200 rounded focus:outline-none focus:border-blue-400">
|
|
|
- <option value="">不指定</option>
|
|
|
- {STYLE_MODULES.map((m) => <option key={m.id} value={m.id}>{m.name} - {m.description}</option>)}
|
|
|
- </select>
|
|
|
- </div>
|
|
|
<div className="flex items-center gap-3">
|
|
|
- <label className="text-xs text-gray-600 w-14">辅助风格</label>
|
|
|
+ <label className="text-xs text-gray-600 w-14 shrink-0">辅助风格</label>
|
|
|
<select value={supportStyleId} onChange={(e) => setSupportStyleId(e.target.value)}
|
|
|
className="flex-1 px-2 py-1.5 text-xs border border-gray-200 rounded focus:outline-none focus:border-blue-400">
|
|
|
- <option value="">无</option>
|
|
|
+ <option value="">不指定</option>
|
|
|
{STYLE_MODULES.filter((m) => m.id !== primaryStyleId).map((m) => <option key={m.id} value={m.id}>{m.name}</option>)}
|
|
|
</select>
|
|
|
</div>
|