Переглянути джерело

refactor: merge style presets into module card selector, remove redundant preset/module duplication

nonvioce 6 місяців тому
батько
коміт
d296902e64
1 змінених файлів з 27 додано та 39 видалено
  1. 27 39
      src/app/features/novel-writer/components/InitWizard.tsx

+ 27 - 39
src/app/features/novel-writer/components/InitWizard.tsx

@@ -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>