Преглед изворни кода

feat: merge Step0 control card and Step1 plot selection, rename hooks to immersion in pipeline UI

Merge Step 0 (ControlCard) and Step 1 (PlotSelection) into a single step where AI
generates 3-5 complete chapter plans as JSON. Plans are shown as expandable selectable
cards; clicking one auto-fills all control card fields. Manual editing remains available.

Pipeline reduced from 7 to 6 steps:
0: 控制卡+剧情 (merged)
1: 埋入代入感 (was 埋入钩子)
2: 生成内容
3: 审核提交
4: 连续性检查
5: 读者反馈

Remove Step1PlotSelection component entirely.
Rename all "钩子" text to "代入感" in pipeline UI components.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nonvioce пре 6 месеци
родитељ
комит
0a180d70ee

+ 56 - 79
src/app/features/novel-writer/components/ChapterDraftPipeline.tsx

@@ -4,7 +4,6 @@ import { useState, useCallback, useEffect } from 'react';
 import { X, Check } from 'lucide-react';
 import { X, Check } from 'lucide-react';
 import { WorldStateFile, WorldLoreFile, WritingConstraint, AntiHomogenizationStrategy, EventNode, ChapterAllocation } from '@/app/lib/novel-types';
 import { WorldStateFile, WorldLoreFile, WritingConstraint, AntiHomogenizationStrategy, EventNode, ChapterAllocation } from '@/app/lib/novel-types';
 import Step0ControlCard from './pipeline/Step0ControlCard';
 import Step0ControlCard from './pipeline/Step0ControlCard';
-import Step1PlotSelection from './pipeline/Step1PlotSelection';
 import Step2EmbedHooks from './pipeline/Step2EmbedHooks';
 import Step2EmbedHooks from './pipeline/Step2EmbedHooks';
 import Step3ContentEditor from './pipeline/Step4ContentEditor';
 import Step3ContentEditor from './pipeline/Step4ContentEditor';
 import Step4ReviewSubmit from './pipeline/Step5ReviewSubmit';
 import Step4ReviewSubmit from './pipeline/Step5ReviewSubmit';
@@ -35,25 +34,22 @@ interface ChapterDraftPipelineProps {
   onCancel: () => void;
   onCancel: () => void;
 }
 }
 
 
-type PipelineStep = 0 | 1 | 2 | 3 | 4 | 5 | 6;
+type PipelineStep = 0 | 1 | 2 | 3 | 4 | 5;
 
 
 interface PipelineState {
 interface PipelineState {
   controlCard: ControlCardData | null;
   controlCard: ControlCardData | null;
-  selectedPlot: string;
-  customNote: string;
   embedHooks: { id: string; type: string; description: string; embedMethod: string }[];
   embedHooks: { id: string; type: string; description: string; embedMethod: string }[];
   manualHooks: string[];
   manualHooks: string[];
   chapterContent: string;
   chapterContent: string;
 }
 }
 
 
 const STEPS = [
 const STEPS = [
-  { id: 0, label: '控制卡' },
-  { id: 1, label: '选择剧情' },
-  { id: 2, label: '埋入钩子' },
-  { id: 3, label: '生成内容' },
-  { id: 4, label: '审核提交' },
-  { id: 5, label: '连续性检查' },
-  { id: 6, label: '读者反馈' },
+  { id: 0, label: '控制卡+剧情' },
+  { id: 1, label: '埋入代入感' },
+  { id: 2, label: '生成内容' },
+  { id: 3, label: '审核提交' },
+  { id: 4, label: '连续性检查' },
+  { id: 5, label: '读者反馈' },
 ] as const;
 ] as const;
 
 
 export default function ChapterDraftPipeline({
 export default function ChapterDraftPipeline({
@@ -80,8 +76,6 @@ export default function ChapterDraftPipeline({
         const parsed = JSON.parse(saved);
         const parsed = JSON.parse(saved);
         return {
         return {
           controlCard: parsed.controlCard || null,
           controlCard: parsed.controlCard || null,
-          selectedPlot: parsed.selectedPlot || '',
-          customNote: parsed.customNote || '',
           embedHooks: parsed.embedHooks || [],
           embedHooks: parsed.embedHooks || [],
           manualHooks: parsed.manualHooks || [],
           manualHooks: parsed.manualHooks || [],
           chapterContent: parsed.chapterContent || '',
           chapterContent: parsed.chapterContent || '',
@@ -90,8 +84,6 @@ export default function ChapterDraftPipeline({
     } catch { /* ignore corrupt data */ }
     } catch { /* ignore corrupt data */ }
     return {
     return {
       controlCard: null,
       controlCard: null,
-      selectedPlot: '',
-      customNote: '',
       embedHooks: [],
       embedHooks: [],
       manualHooks: [],
       manualHooks: [],
       chapterContent: '',
       chapterContent: '',
@@ -144,43 +136,45 @@ export default function ChapterDraftPipeline({
     }).then(r => r.json()).then(d => { if (d.success) setIronRulesContent(d.content as string); }).catch(() => {});
     }).then(r => r.json()).then(d => { if (d.success) setIronRulesContent(d.content as string); }).catch(() => {});
   }, [projectPath, chapterIndex]);
   }, [projectPath, chapterIndex]);
 
 
-  // Step 0 完成(控制卡)
+  // Compute chapter plan summary from control card for downstream steps
+  const chapterPlanSummary = pipelineState.controlCard
+    ? [
+        `本章改变:${pipelineState.controlCard.whatChanges}`,
+        `推进线条:${pipelineState.controlCard.lineToAdvance}`,
+        `情绪债务:${pipelineState.controlCard.debtToReturn}`,
+        `核心冲突:${pipelineState.controlCard.chapterConflict}`,
+        `结尾余波:${pipelineState.controlCard.endingResidue}`,
+        pipelineState.controlCard.workingTitle ? `工作标题:${pipelineState.controlCard.workingTitle}` : '',
+      ].filter(Boolean).join('\n')
+    : '';
+
+  // Step 0 完成(控制卡+剧情,合并原 Step 0 和 Step 1)
   const handleStep0Next = useCallback((data: ControlCardData) => {
   const handleStep0Next = useCallback((data: ControlCardData) => {
     updateState((prev) => ({ ...prev, controlCard: data }));
     updateState((prev) => ({ ...prev, controlCard: data }));
     setCurrentStep(1);
     setCurrentStep(1);
   }, []);
   }, []);
 
 
-  // Step 1 完成
-  const handleStep1Next = useCallback((selectedPlot: string, customNote?: string) => {
-    updateState((prev) => ({
-      ...prev,
-      selectedPlot,
-      customNote: customNote || '',
-    }));
-    setCurrentStep(2);
-  }, []);
-
-  // Step 2 完成(埋入钩子)
-  const handleStep2Next = useCallback(
+  // Step 1 完成(埋入代入感)
+  const handleStep1Next = useCallback(
     (embedHooks: { id: string; type: string; description: string; embedMethod: string }[], manualHooks: string[]) => {
     (embedHooks: { id: string; type: string; description: string; embedMethod: string }[], manualHooks: string[]) => {
       updateState((prev) => ({
       updateState((prev) => ({
         ...prev,
         ...prev,
         embedHooks,
         embedHooks,
         manualHooks,
         manualHooks,
       }));
       }));
-      setCurrentStep(3);
+      setCurrentStep(2);
     },
     },
     []
     []
   );
   );
 
 
-  // Step 3 完成(内容生成,原 Step 4)
-  const handleStep3Next = useCallback((content: string) => {
+  // Step 2 完成(生成内容)
+  const handleStep2Next = useCallback((content: string) => {
     updateState((prev) => ({ ...prev, chapterContent: content }));
     updateState((prev) => ({ ...prev, chapterContent: content }));
-    setCurrentStep(4);
+    setCurrentStep(3);
   }, []);
   }, []);
 
 
-  // Step 4 完成 → 进入连续性检查
-  const handleStep4Complete = useCallback(
+  // Step 3 完成(审核提交)
+  const handleStep3Complete = useCallback(
     (chapterTitle: string) => {
     (chapterTitle: string) => {
       // Save the chapter title into control card for use in final completion
       // Save the chapter title into control card for use in final completion
       updateState((prev) => ({
       updateState((prev) => ({
@@ -189,15 +183,15 @@ export default function ChapterDraftPipeline({
           ? { ...prev.controlCard, workingTitle: prev.controlCard.workingTitle || chapterTitle }
           ? { ...prev.controlCard, workingTitle: prev.controlCard.workingTitle || chapterTitle }
           : { whatChanges: '', lineToAdvance: '', debtToReturn: '', chapterConflict: '', endingResidue: '', chapterTitleCandidates: [], workingTitle: chapterTitle, rawAIResponse: '' },
           : { whatChanges: '', lineToAdvance: '', debtToReturn: '', chapterConflict: '', endingResidue: '', chapterTitleCandidates: [], workingTitle: chapterTitle, rawAIResponse: '' },
       }));
       }));
-      setCurrentStep(5);
+      setCurrentStep(4);
     },
     },
     []
     []
   );
   );
 
 
-  // Step 5 完成 → 条件性进入 Step 6(每5章一次)或直接完成
-  const handleStep5Complete = useCallback(() => {
+  // Step 4 完成(连续性检查)→ 条件性进入 Step 5(每5章一次)或直接完成
+  const handleStep4Complete = useCallback(() => {
     if (chapterIndex % 5 === 0) {
     if (chapterIndex % 5 === 0) {
-      setCurrentStep(6);
+      setCurrentStep(5);
     } else {
     } else {
       sessionStorage.removeItem(storageKey);
       sessionStorage.removeItem(storageKey);
       const title = pipelineState.controlCard?.workingTitle || `第${chapterIndex}章`;
       const title = pipelineState.controlCard?.workingTitle || `第${chapterIndex}章`;
@@ -205,8 +199,8 @@ export default function ChapterDraftPipeline({
     }
     }
   }, [onComplete, pipelineState.chapterContent, pipelineState.controlCard, chapterIndex, storageKey]);
   }, [onComplete, pipelineState.chapterContent, pipelineState.controlCard, chapterIndex, storageKey]);
 
 
-  // Step 6 完成 → 最终提交
-  const handleStep6Complete = useCallback(() => {
+  // Step 5 完成(读者反馈)→ 最终提交
+  const handleStep5Complete = useCallback(() => {
     sessionStorage.removeItem(storageKey);
     sessionStorage.removeItem(storageKey);
     const title = pipelineState.controlCard?.workingTitle || `第${chapterIndex}章`;
     const title = pipelineState.controlCard?.workingTitle || `第${chapterIndex}章`;
     onComplete(title, pipelineState.chapterContent);
     onComplete(title, pipelineState.chapterContent);
@@ -217,11 +211,6 @@ export default function ChapterDraftPipeline({
     onCancel();
     onCancel();
   };
   };
 
 
-  // 构建传给 Step3 的完整剧情描述(含补充说明)
-  const fullPlotDescription = pipelineState.customNote
-    ? `${pipelineState.selectedPlot}\n\n补充说明:${pipelineState.customNote}`
-    : pipelineState.selectedPlot;
-
   return (
   return (
     <div className="fixed inset-0 z-50 bg-white flex flex-col">
     <div className="fixed inset-0 z-50 bg-white flex flex-col">
       {/* 顶部导航栏 */}
       {/* 顶部导航栏 */}
@@ -305,7 +294,7 @@ export default function ChapterDraftPipeline({
       {/* 主内容区域 */}
       {/* 主内容区域 */}
       <div className="flex-1 overflow-hidden">
       <div className="flex-1 overflow-hidden">
         <div className="max-w-4xl mx-auto px-6 py-6 h-full flex flex-col">
         <div className="max-w-4xl mx-auto px-6 py-6 h-full flex flex-col">
-          {/* Step 0: 章节控制卡 */}
+          {/* Step 0: 控制卡+剧情(合并) */}
           {currentStep === 0 && (
           {currentStep === 0 && (
             <Step0ControlCard
             <Step0ControlCard
               projectPath={projectPath}
               projectPath={projectPath}
@@ -317,35 +306,23 @@ export default function ChapterDraftPipeline({
             />
             />
           )}
           )}
 
 
-          {/* Step 1: 剧情选择 */}
+          {/* Step 1: 埋入代入感 */}
           {currentStep === 1 && (
           {currentStep === 1 && (
-            <Step1PlotSelection
-              projectPath={projectPath}
-              apiSettings={apiSettings}
-              recentChaptersCount={recentChaptersCount}
-              savedSelectedPlot={pipelineState.selectedPlot || null}
-              savedCustomNote={pipelineState.customNote}
-              onNext={handleStep1Next}
-            />
-          )}
-
-          {/* Step 2: 埋入钩子 */}
-          {currentStep === 2 && (
             <Step2EmbedHooks
             <Step2EmbedHooks
-              selectedPlot={pipelineState.selectedPlot}
+              chapterPlanSummary={chapterPlanSummary}
               savedEmbedHooks={pipelineState.embedHooks}
               savedEmbedHooks={pipelineState.embedHooks}
               savedManualHooks={pipelineState.manualHooks}
               savedManualHooks={pipelineState.manualHooks}
               apiSettings={apiSettings}
               apiSettings={apiSettings}
-              onNext={handleStep2Next}
-              onBack={() => setCurrentStep(1)}
+              onNext={handleStep1Next}
+              onBack={() => setCurrentStep(0)}
             />
             />
           )}
           )}
 
 
-          {/* Step 3: 内容生成 */}
-          {currentStep === 3 && (
+          {/* Step 2: 生成内容 */}
+          {currentStep === 2 && (
             <Step3ContentEditor
             <Step3ContentEditor
               projectPath={projectPath}
               projectPath={projectPath}
-              selectedPlot={fullPlotDescription}
+              selectedPlot={chapterPlanSummary}
               embedHooks={pipelineState.embedHooks}
               embedHooks={pipelineState.embedHooks}
               manualHooks={pipelineState.manualHooks}
               manualHooks={pipelineState.manualHooks}
               savedChapterContent={pipelineState.chapterContent}
               savedChapterContent={pipelineState.chapterContent}
@@ -356,13 +333,13 @@ export default function ChapterDraftPipeline({
               wordCountMax={wordCountMax}
               wordCountMax={wordCountMax}
               recentChaptersCount={recentChaptersCount}
               recentChaptersCount={recentChaptersCount}
               ironRules={ironRulesContent}
               ironRules={ironRulesContent}
-              onNext={handleStep3Next}
-              onBack={() => setCurrentStep(2)}
+              onNext={handleStep2Next}
+              onBack={() => setCurrentStep(1)}
             />
             />
           )}
           )}
 
 
-          {/* Step 4: 审核提交 */}
-          {currentStep === 4 && (
+          {/* Step 3: 审核提交 */}
+          {currentStep === 3 && (
             <Step4ReviewSubmit
             <Step4ReviewSubmit
               projectPath={projectPath}
               projectPath={projectPath}
               chapterContent={pipelineState.chapterContent}
               chapterContent={pipelineState.chapterContent}
@@ -372,33 +349,33 @@ export default function ChapterDraftPipeline({
               loreFiles={loreFiles}
               loreFiles={loreFiles}
               embedHooks={pipelineState.embedHooks}
               embedHooks={pipelineState.embedHooks}
               manualHooks={pipelineState.manualHooks}
               manualHooks={pipelineState.manualHooks}
-              onComplete={handleStep4Complete}
-              onBack={() => setCurrentStep(3)}
+              onComplete={handleStep3Complete}
+              onBack={() => setCurrentStep(2)}
             />
             />
           )}
           )}
 
 
-          {/* Step 5: 连续性检查 */}
-          {currentStep === 5 && (
+          {/* Step 4: 连续性检查 */}
+          {currentStep === 4 && (
             <Step5ContinuityCheck
             <Step5ContinuityCheck
               projectPath={projectPath}
               projectPath={projectPath}
               chapterIndex={chapterIndex}
               chapterIndex={chapterIndex}
               chapterContent={pipelineState.chapterContent}
               chapterContent={pipelineState.chapterContent}
               apiSettings={apiSettings}
               apiSettings={apiSettings}
               chapterEvents={chapterEvents}
               chapterEvents={chapterEvents}
-              onComplete={handleStep5Complete}
-              onBack={() => setCurrentStep(4)}
+              onComplete={handleStep4Complete}
+              onBack={() => setCurrentStep(3)}
             />
             />
           )}
           )}
 
 
-          {/* Step 6: 读者反馈 */}
-          {currentStep === 6 && (
+          {/* Step 5: 读者反馈 */}
+          {currentStep === 5 && (
             <Step6ReaderFeedback
             <Step6ReaderFeedback
               projectPath={projectPath}
               projectPath={projectPath}
               chapterIndex={chapterIndex}
               chapterIndex={chapterIndex}
               chapterContent={pipelineState.chapterContent}
               chapterContent={pipelineState.chapterContent}
               apiSettings={apiSettings}
               apiSettings={apiSettings}
-              onComplete={handleStep6Complete}
-              onSkip={handleStep6Complete}
+              onComplete={handleStep5Complete}
+              onSkip={handleStep5Complete}
             />
             />
           )}
           )}
         </div>
         </div>

+ 157 - 27
src/app/features/novel-writer/components/pipeline/Step0ControlCard.tsx

@@ -1,8 +1,7 @@
 'use client';
 'use client';
 
 
 import { useState } from 'react';
 import { useState } from 'react';
-import { Sparkles, ArrowRight } from 'lucide-react';
-import { buildControlCardPrompt } from '@/app/lib/chapter-prompts';
+import { Sparkles, ArrowRight, ChevronDown, ChevronUp } from 'lucide-react';
 import type { EventNode } from '@/app/lib/novel-types';
 import type { EventNode } from '@/app/lib/novel-types';
 
 
 interface ApiSettings {
 interface ApiSettings {
@@ -31,6 +30,16 @@ export interface ControlCardData {
   rawAIResponse: string;
   rawAIResponse: string;
 }
 }
 
 
+interface ChapterPlan {
+  summary: string;
+  whatChanges: string;
+  lineToAdvance: string;
+  debtToReturn: string;
+  chapterConflict: string;
+  endingResidue: string;
+  titleCandidates: string[];
+}
+
 export default function Step0ControlCard({
 export default function Step0ControlCard({
   projectPath,
   projectPath,
   chapterIndex,
   chapterIndex,
@@ -50,10 +59,17 @@ export default function Step0ControlCard({
   const [generating, setGenerating] = useState(false);
   const [generating, setGenerating] = useState(false);
   const [error, setError] = useState<string | null>(null);
   const [error, setError] = useState<string | null>(null);
 
 
+  // Chapter plans state for merged Step 0+1
+  const [chapterPlans, setChapterPlans] = useState<ChapterPlan[]>([]);
+  const [selectedPlanIndex, setSelectedPlanIndex] = useState<number | null>(null);
+  const [expandedPlanIndex, setExpandedPlanIndex] = useState<number | null>(null);
+
   const handleGenerate = async () => {
   const handleGenerate = async () => {
     if (!apiSettings) return;
     if (!apiSettings) return;
     setGenerating(true);
     setGenerating(true);
     setError(null);
     setError(null);
+    setChapterPlans([]);
+    setSelectedPlanIndex(null);
 
 
     try {
     try {
       // Read project context files via API
       // Read project context files via API
@@ -92,7 +108,34 @@ export default function Step0ControlCard({
         ).join('\n');
         ).join('\n');
       }
       }
 
 
-      const prompt = buildControlCardPrompt(chapterIndex, projectContext);
+      // Build prompt for 3-5 complete chapter plans in JSON format
+      const prompt = `你是一部长篇小说的创作总控。请在动笔前,基于以下项目文档为第${chapterIndex}章生成 3-5 个完整的章节创作方案。
+
+## 项目文档
+${projectContext || '(项目文档不可用)'}
+
+## 要求
+为本章生成 3-5 个不同的章节方案。每个方案是一个完整的控制卡,包含以下字段:
+- summary: 本章剧情/方向的一句话概述(每个方案的核心标识)
+- whatChanges: 本章结束时什么状态必须和开头不同?(人物状态/关系/情节压力的变化)
+- lineToAdvance: 本章推进哪条线?(主线/支线/暗线)
+- debtToReturn: 回归什么情绪债务?(前文留下的什么承诺本章要兑现)
+- chapterConflict: 核心冲突是什么?(不是"谁打谁",而是"什么在逼人物做选择")
+- endingResidue: 结尾留下什么未解决的问题或悬念?
+- titleCandidates: 3-5 个候选标题
+
+请直接返回一个 JSON 数组,不要包含其他文字。格式:
+[
+  {
+    "summary": "...",
+    "whatChanges": "...",
+    "lineToAdvance": "...",
+    "debtToReturn": "...",
+    "chapterConflict": "...",
+    "endingResidue": "...",
+    "titleCandidates": ["标题1", "标题2", "标题3"]
+  }
+]`;
 
 
       const requestBody = {
       const requestBody = {
         model: apiSettings.model,
         model: apiSettings.model,
@@ -126,35 +169,45 @@ export default function Step0ControlCard({
 
 
       setRawResponse(content);
       setRawResponse(content);
 
 
-      // Parse response
-      const changeMatch = content.match(/【本章改变】\s*([\s\S]*?)(?=【推进线条】|$)/);
-      const lineMatch = content.match(/【推进线条】\s*([\s\S]*?)(?=【情绪债务】|$)/);
-      const debtMatch = content.match(/【情绪债务】\s*([\s\S]*?)(?=【核心冲突】|$)/);
-      const conflictMatch = content.match(/【核心冲突】\s*([\s\S]*?)(?=【结尾余波】|$)/);
-      const residueMatch = content.match(/【结尾余波】\s*([\s\S]*?)(?=【候选标题】|$)/);
-      const titleMatch = content.match(/【候选标题】\s*([\s\S]*?)$/);
-
-      if (changeMatch?.[1]) setWhatChanges(changeMatch[1].trim());
-      if (lineMatch?.[1]) setLineToAdvance(lineMatch[1].trim());
-      if (debtMatch?.[1]) setDebtToReturn(debtMatch[1].trim());
-      if (conflictMatch?.[1]) setChapterConflict(conflictMatch[1].trim());
-      if (residueMatch?.[1]) setEndingResidue(residueMatch[1].trim());
-      if (titleMatch?.[1]) {
-        const titles = titleMatch[1]
-          .split('\n')
-          .map((t: string) => t.replace(/^\d+\.\s*/, '').trim())
-          .filter((t: string) => t.length > 0);
-        setCandidates(titles);
+      // Parse JSON response for chapter plans
+      const jsonMatch = content.match(/\[[\s\S]*\]/);
+      if (!jsonMatch) throw new Error('AI 返回格式不是有效的 JSON 数组');
+
+      const plans: ChapterPlan[] = JSON.parse(jsonMatch[0]);
+      if (!Array.isArray(plans) || plans.length === 0) {
+        throw new Error('AI 返回的章节方案格式不正确');
       }
       }
+
+      setChapterPlans(plans);
+
+      // Auto-select the first plan and fill fields
+      applyPlan(plans[0], 0);
     } catch (err) {
     } catch (err) {
       const msg = err instanceof Error ? err.message : '未知错误';
       const msg = err instanceof Error ? err.message : '未知错误';
-      console.error('控制卡生成失败:', msg, err);
+      console.error('章节方案生成失败:', msg, err);
       setError(msg);
       setError(msg);
     } finally {
     } finally {
       setGenerating(false);
       setGenerating(false);
     }
     }
   };
   };
 
 
+  const applyPlan = (plan: ChapterPlan, index: number) => {
+    setSelectedPlanIndex(index);
+    setWhatChanges(plan.whatChanges);
+    setLineToAdvance(plan.lineToAdvance);
+    setDebtToReturn(plan.debtToReturn);
+    setChapterConflict(plan.chapterConflict);
+    setEndingResidue(plan.endingResidue);
+    setCandidates(plan.titleCandidates);
+    if (plan.titleCandidates.length > 0 && !workingTitle) {
+      setWorkingTitle(plan.titleCandidates[0]);
+    }
+  };
+
+  const toggleExpand = (index: number) => {
+    setExpandedPlanIndex(expandedPlanIndex === index ? null : index);
+  };
+
   const handleNext = () => {
   const handleNext = () => {
     onNext({
     onNext({
       whatChanges,
       whatChanges,
@@ -172,9 +225,9 @@ export default function Step0ControlCard({
     <div className="flex-1 overflow-y-auto">
     <div className="flex-1 overflow-y-auto">
       <div className="space-y-4 max-w-2xl mx-auto">
       <div className="space-y-4 max-w-2xl mx-auto">
         <div className="text-center mb-4">
         <div className="text-center mb-4">
-          <h2 className="text-lg font-semibold text-gray-800">第{chapterIndex}章 控制卡</h2>
+          <h2 className="text-lg font-semibold text-gray-800">第{chapterIndex}章 控制卡+剧情</h2>
           <p className="text-sm text-gray-500 mt-1">
           <p className="text-sm text-gray-500 mt-1">
-            动笔前明确本章的目标和方向。可手动填写或让 AI 辅助生成。
+            AI 将生成 3-5 个完整的章节方案供您选择,也可手动填写
           </p>
           </p>
         </div>
         </div>
 
 
@@ -187,12 +240,89 @@ export default function Step0ControlCard({
               hover:bg-indigo-700 transition-colors disabled:opacity-50"
               hover:bg-indigo-700 transition-colors disabled:opacity-50"
           >
           >
             <Sparkles className="h-4 w-4" />
             <Sparkles className="h-4 w-4" />
-            {generating ? 'AI 生成中...' : 'AI 生成控制卡'}
+            {generating ? 'AI 生成中...' : 'AI 生成章节方案'}
           </button>
           </button>
         </div>
         </div>
 
 
         {error && <div className="bg-red-50 border border-red-200 rounded-md p-3 text-sm text-red-600">{error}</div>}
         {error && <div className="bg-red-50 border border-red-200 rounded-md p-3 text-sm text-red-600">{error}</div>}
 
 
+        {/* Chapter Plans - Selectable Cards */}
+        {chapterPlans.length > 0 && (
+          <div className="space-y-2">
+            <h3 className="text-sm font-medium text-gray-700">AI 生成的章节方案(点击选择)</h3>
+            {chapterPlans.map((plan, index) => (
+              <div
+                key={index}
+                className={`border-2 rounded-xl transition-all overflow-hidden ${
+                  selectedPlanIndex === index
+                    ? 'border-indigo-500 bg-indigo-50'
+                    : 'border-gray-200 bg-white hover:border-indigo-300'
+                }`}
+              >
+                {/* Card header - click to select */}
+                <div
+                  className="flex items-start gap-3 p-3 cursor-pointer"
+                  onClick={() => applyPlan(plan, index)}
+                >
+                  <div className={`mt-1 w-4 h-4 rounded-full border-2 flex-shrink-0 ${
+                    selectedPlanIndex === index
+                      ? 'border-indigo-600 bg-indigo-600'
+                      : 'border-gray-300'
+                  }`}>
+                    {selectedPlanIndex === index && (
+                      <div className="w-full h-full flex items-center justify-center">
+                        <div className="w-1.5 h-1.5 rounded-full bg-white" />
+                      </div>
+                    )}
+                  </div>
+                  <div className="flex-1 min-w-0">
+                    <div className="flex items-center justify-between gap-2">
+                      <span className="text-sm font-medium text-gray-800">方案 {index + 1}</span>
+                      <button
+                        onClick={(e) => { e.stopPropagation(); toggleExpand(index); }}
+                        className="flex-shrink-0 text-gray-400 hover:text-gray-600"
+                      >
+                        {expandedPlanIndex === index ? (
+                          <ChevronUp className="h-4 w-4" />
+                        ) : (
+                          <ChevronDown className="h-4 w-4" />
+                        )}
+                      </button>
+                    </div>
+                    <p className="text-sm text-gray-600 mt-1">{plan.summary}</p>
+                  </div>
+                  <button
+                    onClick={(e) => { e.stopPropagation(); applyPlan(plan, index); }}
+                    className={`flex-shrink-0 px-3 py-1 text-xs rounded-lg transition-colors ${
+                      selectedPlanIndex === index
+                        ? 'bg-indigo-600 text-white'
+                        : 'bg-gray-100 text-gray-600 hover:bg-indigo-100 hover:text-indigo-700'
+                    }`}
+                  >
+                    {selectedPlanIndex === index ? '已选择' : '选择'}
+                  </button>
+                </div>
+
+                {/* Expanded details */}
+                {expandedPlanIndex === index && (
+                  <div className="px-3 pb-3 pt-0 space-y-2 border-t border-gray-100">
+                    <div className="mt-2 text-xs text-gray-500">
+                      <p><span className="font-medium text-gray-700">本章改变:</span>{plan.whatChanges}</p>
+                      <p className="mt-1"><span className="font-medium text-gray-700">推进线条:</span>{plan.lineToAdvance}</p>
+                      <p className="mt-1"><span className="font-medium text-gray-700">情绪债务:</span>{plan.debtToReturn}</p>
+                      <p className="mt-1"><span className="font-medium text-gray-700">核心冲突:</span>{plan.chapterConflict}</p>
+                      <p className="mt-1"><span className="font-medium text-gray-700">结尾余波:</span>{plan.endingResidue}</p>
+                      <p className="mt-1"><span className="font-medium text-gray-700">候选题名:</span>
+                        {plan.titleCandidates.join(' / ')}
+                      </p>
+                    </div>
+                  </div>
+                )}
+              </div>
+            ))}
+          </div>
+        )}
+
         {/* Control Card Fields */}
         {/* Control Card Fields */}
         <div className="space-y-3">
         <div className="space-y-3">
           <Field label="本章必须改变什么" value={whatChanges} onChange={setWhatChanges}
           <Field label="本章必须改变什么" value={whatChanges} onChange={setWhatChanges}
@@ -203,7 +333,7 @@ export default function Step0ControlCard({
             placeholder="前文留下的什么承诺本章要兑现?" />
             placeholder="前文留下的什么承诺本章要兑现?" />
           <Field label="核心冲突" value={chapterConflict} onChange={setChapterConflict}
           <Field label="核心冲突" value={chapterConflict} onChange={setChapterConflict}
             placeholder="什么在逼人物做选择?" />
             placeholder="什么在逼人物做选择?" />
-          <Field label="结尾余波/钩子" value={endingResidue} onChange={setEndingResidue}
+          <Field label="结尾余波/代入感" value={endingResidue} onChange={setEndingResidue}
             placeholder="本章结束时留下什么未解决的问题?" />
             placeholder="本章结束时留下什么未解决的问题?" />
         </div>
         </div>
 
 

+ 0 - 296
src/app/features/novel-writer/components/pipeline/Step1PlotSelection.tsx

@@ -1,296 +0,0 @@
-'use client';
-
-import { useState, useCallback } from 'react';
-import { RefreshCw, ChevronRight, PenLine, Loader2, AlertCircle } from 'lucide-react';
-
-interface ApiSettings {
-  apiUrl: string;
-  apiKey: string;
-  model: string;
-}
-
-interface Step1PlotSelectionProps {
-  projectPath: string;
-  apiSettings: ApiSettings | null;
-  recentChaptersCount: number;
-  savedSelectedPlot: string | null;
-  savedCustomNote: string;
-  onNext: (selectedPlot: string, customNote?: string) => void;
-}
-
-interface PlotOption {
-  id: number;
-  description: string;
-}
-
-export default function Step1PlotSelection({
-  projectPath,
-  apiSettings,
-  recentChaptersCount,
-  savedSelectedPlot,
-  savedCustomNote,
-  onNext,
-}: Step1PlotSelectionProps) {
-  const [plotOptions, setPlotOptions] = useState<PlotOption[]>([]);
-  const [isGenerating, setIsGenerating] = useState(false);
-  const [error, setError] = useState<string | null>(null);
-  const [selectedPlot, setSelectedPlot] = useState<string | null>(savedSelectedPlot);
-  const [customNote, setCustomNote] = useState(savedCustomNote);
-  const [isManualMode, setIsManualMode] = useState(savedSelectedPlot !== null && !plotOptions.some(p => p.description === savedSelectedPlot));
-  const [manualPlot, setManualPlot] = useState(savedSelectedPlot && !plotOptions.some(p => p.description === savedSelectedPlot) ? savedSelectedPlot : '');
-  const [tokenUsage, setTokenUsage] = useState<string | null>(null);
-
-  const generatePlotOptions = useCallback(async () => {
-    if (!apiSettings) {
-      setError('请先配置 API 设置');
-      return;
-    }
-
-    setIsGenerating(true);
-    setError(null);
-    setSelectedPlot(null);
-    setTokenUsage(null);
-
-    try {
-      // 读取世界状态文件作为上下文
-      const filesResponse = await fetch(
-        `/api/novel/files?projectPath=${encodeURIComponent(projectPath)}&type=all`
-      );
-      const filesData = await filesResponse.json();
-
-      let contextPrefix = '';
-      if (filesData.success && Array.isArray(filesData.files)) {
-        const enabledFiles = filesData.files.filter(
-          (f: { enabled: boolean; content: string }) => f.enabled && f.content?.trim()
-        );
-        if (enabledFiles.length > 0) {
-          contextPrefix = '[小说世界状态]\n';
-          for (const file of enabledFiles) {
-            contextPrefix += `=== ${file.fileName} ===\n${file.content}\n`;
-          }
-          contextPrefix += '[上下文结束]\n\n';
-        }
-      }
-
-      const prompt = `${contextPrefix}请为小说生成 5 个下一章的剧情方向选项。每个选项包含 100-200 字的描述,涵盖主要事件、涉及角色、预期情感基调。请以 JSON 数组格式返回,格式为:[{"id": 1, "description": "..."}]`;
-
-      const response = await fetch('/api/proxy', {
-        method: 'POST',
-        headers: { 'Content-Type': 'application/json' },
-        body: JSON.stringify({
-          targetUrl: apiSettings.apiUrl,
-          headers: { Authorization: `Bearer ${apiSettings.apiKey}` },
-          body: {
-            model: apiSettings.model,
-            messages: [{ role: 'user', content: prompt }],
-            max_tokens: 2000,
-          },
-        }),
-      });
-
-      if (!response.ok) {
-        throw new Error(`AI 请求失败:HTTP ${response.status}`);
-      }
-
-      const data = await response.json();
-      const content = data.choices?.[0]?.message?.content;
-      if (!content) throw new Error('AI 响应格式异常');
-
-      if (data?.usage?.total_tokens) {
-        setTokenUsage(
-          `Token 消耗:输入 ${data.usage.prompt_tokens ?? 0} · 输出 ${data.usage.completion_tokens ?? 0} · 总计 ${data.usage.total_tokens}`
-        );
-      }
-
-      // 解析 JSON 数组
-      const jsonMatch = content.match(/\[[\s\S]*\]/);
-      if (!jsonMatch) throw new Error('无法解析 AI 返回的剧情选项');
-
-      const parsed: PlotOption[] = JSON.parse(jsonMatch[0]);
-      if (!Array.isArray(parsed) || parsed.length === 0) {
-        throw new Error('AI 返回的剧情选项格式不正确');
-      }
-
-      setPlotOptions(parsed);
-    } catch (err) {
-      setError(err instanceof Error ? err.message : '生成剧情选项失败,请重试');
-    } finally {
-      setIsGenerating(false);
-    }
-  }, [apiSettings, projectPath, recentChaptersCount]);
-
-  const handleSelectPlot = (description: string) => {
-    setSelectedPlot(description);
-    setIsManualMode(false);
-  };
-
-  const handleNext = () => {
-    if (isManualMode) {
-      if (!manualPlot.trim()) return;
-      onNext(manualPlot.trim(), customNote.trim() || undefined);
-    } else {
-      if (!selectedPlot) return;
-      onNext(selectedPlot, customNote.trim() || undefined);
-    }
-  };
-
-  const canProceed = isManualMode ? manualPlot.trim().length > 0 : selectedPlot !== null;
-
-  return (
-    <div className="flex flex-col h-full">
-      {/* 顶部操作栏 */}
-      <div className="flex items-center justify-between mb-6">
-        <div>
-          <h2 className="text-xl font-semibold text-gray-900">选择剧情方向</h2>
-          <p className="text-sm text-gray-500 mt-1">
-            AI 将根据当前世界状态生成 5 个剧情方向,选择一个或自行输入
-          </p>
-        </div>
-        <div className="flex gap-2">
-          <button
-            onClick={() => {
-              setIsManualMode(true);
-              setSelectedPlot(null);
-            }}
-            className="flex items-center gap-1.5 px-3 py-2 text-sm text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
-          >
-            <PenLine className="h-4 w-4" />
-            自行输入
-          </button>
-          <button
-            onClick={generatePlotOptions}
-            disabled={isGenerating || !apiSettings}
-            className="flex items-center gap-1.5 px-4 py-2 text-sm bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
-          >
-            {isGenerating ? (
-              <Loader2 className="h-4 w-4 animate-spin" />
-            ) : (
-              <RefreshCw className="h-4 w-4" />
-            )}
-            {plotOptions.length > 0 ? '重新生成' : '生成剧情选项'}
-          </button>
-        </div>
-      </div>
-
-      {/* 错误提示 */}
-      {error && (
-        <div className="flex items-center gap-2 p-3 mb-4 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
-          <AlertCircle className="h-4 w-4 flex-shrink-0" />
-          {error}
-        </div>
-      )}
-
-      {tokenUsage && (
-        <p className="text-[11px] text-gray-500 mb-3">{tokenUsage}</p>
-      )}
-
-      {/* 手动输入模式 */}
-      {isManualMode && (
-        <div className="mb-6 p-4 border-2 border-indigo-300 rounded-xl bg-indigo-50">
-          <label className="block text-sm font-medium text-gray-700 mb-2">
-            手动输入剧情方向
-          </label>
-          <textarea
-            value={manualPlot}
-            onChange={(e) => setManualPlot(e.target.value)}
-            placeholder="请描述本章的剧情方向,包括主要事件、涉及角色、预期情感基调..."
-            className="w-full h-32 px-3 py-2 text-sm border border-gray-300 rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-indigo-500 bg-white"
-          />
-          <button
-            onClick={() => setIsManualMode(false)}
-            className="mt-2 text-xs text-gray-500 hover:text-gray-700 underline"
-          >
-            返回 AI 生成选项
-          </button>
-        </div>
-      )}
-
-      {/* 生成中占位 */}
-      {isGenerating && (
-        <div className="flex-1 flex items-center justify-center">
-          <div className="text-center">
-            <Loader2 className="h-8 w-8 animate-spin text-indigo-500 mx-auto mb-3" />
-            <p className="text-gray-500 text-sm">正在生成剧情方向选项...</p>
-          </div>
-        </div>
-      )}
-
-      {/* 剧情选项卡片列表 */}
-      {!isGenerating && plotOptions.length > 0 && !isManualMode && (
-        <div className="flex-1 overflow-y-auto space-y-3 pr-1">
-          {plotOptions.map((option) => (
-            <div
-              key={option.id}
-              onClick={() => handleSelectPlot(option.description)}
-              className={`p-4 border-2 rounded-xl cursor-pointer transition-all ${
-                selectedPlot === option.description
-                  ? 'border-indigo-500 bg-indigo-50 shadow-sm'
-                  : 'border-gray-200 bg-white hover:border-indigo-300 hover:bg-gray-50'
-              }`}
-            >
-              <div className="flex items-start justify-between gap-3">
-                <div className="flex-1">
-                  <span className="inline-block text-xs font-semibold text-indigo-600 bg-indigo-100 px-2 py-0.5 rounded mb-2">
-                    选项 {option.id}
-                  </span>
-                  <p className="text-sm text-gray-700 leading-relaxed">{option.description}</p>
-                </div>
-                <button
-                  onClick={(e) => {
-                    e.stopPropagation();
-                    handleSelectPlot(option.description);
-                  }}
-                  className={`flex-shrink-0 px-3 py-1.5 text-xs rounded-lg transition-colors ${
-                    selectedPlot === option.description
-                      ? 'bg-indigo-600 text-white'
-                      : 'bg-gray-100 text-gray-600 hover:bg-indigo-100 hover:text-indigo-700'
-                  }`}
-                >
-                  {selectedPlot === option.description ? '已选择' : '选择此剧情'}
-                </button>
-              </div>
-            </div>
-          ))}
-        </div>
-      )}
-
-      {/* 空状态提示 */}
-      {!isGenerating && plotOptions.length === 0 && !isManualMode && (
-        <div className="flex-1 flex items-center justify-center">
-          <div className="text-center text-gray-400">
-            <div className="text-4xl mb-3">📖</div>
-              <p className="text-sm">点击{'"'}生成剧情选项{'"'}让 AI 为您提供剧情方向建议</p>
-            <p className="text-xs mt-1">或点击{'"'}自行输入{'"'}手动描述剧情方向</p>
-          </div>
-        </div>
-      )}
-
-      {/* 底部:补充说明 + 下一步 */}
-      {canProceed && (
-        <div className="mt-4 pt-4 border-t border-gray-200">
-          <div className="mb-3">
-            <label className="block text-sm font-medium text-gray-700 mb-1">
-              补充说明(可选)
-            </label>
-            <input
-              type="text"
-              value={customNote}
-              onChange={(e) => setCustomNote(e.target.value)}
-              placeholder="如:但要加入一个意外转折、保持悬疑氛围..."
-              className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
-            />
-          </div>
-          <div className="flex justify-end">
-            <button
-              onClick={handleNext}
-              className="flex items-center gap-2 px-6 py-2.5 bg-indigo-600 text-white text-sm font-medium rounded-lg hover:bg-indigo-700 transition-colors"
-            >
-              下一步:选择钩子
-              <ChevronRight className="h-4 w-4" />
-            </button>
-          </div>
-        </div>
-      )}
-    </div>
-  );
-}

+ 15 - 15
src/app/features/novel-writer/components/pipeline/Step2EmbedHooks.tsx

@@ -17,7 +17,7 @@ interface HookOption {
 }
 }
 
 
 interface Step2EmbedHooksProps {
 interface Step2EmbedHooksProps {
-  selectedPlot: string;
+  chapterPlanSummary: string;
   savedEmbedHooks: HookOption[];
   savedEmbedHooks: HookOption[];
   savedManualHooks: string[];
   savedManualHooks: string[];
   apiSettings: ApiSettings | null;
   apiSettings: ApiSettings | null;
@@ -26,7 +26,7 @@ interface Step2EmbedHooksProps {
 }
 }
 
 
 export default function Step2EmbedHooks({
 export default function Step2EmbedHooks({
-  selectedPlot,
+  chapterPlanSummary,
   savedEmbedHooks,
   savedEmbedHooks,
   savedManualHooks,
   savedManualHooks,
   apiSettings,
   apiSettings,
@@ -52,15 +52,15 @@ export default function Step2EmbedHooks({
     setTokenUsage(null);
     setTokenUsage(null);
 
 
     try {
     try {
-      const prompt = `根据以下选定的剧情方向,为本章生成 5-8 个代入感候选项。
+      const prompt = `根据以下选定的章节方案,为本章生成 5-8 个代入感候选项。
 
 
-选定剧情方向:
-${selectedPlot}
+选定章节方案:
+${chapterPlanSummary}
 
 
 请生成 5-8 个本章可以埋入的代入感设计,每个包含:类型(悬念/作者读者信息差/读者主角信息差)、描述、预期在本章中的埋入方式。
 请生成 5-8 个本章可以埋入的代入感设计,每个包含:类型(悬念/作者读者信息差/读者主角信息差)、描述、预期在本章中的埋入方式。
 
 
 请以 JSON 数组格式返回,格式为:
 请以 JSON 数组格式返回,格式为:
-[{"id": "hook-1", "type": "悬念", "description": "...", "embedMethod": "..."}]`;
+[{"id": "immersion-1", "type": "悬念", "description": "...", "embedMethod": "..."}]`;
 
 
       const response = await fetch('/api/proxy', {
       const response = await fetch('/api/proxy', {
         method: 'POST',
         method: 'POST',
@@ -89,21 +89,21 @@ ${selectedPlot}
       }
       }
 
 
       const jsonMatch = content.match(/\[[\s\S]*\]/);
       const jsonMatch = content.match(/\[[\s\S]*\]/);
-      if (!jsonMatch) throw new Error('无法解析 AI 返回的钩子选项');
+      if (!jsonMatch) throw new Error('无法解析 AI 返回的代入感选项');
 
 
       const parsed: HookOption[] = JSON.parse(jsonMatch[0]);
       const parsed: HookOption[] = JSON.parse(jsonMatch[0]);
       if (!Array.isArray(parsed) || parsed.length === 0) {
       if (!Array.isArray(parsed) || parsed.length === 0) {
-        throw new Error('AI 返回的钩子选项格式不正确');
+        throw new Error('AI 返回的代入感选项格式不正确');
       }
       }
 
 
       setHookOptions(parsed);
       setHookOptions(parsed);
       setSelectedHookIds(new Set(parsed.map(h => h.id)));
       setSelectedHookIds(new Set(parsed.map(h => h.id)));
     } catch (err) {
     } catch (err) {
-      setError(err instanceof Error ? err.message : '生成钩子选项失败,请重试');
+      setError(err instanceof Error ? err.message : '生成代入感选项失败,请重试');
     } finally {
     } finally {
       setIsGenerating(false);
       setIsGenerating(false);
     }
     }
-  }, [apiSettings, selectedPlot]);
+  }, [apiSettings, chapterPlanSummary]);
 
 
   const toggleHook = (id: string) => {
   const toggleHook = (id: string) => {
     setSelectedHookIds((prev) => {
     setSelectedHookIds((prev) => {
@@ -163,7 +163,7 @@ ${selectedPlot}
           ) : (
           ) : (
             <RefreshCw className="h-4 w-4" />
             <RefreshCw className="h-4 w-4" />
           )}
           )}
-          {hookOptions.length > 0 ? '重新生成' : '生成钩子选项'}
+          {hookOptions.length > 0 ? '重新生成' : '生成代入感选项'}
         </button>
         </button>
       </div>
       </div>
 
 
@@ -249,7 +249,7 @@ ${selectedPlot}
           <div className="flex items-center justify-center py-12">
           <div className="flex items-center justify-center py-12">
             <div className="text-center">
             <div className="text-center">
               <Loader2 className="h-8 w-8 animate-spin text-indigo-500 mx-auto mb-3" />
               <Loader2 className="h-8 w-8 animate-spin text-indigo-500 mx-auto mb-3" />
-              <p className="text-gray-500 text-sm">正在生成钩子选项...</p>
+              <p className="text-gray-500 text-sm">正在生成代入感选项...</p>
             </div>
             </div>
           </div>
           </div>
         )}
         )}
@@ -258,7 +258,7 @@ ${selectedPlot}
           <div className="flex items-center justify-center py-8">
           <div className="flex items-center justify-center py-8">
             <div className="text-center text-gray-400">
             <div className="text-center text-gray-400">
               <div className="text-3xl mb-2">🪝</div>
               <div className="text-3xl mb-2">🪝</div>
-              <p className="text-sm">点击{'"'}生成钩子选项{'"'}让 AI 为本章推荐代入感设计</p>
+              <p className="text-sm">点击{'生成代入感选项'}让 AI 为本章推荐代入感设计</p>
             </div>
             </div>
           </div>
           </div>
         )}
         )}
@@ -271,7 +271,7 @@ ${selectedPlot}
               value={manualHook}
               value={manualHook}
               onChange={(e) => setManualHook(e.target.value)}
               onChange={(e) => setManualHook(e.target.value)}
               onKeyDown={(e) => e.key === 'Enter' && addManualHook()}
               onKeyDown={(e) => e.key === 'Enter' && addManualHook()}
-              placeholder="输入自定义钩子描述,按 Enter 或点击添加..."
+              placeholder="输入自定义代入感描述,按 Enter 或点击添加..."
               className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
               className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500"
             />
             />
             <button
             <button
@@ -325,4 +325,4 @@ ${selectedPlot}
       </div>
       </div>
     </div>
     </div>
   );
   );
-}
+}

+ 6 - 6
src/app/features/novel-writer/components/pipeline/Step3ResolveHooks.tsx

@@ -57,9 +57,9 @@ export default function Step3ResolveHooks({
   return (
   return (
     <div className="flex flex-col h-full">
     <div className="flex flex-col h-full">
       <div className="mb-6">
       <div className="mb-6">
-        <h2 className="text-xl font-semibold text-gray-900">选择解决的钩子</h2>
+        <h2 className="text-xl font-semibold text-gray-900">选择解决的代入感</h2>
         <p className="text-sm text-gray-500 mt-1">
         <p className="text-sm text-gray-500 mt-1">
-          选择本章要解决的之前埋入的历史钩子(可选)
+          选择本章要解决的之前埋入的历史代入感(可选)
         </p>
         </p>
       </div>
       </div>
 
 
@@ -68,7 +68,7 @@ export default function Step3ResolveHooks({
           <div>
           <div>
             <div className="flex items-center justify-between mb-3">
             <div className="flex items-center justify-between mb-3">
               <h3 className="text-sm font-medium text-gray-700">
               <h3 className="text-sm font-medium text-gray-700">
-                历史未解决钩子({resolveHookIds.size}/{openHooks.length} 已选)
+                历史未解决代入感({resolveHookIds.size}/{openHooks.length} 已选)
               </h3>
               </h3>
               <div className="flex gap-2">
               <div className="flex gap-2">
                 <button
                 <button
@@ -133,7 +133,7 @@ export default function Step3ResolveHooks({
           <div className="flex items-center justify-center py-12">
           <div className="flex items-center justify-center py-12">
             <div className="text-center text-gray-400">
             <div className="text-center text-gray-400">
               <div className="text-3xl mb-2">✅</div>
               <div className="text-3xl mb-2">✅</div>
-              <p className="text-sm">当前没有需要解决的历史钩子</p>
+              <p className="text-sm">当前没有需要解决的历史代入感</p>
             </div>
             </div>
           </div>
           </div>
         )}
         )}
@@ -145,10 +145,10 @@ export default function Step3ResolveHooks({
           className="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
           className="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
         >
         >
           <ChevronLeft className="h-4 w-4" />
           <ChevronLeft className="h-4 w-4" />
-          上一步:选择埋入的钩子
+          上一步:选择埋入的代入感
         </button>
         </button>
         <div className="text-xs text-gray-400">
         <div className="text-xs text-gray-400">
-          {resolveHookIds.size > 0 ? `将解决 ${resolveHookIds.size} 个历史钩子` : '不解决任何历史钩子'}
+          {resolveHookIds.size > 0 ? `将解决 ${resolveHookIds.size} 个历史代入感` : '不解决任何历史代入感'}
         </div>
         </div>
         <button
         <button
           onClick={handleNext}
           onClick={handleNext}

+ 5 - 5
src/app/features/novel-writer/components/pipeline/Step4ContentEditor.tsx

@@ -140,11 +140,11 @@ export default function Step4ContentEditor({
         .join('\n');
         .join('\n');
 
 
       const embedHooksText = embedHooks.length > 0
       const embedHooksText = embedHooks.length > 0
-        ? `\n本章需要埋入的叙事钩子:\n${embedHooks.map((h) => `- [${h.type}] ${h.description}(埋入方式:${h.embedMethod})`).join('\n')}`
+        ? `\n本章需要埋入的代入感设计:\n${embedHooks.map((h) => `- [${h.type}] ${h.description}(埋入方式:${h.embedMethod})`).join('\n')}`
         : '';
         : '';
 
 
       const manualHooksText = manualHooks.length > 0
       const manualHooksText = manualHooks.length > 0
-        ? `\n手动添加的叙事钩子:\n${manualHooks.map((h) => `- ${h}`).join('\n')}`
+        ? `\n手动添加的代入感设计:\n${manualHooks.map((h) => `- ${h}`).join('\n')}`
         : '';
         : '';
 
 
       const hooksText = embedHooksText + manualHooksText;
       const hooksText = embedHooksText + manualHooksText;
@@ -206,7 +206,7 @@ ${antiText}
 【创作要求】
 【创作要求】
 1. 严格遵循上方世界状态和文风设定
 1. 严格遵循上方世界状态和文风设定
 2. 完整呈现选定的剧情方向
 2. 完整呈现选定的剧情方向
-3. 自然地埋入所有选定的叙事钩子
+3. 自然地埋入所有选定的代入感设计
 4. 满足所有写作约束
 4. 满足所有写作约束
 5. 体现反同质化策略,避免风格同质化
 5. 体现反同质化策略,避免风格同质化
 6. 直接输出章节正文,不要包含标题或额外说明
 6. 直接输出章节正文,不要包含标题或额外说明
@@ -618,7 +618,7 @@ ${antiText}
         <div>
         <div>
           <h2 className="text-xl font-semibold text-gray-900">生成章节内容</h2>
           <h2 className="text-xl font-semibold text-gray-900">生成章节内容</h2>
           <p className="text-sm text-gray-500 mt-1">
           <p className="text-sm text-gray-500 mt-1">
-            AI 正在根据剧情方向和钩子生成章节,您可以直接编辑修改
+            AI 正在根据剧情方向和代入感设计生成章节,您可以直接编辑修改
           </p>
           </p>
         </div>
         </div>
         <div className="flex items-center gap-2">
         <div className="flex items-center gap-2">
@@ -832,7 +832,7 @@ ${antiText}
           className="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
           className="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
         >
         >
           <ChevronLeft className="h-4 w-4" />
           <ChevronLeft className="h-4 w-4" />
-          上一步:埋入钩子
+          上一步:埋入代入感
         </button>
         </button>
         <button
         <button
           onClick={handleNext}
           onClick={handleNext}