Просмотр исходного кода

refactor: remove redundant inline file editor from Phase 2

- Delete inline textarea editor in NovelContextManager (editingFile view)
- Sidebar edit button now navigates to Phase 1 WorldEditor
- WorldEditor accepts initialFileName prop for auto-selection
- Remove dead handlers (handleSaveFile, handleCancelEdit)
- Update sidebar edit button tooltip to indicate Phase 1 editing
nonvioce 6 месяцев назад
Родитель
Сommit
9ebb2e3aaa

+ 575 - 636
src/app/features/novel-writer/components/NovelContextManager.tsx

@@ -1,636 +1,575 @@
-'use client';
-
-import { useState, useEffect, useCallback } from 'react';
-import { NovelType, NovelProject, WritingConstraint, AntiHomogenizationStrategy, WorldStateFile, NovelPhase, WorkflowState } from '@/app/lib/novel-types';
-import PhaseNavigation from '@/app/features/novel-writer/components/PhaseNavigation';
-import EventGraphEditor from '@/app/features/novel-writer/components/EventGraphEditor';
-import WorldEditor from '@/app/features/novel-writer/components/WorldEditor';
-import { ApiProvider } from '@/app/components/ApiSettings';
-import ApiSettings from '@/app/components/ApiSettings';
-import WorldStateSidebar from '@/app/features/novel-writer/components/WorldStateSidebar';
-import ChapterDraftPipeline from '@/app/features/novel-writer/components/ChapterDraftPipeline';
-import ChapterHistoryList from '@/app/features/novel-writer/components/ChapterHistoryList';
-import WritingConstraintManager from '@/app/features/novel-writer/components/WritingConstraintManager';
-import AntiHomogenizationPanel from '@/app/features/novel-writer/components/AntiHomogenizationPanel';
-import MaintenancePanel from '@/app/features/novel-writer/components/MaintenancePanel';
-import { useWorldState } from '@/app/hooks/useWorldState';
-import { useChapterHistory } from '@/app/hooks/useChapterHistory';
-
-// ── Prompt templates ──────────────────────────────────────────────────────────
-
-interface PromptTemplate {
-  id: string;
-  label: string;
-  description: string;
-}
-
-const PROMPT_TEMPLATES: PromptTemplate[] = [
-  {
-    id: 'start_first_chapter',
-    label: '开始第一章',
-    description: '基于世界设定和背景信息,创作完整的第一章内容,开启故事',
-  },
-  {
-    id: 'continue_paragraph',
-    label: '续写下一段',
-    description: '在当前内容基础上,续写下一个自然段,保持文风一致',
-  },
-  {
-    id: 'continue_chapter',
-    label: '续写下一章',
-    description: '根据当前剧情走向,续写完整的下一章内容',
-  },
-  {
-    id: 'expand_outline',
-    label: '根据大纲扩写',
-    description: '将提供的大纲要点扩写为完整的章节内容',
-  },
-];
-
-const TEMPLATE_STORAGE_KEY = 'novel_writer_last_template';
-
-// ── Props ─────────────────────────────────────────────────────────────────────
-
-interface NovelContextManagerProps {
-  projectPath: string;
-  novelType: NovelType;
-  apiSettings: { apiUrl: string; apiKey: string; model: string } | null;
-  onProjectClose: () => void;
-  // API settings panel passthrough
-  showApiSettings: boolean;
-  onToggleApiSettings: () => void;
-  apiProvider: ApiProvider;
-  setApiProvider: (provider: ApiProvider) => void;
-  apiUrl: string;
-  setApiUrl: (url: string) => void;
-  apiKey: string;
-  setApiKey: (key: string) => void;
-  model: string;
-  setModel: (model: string) => void;
-  availableModels: string[];
-  fetchModels: () => Promise<string[] | void>;
-}
-
-// ── Main component ────────────────────────────────────────────────────────────
-
-export default function NovelContextManager({
-  projectPath,
-  novelType,
-  apiSettings,
-  onProjectClose,
-  showApiSettings,
-  onToggleApiSettings,
-  apiProvider,
-  setApiProvider,
-  apiUrl,
-  setApiUrl,
-  apiKey,
-  setApiKey,
-  model,
-  setModel,
-  availableModels,
-  fetchModels,
-}: NovelContextManagerProps) {
-  // ── Project config ──
-  const [projectConfig, setProjectConfig] = useState<NovelProject | null>(null);
-  const [writingConstraints, setWritingConstraints] = useState<WritingConstraint[]>([]);
-  const [antiHomogenizationStrategies, setAntiHomogenizationStrategies] = useState<AntiHomogenizationStrategy[]>([]);
-  const [chapterWordCountMin, setChapterWordCountMin] = useState(2000);
-  const [chapterWordCountMax, setChapterWordCountMax] = useState(4000);
-  const [recentChaptersCount, setRecentChaptersCount] = useState(3);
-
-  // ── UI state ──
-  const [activeTab, setActiveTab] = useState<'chapters' | 'constraints' | 'anti'>('chapters');
-  const [showPipeline, setShowPipeline] = useState(false);
-  const [showCloseConfirm, setShowCloseConfirm] = useState(false);
-  const [marathonMode, setMarathonMode] = useState(false);
-  const [savedNotification, setSavedNotification] = useState(false);
-  const [selectedTemplateId, setSelectedTemplateId] = useState<string>(() => {
-    if (typeof window !== 'undefined') {
-      return localStorage.getItem(TEMPLATE_STORAGE_KEY) || PROMPT_TEMPLATES[0].id;
-    }
-    return PROMPT_TEMPLATES[0].id;
-  });
-
-  // ── Phase 1 state ──
-  const [currentPhase, setCurrentPhase] = useState<NovelPhase>('phase2');
-  const [phase1Tab, setPhase1Tab] = useState<'world' | 'events'>('world');
-
-  // ── Hooks ──
-  const { worldStateFiles, loreFiles, updateFile } = useWorldState(projectPath);
-  const { chapters, saveChapter } = useChapterHistory(projectPath);
-
-  // ── File editor state ──
-  const [editingFile, setEditingFile] = useState<{ file: WorldStateFile; isLore: boolean } | null>(null);
-  const [editContent, setEditContent] = useState('');
-  const [isSaving, setIsSaving] = useState(false);
-  const [saveError, setSaveError] = useState<string | null>(null);
-
-  // ── Load project config ──
-  const loadProjectConfig = useCallback(async () => {
-    try {
-      const response = await fetch('/api/novel/project', {
-        method: 'POST',
-        headers: { 'Content-Type': 'application/json' },
-        body: JSON.stringify({ action: 'open', path: projectPath }),
-      });
-      const data = await response.json();
-      if (data.success && data.project) {
-        const project = data.project as NovelProject;
-        setProjectConfig(project);
-        if (Array.isArray(project.writingConstraints)) {
-          setWritingConstraints(project.writingConstraints);
-        }
-        if (Array.isArray(project.antiHomogenizationStrategies)) {
-          setAntiHomogenizationStrategies(project.antiHomogenizationStrategies);
-        }
-        if (project.chapterWordCountMin) setChapterWordCountMin(project.chapterWordCountMin);
-        if (project.chapterWordCountMax) setChapterWordCountMax(project.chapterWordCountMax);
-        if (project.recentChaptersCount) setRecentChaptersCount(project.recentChaptersCount);
-      }
-    } catch {
-      // silently ignore — use defaults
-    }
-  }, [projectPath]);
-
-  useEffect(() => {
-    loadProjectConfig();
-  }, [loadProjectConfig]);
-
-  // ── Save config changes back to project ──
-  const saveProjectConfig = useCallback(
-    async (updates: Partial<NovelProject>) => {
-      try {
-        await fetch('/api/novel/project', {
-          method: 'POST',
-          headers: { 'Content-Type': 'application/json' },
-          body: JSON.stringify({
-            action: 'save_config',
-            path: projectPath,
-            config: updates,
-          }),
-        });
-      } catch {
-        // silently ignore
-      }
-    },
-    [projectPath]
-  );
-
-  // ── Template selection ──
-  const handleTemplateSelect = (templateId: string) => {
-    setSelectedTemplateId(templateId);
-    if (typeof window !== 'undefined') {
-      localStorage.setItem(TEMPLATE_STORAGE_KEY, templateId);
-    }
-  };
-
-  // ── Writing constraints change ──
-  const handleConstraintsChange = useCallback(
-    (constraints: WritingConstraint[]) => {
-      setWritingConstraints(constraints);
-      saveProjectConfig({ writingConstraints: constraints });
-    },
-    [saveProjectConfig]
-  );
-
-  // ── Anti-homogenization strategies change ──
-  const handleStrategiesChange = useCallback(
-    (strategies: AntiHomogenizationStrategy[]) => {
-      setAntiHomogenizationStrategies(strategies);
-      saveProjectConfig({ antiHomogenizationStrategies: strategies });
-    },
-    [saveProjectConfig]
-  );
-
-  // ── Chapter pipeline ──
-  const handleStartNewChapter = () => {
-    setShowPipeline(true);
-  };
-
-  const handlePipelineComplete = useCallback(
-    async (chapterTitle: string, content: string) => {
-      setShowPipeline(false);
-      try {
-        await saveChapter(content, chapterTitle);
-        setSavedNotification(true);
-        setTimeout(() => setSavedNotification(false), 3000);
-
-        // Background agents: RollingSummarizer every 5 chapters, MilestoneAudit every 20
-        const newChapterIndex = chapters.length + 1;
-        if (apiSettings) {
-          if (newChapterIndex % 5 === 0) {
-            const startChapter = newChapterIndex - 4;
-            fetch('/api/novel/summarize', {
-              method: 'POST',
-              headers: { 'Content-Type': 'application/json' },
-              body: JSON.stringify({ projectPath, startChapter, endChapter: newChapterIndex, apiSettings }),
-            }).catch(() => {});
-          }
-          if (newChapterIndex > 0 && newChapterIndex % 20 === 0) {
-            fetch('/api/novel/audit', {
-              method: 'POST',
-              headers: { 'Content-Type': 'application/json' },
-              body: JSON.stringify({ projectPath, chapterIndex: newChapterIndex, apiSettings }),
-            }).catch(() => {});
-          }
-        }
-
-        // Marathon mode: auto-start next chapter after short delay
-        if (marathonMode) {
-          setTimeout(() => setShowPipeline(true), 1000);
-        }
-      } catch {
-        // silently ignore — chapter was still written
-      }
-    },
-    [saveChapter, marathonMode, chapters.length, apiSettings, projectPath]
-  );
-
-  const handlePipelineCancel = () => {
-    setShowPipeline(false);
-  };
-
-  // ── File editor handlers ──
-// 文件编辑处理函数
-  const handleFileEdit = (file: WorldStateFile, isLore: boolean) => {
-    setEditingFile({ file, isLore });
-    setEditContent(file.content);
-    setSaveError(null);
-  };
-
-  // 文风设定编辑处理函数
-  const handleWritingStyleClick = async () => {
-    try {
-      const response = await fetch('/api/novel/files', {
-        method: 'POST',
-        headers: { 'Content-Type': 'application/json' },
-        body: JSON.stringify({
-          action: 'read',
-          projectPath,
-          filePath: 'world_state/writing_style.md',
-        }),
-      });
-      const data = await response.json();
-      
-      if (data.success) {
-        const writingStyleFile: WorldStateFile = {
-          fileName: 'writing_style.md',
-          displayName: '文风设定',
-          content: data.content as string,
-          lastUpdatedAt: data.lastUpdatedAt as string || new Date().toISOString(),
-          enabled: true,
-          isLore: false,
-        };
-        setEditingFile({ file: writingStyleFile, isLore: false });
-        setEditContent(data.content as string);
-        setSaveError(null);
-      }
-    } catch (err) {
-      console.error('加载文风设定失败:', err);
-    }
-  };
-
-  const handleSaveFile = async () => {
-    if (!editingFile) return;
-
-    setIsSaving(true);
-    setSaveError(null);
-    try {
-      await updateFile(editingFile.file.fileName, editContent, editingFile.isLore);
-      setEditingFile(null);
-    } catch (err) {
-      setSaveError(err instanceof Error ? err.message : '保存失败');
-    } finally {
-      setIsSaving(false);
-    }
-  };
-
-  const handleCancelEdit = () => {
-    setEditingFile(null);
-    setSaveError(null);
-  };
-
-  // ── Project name ──
-  const projectName = projectConfig?.name || projectPath.replace(/\\/g, '/').split('/').pop() || '未命名项目';
-
-  // ── Render pipeline (full-screen overlay) ──
-  if (showPipeline) {
-    return (
-      <ChapterDraftPipeline
-        projectPath={projectPath}
-        chapterIndex={chapters.length + 1}
-        apiSettings={apiSettings}
-        worldStateFiles={worldStateFiles}
-        loreFiles={loreFiles}
-        writingConstraints={writingConstraints}
-        antiHomogenizationStrategies={antiHomogenizationStrategies}
-        wordCountMin={chapterWordCountMin}
-        wordCountMax={chapterWordCountMax}
-        recentChaptersCount={recentChaptersCount}
-        onComplete={handlePipelineComplete}
-        onCancel={handlePipelineCancel}
-      />
-    );
-  }
-
-  // ── Main layout ──
-  return (
-    <div className="flex h-screen bg-gray-50 overflow-hidden">
-      {/* Left sidebar: World State */}
-      <WorldStateSidebar 
-        projectPath={projectPath} 
-        latestChapterIndex={chapters.length}
-        onFileEdit={handleFileEdit}
-        onWritingStyleClick={handleWritingStyleClick}
-      />
-
-      {/* Right main area */}
-      <div className="flex-1 flex flex-col min-w-0 overflow-hidden">
-        {/* Top toolbar */}
-        <header className="flex items-center justify-between px-5 py-3 bg-white border-b border-gray-200 shrink-0">
-          <div className="flex items-center gap-3 min-w-0">
-            <div className="min-w-0">
-              <h1 className="text-base font-semibold text-gray-800 truncate">{projectName}</h1>
-              <p className="text-xs text-gray-400 truncate">{projectPath}</p>
-            </div>
-          </div>
-
-          <div className="flex items-center gap-2 shrink-0">
-            {/* API settings toggle */}
-            <div className="relative">
-              <button
-                onClick={onToggleApiSettings}
-                className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg border transition-colors ${
-                  showApiSettings
-                    ? 'bg-blue-50 border-blue-300 text-blue-700'
-                    : 'bg-white border-gray-200 text-gray-600 hover:bg-gray-50'
-                }`}
-                aria-label="API 设置"
-              >
-                <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-                  <path
-                    strokeLinecap="round"
-                    strokeLinejoin="round"
-                    strokeWidth={2}
-                    d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
-                  />
-                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
-                </svg>
-                API 设置
-              </button>
-
-              {showApiSettings && (
-                <div className="absolute top-10 right-0 w-80 bg-white border border-gray-200 rounded-xl shadow-xl p-4 z-50">
-                  <div className="flex items-center justify-between mb-3">
-                    <h3 className="text-sm font-semibold text-gray-700">API 设置</h3>
-                    <button
-                      onClick={onToggleApiSettings}
-                      className="text-gray-400 hover:text-gray-600 transition-colors"
-                      aria-label="关闭"
-                    >
-                      <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-                        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
-                      </svg>
-                    </button>
-                  </div>
-                  <ApiSettings
-                    showSettings={true}
-                    toggleSettings={onToggleApiSettings}
-                    apiProvider={apiProvider}
-                    setApiProvider={setApiProvider}
-                    apiUrl={apiUrl}
-                    setApiUrl={setApiUrl}
-                    apiKey={apiKey}
-                    setApiKey={setApiKey}
-                    model={model}
-                    setModel={setModel}
-                    availableModels={availableModels}
-                    fetchModels={fetchModels}
-                  />
-                </div>
-              )}
-            </div>
-
-            {/* Close project */}
-            {!showCloseConfirm ? (
-              <button
-                onClick={() => setShowCloseConfirm(true)}
-                className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-red-50 hover:border-red-300 hover:text-red-600 transition-colors"
-              >
-                <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
-                </svg>
-                关闭项目
-              </button>
-            ) : (
-              <div className="flex items-center gap-2">
-                <span className="text-xs text-red-600">确定关闭?</span>
-                <button onClick={onProjectClose} className="px-2 py-1 text-xs font-medium text-white bg-red-500 rounded hover:bg-red-600">确认</button>
-                <button onClick={() => setShowCloseConfirm(false)} className="px-2 py-1 text-xs text-gray-500 border border-gray-300 rounded hover:bg-gray-50">取消</button>
-              </div>
-            )}
-          </div>
-        </header>
-
-        <PhaseNavigation currentPhase={currentPhase} onPhaseChange={setCurrentPhase} />
-
-        {currentPhase === 'phase1' && (
-          <div className="flex-1 flex flex-col overflow-hidden">
-            <div className="flex items-center gap-0 border-b border-gray-200 bg-white px-4 shrink-0">
-              {[
-                { id: 'world' as const, label: '世界观编辑' },
-                { id: 'events' as const, label: '事件图' },
-              ].map(tab => (
-                <button key={tab.id} onClick={() => setPhase1Tab(tab.id)}
-                  className={`px-4 py-2.5 text-sm font-medium border-b-2 transition-colors ${
-                    phase1Tab === tab.id ? 'border-indigo-600 text-indigo-600' : 'border-transparent text-gray-500 hover:text-gray-700'
-                  }`}>
-                  {tab.label}
-                </button>
-              ))}
-            </div>
-            {phase1Tab === 'world' ? (
-              <WorldEditor projectPath={projectPath} apiSettings={apiSettings} />
-            ) : (
-              <EventGraphEditor projectPath={projectPath} apiSettings={apiSettings} />
-            )}
-          </div>
-        )}
-
-        {currentPhase === 'phase3' && (
-          <MaintenancePanel projectPath={projectPath} apiSettings={apiSettings} />
-        )}
-
-        {currentPhase !== 'phase1' && currentPhase !== 'phase3' && (
-          <>
-        {editingFile ? (
-          <div className="flex-1 overflow-auto p-5">
-            <div className="max-w-4xl mx-auto space-y-4">
-              <div className="flex items-center justify-between">
-                <h2 className="text-lg font-semibold text-gray-800">
-                  编辑文件: {editingFile.file.displayName || editingFile.file.fileName}
-                </h2>
-                <div className="flex gap-2">
-                  <button
-                    onClick={handleCancelEdit}
-                    className="px-4 py-2 text-sm bg-gray-100 text-gray-600 rounded hover:bg-gray-200 transition-colors"
-                  >
-                    取消
-                  </button>
-                  <button
-                    onClick={handleSaveFile}
-                    disabled={isSaving}
-                    className="px-4 py-2 text-sm bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors disabled:opacity-50"
-                  >
-                    {isSaving ? '保存中...' : '保存'}
-                  </button>
-                </div>
-              </div>
-              {saveError && (
-                <div className="p-3 bg-red-50 border border-red-200 rounded text-sm text-red-600">
-                  {saveError}
-                </div>
-              )}
-              <div className="border border-gray-200 rounded-lg overflow-hidden">
-                <textarea
-                  value={editContent}
-                  onChange={(e) => setEditContent(e.target.value)}
-                  className="w-full h-[60vh] p-4 text-sm font-mono border-0 focus:outline-none resize-none"
-                  placeholder="文件内容..."
-                />
-              </div>
-              <div className="text-xs text-gray-400">
-                最后更新: {new Date(editingFile.file.lastUpdatedAt).toLocaleString()}
-              </div>
-            </div>
-          </div>
-        ) : (
-          <div className="flex-1 flex flex-col">
-            {/* Saved notification */}
-            {savedNotification && (
-              <div className="mx-5 mt-3 px-4 py-2 bg-green-50 border border-green-200 rounded-lg text-sm text-green-700 flex items-center gap-2 shrink-0">
-                <svg className="w-4 h-4 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
-                </svg>
-                已自动保存
-              </div>
-            )}
-
-            {/* Prompt template selector */}
-            <div className="px-5 py-3 bg-white border-b border-gray-100 shrink-0">
-              <div className="flex items-center gap-2">
-                <span className="text-xs text-gray-500 shrink-0">AI 提示词模板:</span>
-                <div className="flex gap-2 flex-wrap">
-                  {PROMPT_TEMPLATES.map((template) => (
-                    <button
-                      key={template.id}
-                      onClick={() => handleTemplateSelect(template.id)}
-                      title={template.description}
-                      className={`px-3 py-1 text-xs rounded-full border transition-all ${
-                        selectedTemplateId === template.id
-                          ? 'bg-indigo-600 border-indigo-600 text-white'
-                          : 'bg-white border-gray-200 text-gray-600 hover:border-indigo-300 hover:text-indigo-600'
-                      }`}
-                    >
-                      {template.label}
-                    </button>
-                  ))}
-                </div>
-              </div>
-            </div>
-
-            {/* Start new chapter + Marathon toggle */}
-            <div className="px-5 py-4 shrink-0 space-y-3">
-              <button
-                onClick={handleStartNewChapter}
-                className="flex items-center gap-2 w-full px-5 py-2.5 bg-indigo-600 text-white text-sm font-medium rounded-xl hover:bg-indigo-700 transition-colors shadow-sm"
-              >
-                <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
-                </svg>
-                开始新章节(第 {chapters.length + 1} 章)
-              </button>
-              <label className="flex items-center gap-2 px-3 py-2 bg-gray-50 border border-gray-200 rounded-lg cursor-pointer hover:bg-gray-100 transition-colors">
-                <input
-                  type="checkbox"
-                  checked={marathonMode}
-                  onChange={(e) => setMarathonMode(e.target.checked)}
-                  className="w-4 h-4 text-indigo-600 rounded focus:ring-indigo-500"
-                />
-                <span className="text-xs text-gray-600">
-                  马拉松模式
-                  {marathonMode && <span className="text-indigo-600 font-medium ml-1">— 自动续写</span>}
-                </span>
-              </label>
-            </div>
-
-            {/* Tab navigation */}
-            <div className="px-5 border-b border-gray-200 bg-white shrink-0">
-              <nav className="flex gap-0" role="tablist">
-                {[
-                  { id: 'chapters' as const, label: '章节历史' },
-                  { id: 'constraints' as const, label: '写作约束' },
-                  { id: 'anti' as const, label: '反同质化' },
-                ].map((tab) => (
-                  <button
-                    key={tab.id}
-                    role="tab"
-                    aria-selected={activeTab === tab.id}
-                    onClick={() => setActiveTab(tab.id)}
-                    className={`px-4 py-2.5 text-sm font-medium border-b-2 transition-colors ${
-                      activeTab === tab.id
-                        ? 'border-indigo-600 text-indigo-600'
-                        : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
-                    }`}
-                  >
-                    {tab.label}
-                  </button>
-                ))}
-              </nav>
-            </div>
-
-            {/* Tab content */}
-            <div className="flex-1 overflow-hidden">
-              {activeTab === 'chapters' && (
-                <ChapterHistoryList projectPath={projectPath} />
-              )}
-
-              {activeTab === 'constraints' && (
-                <div className="h-full overflow-y-auto p-5">
-                  <div className="max-w-2xl">
-                    <h2 className="text-sm font-semibold text-gray-700 mb-1">写作约束</h2>
-                    <p className="text-xs text-gray-400 mb-4">
-                      设置每章必须满足的写作要求,这些约束将注入 AI 提示词中。
-                    </p>
-                    <WritingConstraintManager
-                      constraints={writingConstraints}
-                      onChange={handleConstraintsChange}
-                    />
-                  </div>
-                </div>
-              )}
-
-              {activeTab === 'anti' && (
-                <div className="h-full overflow-y-auto p-5">
-                  <div className="max-w-2xl">
-                    <h2 className="text-sm font-semibold text-gray-700 mb-1">反同质化策略</h2>
-                    <p className="text-xs text-gray-400 mb-4">
-                      选择用于避免内容同质化的写作策略,这些策略将注入 AI 提示词中。
-                    </p>
-                    <AntiHomogenizationPanel
-                      strategies={antiHomogenizationStrategies}
-                      onChange={handleStrategiesChange}
-                    />
-                  </div>
-                </div>
-              )}
-            </div>
-          </div>
-        )}
-          </>
-        )}
-      </div>
-    </div>
-  );
-}
+'use client';
+
+import { useState, useEffect, useCallback } from 'react';
+import { NovelType, NovelProject, WritingConstraint, AntiHomogenizationStrategy, WorldStateFile, NovelPhase, WorkflowState } from '@/app/lib/novel-types';
+import PhaseNavigation from '@/app/features/novel-writer/components/PhaseNavigation';
+import EventGraphEditor from '@/app/features/novel-writer/components/EventGraphEditor';
+import WorldEditor from '@/app/features/novel-writer/components/WorldEditor';
+import { ApiProvider } from '@/app/components/ApiSettings';
+import ApiSettings from '@/app/components/ApiSettings';
+import WorldStateSidebar from '@/app/features/novel-writer/components/WorldStateSidebar';
+import ChapterDraftPipeline from '@/app/features/novel-writer/components/ChapterDraftPipeline';
+import ChapterHistoryList from '@/app/features/novel-writer/components/ChapterHistoryList';
+import WritingConstraintManager from '@/app/features/novel-writer/components/WritingConstraintManager';
+import AntiHomogenizationPanel from '@/app/features/novel-writer/components/AntiHomogenizationPanel';
+import MaintenancePanel from '@/app/features/novel-writer/components/MaintenancePanel';
+import { useWorldState } from '@/app/hooks/useWorldState';
+import { useChapterHistory } from '@/app/hooks/useChapterHistory';
+
+// ── Prompt templates ──────────────────────────────────────────────────────────
+
+interface PromptTemplate {
+  id: string;
+  label: string;
+  description: string;
+}
+
+const PROMPT_TEMPLATES: PromptTemplate[] = [
+  {
+    id: 'start_first_chapter',
+    label: '开始第一章',
+    description: '基于世界设定和背景信息,创作完整的第一章内容,开启故事',
+  },
+  {
+    id: 'continue_paragraph',
+    label: '续写下一段',
+    description: '在当前内容基础上,续写下一个自然段,保持文风一致',
+  },
+  {
+    id: 'continue_chapter',
+    label: '续写下一章',
+    description: '根据当前剧情走向,续写完整的下一章内容',
+  },
+  {
+    id: 'expand_outline',
+    label: '根据大纲扩写',
+    description: '将提供的大纲要点扩写为完整的章节内容',
+  },
+];
+
+const TEMPLATE_STORAGE_KEY = 'novel_writer_last_template';
+
+// ── Props ─────────────────────────────────────────────────────────────────────
+
+interface NovelContextManagerProps {
+  projectPath: string;
+  novelType: NovelType;
+  apiSettings: { apiUrl: string; apiKey: string; model: string } | null;
+  onProjectClose: () => void;
+  // API settings panel passthrough
+  showApiSettings: boolean;
+  onToggleApiSettings: () => void;
+  apiProvider: ApiProvider;
+  setApiProvider: (provider: ApiProvider) => void;
+  apiUrl: string;
+  setApiUrl: (url: string) => void;
+  apiKey: string;
+  setApiKey: (key: string) => void;
+  model: string;
+  setModel: (model: string) => void;
+  availableModels: string[];
+  fetchModels: () => Promise<string[] | void>;
+}
+
+// ── Main component ────────────────────────────────────────────────────────────
+
+export default function NovelContextManager({
+  projectPath,
+  novelType,
+  apiSettings,
+  onProjectClose,
+  showApiSettings,
+  onToggleApiSettings,
+  apiProvider,
+  setApiProvider,
+  apiUrl,
+  setApiUrl,
+  apiKey,
+  setApiKey,
+  model,
+  setModel,
+  availableModels,
+  fetchModels,
+}: NovelContextManagerProps) {
+  // ── Project config ──
+  const [projectConfig, setProjectConfig] = useState<NovelProject | null>(null);
+  const [writingConstraints, setWritingConstraints] = useState<WritingConstraint[]>([]);
+  const [antiHomogenizationStrategies, setAntiHomogenizationStrategies] = useState<AntiHomogenizationStrategy[]>([]);
+  const [chapterWordCountMin, setChapterWordCountMin] = useState(2000);
+  const [chapterWordCountMax, setChapterWordCountMax] = useState(4000);
+  const [recentChaptersCount, setRecentChaptersCount] = useState(3);
+
+  // ── UI state ──
+  const [activeTab, setActiveTab] = useState<'chapters' | 'constraints' | 'anti'>('chapters');
+  const [showPipeline, setShowPipeline] = useState(false);
+  const [showCloseConfirm, setShowCloseConfirm] = useState(false);
+  const [marathonMode, setMarathonMode] = useState(false);
+  const [savedNotification, setSavedNotification] = useState(false);
+  const [selectedTemplateId, setSelectedTemplateId] = useState<string>(() => {
+    if (typeof window !== 'undefined') {
+      return localStorage.getItem(TEMPLATE_STORAGE_KEY) || PROMPT_TEMPLATES[0].id;
+    }
+    return PROMPT_TEMPLATES[0].id;
+  });
+
+  // ── Phase 1 state ──
+  const [currentPhase, setCurrentPhase] = useState<NovelPhase>('phase2');
+  const [phase1Tab, setPhase1Tab] = useState<'world' | 'events'>('world');
+
+  // ── Hooks ──
+  const { worldStateFiles, loreFiles } = useWorldState(projectPath);
+  const { chapters, saveChapter } = useChapterHistory(projectPath);
+
+  // ── File editor state ──
+  const [editingFile, setEditingFile] = useState<{ file: WorldStateFile; isLore: boolean } | null>(null);
+
+  // ── Load project config ──
+  const loadProjectConfig = useCallback(async () => {
+    try {
+      const response = await fetch('/api/novel/project', {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({ action: 'open', path: projectPath }),
+      });
+      const data = await response.json();
+      if (data.success && data.project) {
+        const project = data.project as NovelProject;
+        setProjectConfig(project);
+        if (Array.isArray(project.writingConstraints)) {
+          setWritingConstraints(project.writingConstraints);
+        }
+        if (Array.isArray(project.antiHomogenizationStrategies)) {
+          setAntiHomogenizationStrategies(project.antiHomogenizationStrategies);
+        }
+        if (project.chapterWordCountMin) setChapterWordCountMin(project.chapterWordCountMin);
+        if (project.chapterWordCountMax) setChapterWordCountMax(project.chapterWordCountMax);
+        if (project.recentChaptersCount) setRecentChaptersCount(project.recentChaptersCount);
+      }
+    } catch {
+      // silently ignore — use defaults
+    }
+  }, [projectPath]);
+
+  useEffect(() => {
+    loadProjectConfig();
+  }, [loadProjectConfig]);
+
+  // ── Save config changes back to project ──
+  const saveProjectConfig = useCallback(
+    async (updates: Partial<NovelProject>) => {
+      try {
+        await fetch('/api/novel/project', {
+          method: 'POST',
+          headers: { 'Content-Type': 'application/json' },
+          body: JSON.stringify({
+            action: 'save_config',
+            path: projectPath,
+            config: updates,
+          }),
+        });
+      } catch {
+        // silently ignore
+      }
+    },
+    [projectPath]
+  );
+
+  // ── Template selection ──
+  const handleTemplateSelect = (templateId: string) => {
+    setSelectedTemplateId(templateId);
+    if (typeof window !== 'undefined') {
+      localStorage.setItem(TEMPLATE_STORAGE_KEY, templateId);
+    }
+  };
+
+  // ── Writing constraints change ──
+  const handleConstraintsChange = useCallback(
+    (constraints: WritingConstraint[]) => {
+      setWritingConstraints(constraints);
+      saveProjectConfig({ writingConstraints: constraints });
+    },
+    [saveProjectConfig]
+  );
+
+  // ── Anti-homogenization strategies change ──
+  const handleStrategiesChange = useCallback(
+    (strategies: AntiHomogenizationStrategy[]) => {
+      setAntiHomogenizationStrategies(strategies);
+      saveProjectConfig({ antiHomogenizationStrategies: strategies });
+    },
+    [saveProjectConfig]
+  );
+
+  // ── Chapter pipeline ──
+  const handleStartNewChapter = () => {
+    setShowPipeline(true);
+  };
+
+  const handlePipelineComplete = useCallback(
+    async (chapterTitle: string, content: string) => {
+      setShowPipeline(false);
+      try {
+        await saveChapter(content, chapterTitle);
+        setSavedNotification(true);
+        setTimeout(() => setSavedNotification(false), 3000);
+
+        // Background agents: RollingSummarizer every 5 chapters, MilestoneAudit every 20
+        const newChapterIndex = chapters.length + 1;
+        if (apiSettings) {
+          if (newChapterIndex % 5 === 0) {
+            const startChapter = newChapterIndex - 4;
+            fetch('/api/novel/summarize', {
+              method: 'POST',
+              headers: { 'Content-Type': 'application/json' },
+              body: JSON.stringify({ projectPath, startChapter, endChapter: newChapterIndex, apiSettings }),
+            }).catch(() => {});
+          }
+          if (newChapterIndex > 0 && newChapterIndex % 20 === 0) {
+            fetch('/api/novel/audit', {
+              method: 'POST',
+              headers: { 'Content-Type': 'application/json' },
+              body: JSON.stringify({ projectPath, chapterIndex: newChapterIndex, apiSettings }),
+            }).catch(() => {});
+          }
+        }
+
+        // Marathon mode: auto-start next chapter after short delay
+        if (marathonMode) {
+          setTimeout(() => setShowPipeline(true), 1000);
+        }
+      } catch {
+        // silently ignore — chapter was still written
+      }
+    },
+    [saveChapter, marathonMode, chapters.length, apiSettings, projectPath]
+  );
+
+  const handlePipelineCancel = () => {
+    setShowPipeline(false);
+  };
+
+  // ── File editor handlers ──
+// 文件编辑处理函数
+  const handleFileEdit = (file: WorldStateFile, isLore: boolean) => {
+    setEditingFile({ file, isLore });
+    setCurrentPhase('phase1');
+    setPhase1Tab('world');
+  };
+
+  // 文风设定编辑处理函数
+  const handleWritingStyleClick = async () => {
+    try {
+      const response = await fetch('/api/novel/files', {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({
+          action: 'read',
+          projectPath,
+          filePath: 'world_state/writing_style.md',
+        }),
+      });
+      const data = await response.json();
+      
+      if (data.success) {
+        const writingStyleFile: WorldStateFile = {
+          fileName: 'writing_style.md',
+          displayName: '文风设定',
+          content: data.content as string,
+          lastUpdatedAt: data.lastUpdatedAt as string || new Date().toISOString(),
+          enabled: true,
+          isLore: false,
+        };
+        setEditingFile({ file: writingStyleFile, isLore: false });
+        setCurrentPhase('phase1');
+        setPhase1Tab('world');
+      }
+    } catch (err) {
+      console.error('加载文风设定失败:', err);
+    }
+  };
+
+
+  // ── Project name ──
+  const projectName = projectConfig?.name || projectPath.replace(/\\/g, '/').split('/').pop() || '未命名项目';
+
+  // ── Render pipeline (full-screen overlay) ──
+  if (showPipeline) {
+    return (
+      <ChapterDraftPipeline
+        projectPath={projectPath}
+        chapterIndex={chapters.length + 1}
+        apiSettings={apiSettings}
+        worldStateFiles={worldStateFiles}
+        loreFiles={loreFiles}
+        writingConstraints={writingConstraints}
+        antiHomogenizationStrategies={antiHomogenizationStrategies}
+        wordCountMin={chapterWordCountMin}
+        wordCountMax={chapterWordCountMax}
+        recentChaptersCount={recentChaptersCount}
+        onComplete={handlePipelineComplete}
+        onCancel={handlePipelineCancel}
+      />
+    );
+  }
+
+  // ── Main layout ──
+  return (
+    <div className="flex h-screen bg-gray-50 overflow-hidden">
+      {/* Left sidebar: World State */}
+      <WorldStateSidebar 
+        projectPath={projectPath} 
+        latestChapterIndex={chapters.length}
+        onFileEdit={handleFileEdit}
+        onWritingStyleClick={handleWritingStyleClick}
+      />
+
+      {/* Right main area */}
+      <div className="flex-1 flex flex-col min-w-0 overflow-hidden">
+        {/* Top toolbar */}
+        <header className="flex items-center justify-between px-5 py-3 bg-white border-b border-gray-200 shrink-0">
+          <div className="flex items-center gap-3 min-w-0">
+            <div className="min-w-0">
+              <h1 className="text-base font-semibold text-gray-800 truncate">{projectName}</h1>
+              <p className="text-xs text-gray-400 truncate">{projectPath}</p>
+            </div>
+          </div>
+
+          <div className="flex items-center gap-2 shrink-0">
+            {/* API settings toggle */}
+            <div className="relative">
+              <button
+                onClick={onToggleApiSettings}
+                className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg border transition-colors ${
+                  showApiSettings
+                    ? 'bg-blue-50 border-blue-300 text-blue-700'
+                    : 'bg-white border-gray-200 text-gray-600 hover:bg-gray-50'
+                }`}
+                aria-label="API 设置"
+              >
+                <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                  <path
+                    strokeLinecap="round"
+                    strokeLinejoin="round"
+                    strokeWidth={2}
+                    d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
+                  />
+                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
+                </svg>
+                API 设置
+              </button>
+
+              {showApiSettings && (
+                <div className="absolute top-10 right-0 w-80 bg-white border border-gray-200 rounded-xl shadow-xl p-4 z-50">
+                  <div className="flex items-center justify-between mb-3">
+                    <h3 className="text-sm font-semibold text-gray-700">API 设置</h3>
+                    <button
+                      onClick={onToggleApiSettings}
+                      className="text-gray-400 hover:text-gray-600 transition-colors"
+                      aria-label="关闭"
+                    >
+                      <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
+                      </svg>
+                    </button>
+                  </div>
+                  <ApiSettings
+                    showSettings={true}
+                    toggleSettings={onToggleApiSettings}
+                    apiProvider={apiProvider}
+                    setApiProvider={setApiProvider}
+                    apiUrl={apiUrl}
+                    setApiUrl={setApiUrl}
+                    apiKey={apiKey}
+                    setApiKey={setApiKey}
+                    model={model}
+                    setModel={setModel}
+                    availableModels={availableModels}
+                    fetchModels={fetchModels}
+                  />
+                </div>
+              )}
+            </div>
+
+            {/* Close project */}
+            {!showCloseConfirm ? (
+              <button
+                onClick={() => setShowCloseConfirm(true)}
+                className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-red-50 hover:border-red-300 hover:text-red-600 transition-colors"
+              >
+                <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
+                </svg>
+                关闭项目
+              </button>
+            ) : (
+              <div className="flex items-center gap-2">
+                <span className="text-xs text-red-600">确定关闭?</span>
+                <button onClick={onProjectClose} className="px-2 py-1 text-xs font-medium text-white bg-red-500 rounded hover:bg-red-600">确认</button>
+                <button onClick={() => setShowCloseConfirm(false)} className="px-2 py-1 text-xs text-gray-500 border border-gray-300 rounded hover:bg-gray-50">取消</button>
+              </div>
+            )}
+          </div>
+        </header>
+
+        <PhaseNavigation currentPhase={currentPhase} onPhaseChange={setCurrentPhase} />
+
+        {currentPhase === 'phase1' && (
+          <div className="flex-1 flex flex-col overflow-hidden">
+            <div className="flex items-center gap-0 border-b border-gray-200 bg-white px-4 shrink-0">
+              {[
+                { id: 'world' as const, label: '世界观编辑' },
+                { id: 'events' as const, label: '事件图' },
+              ].map(tab => (
+                <button key={tab.id} onClick={() => setPhase1Tab(tab.id)}
+                  className={`px-4 py-2.5 text-sm font-medium border-b-2 transition-colors ${
+                    phase1Tab === tab.id ? 'border-indigo-600 text-indigo-600' : 'border-transparent text-gray-500 hover:text-gray-700'
+                  }`}>
+                  {tab.label}
+                </button>
+              ))}
+            </div>
+            {phase1Tab === 'world' ? (
+              <WorldEditor
+                projectPath={projectPath}
+                apiSettings={apiSettings}
+                initialFileName={editingFile?.file.fileName}
+              />
+            ) : (
+              <EventGraphEditor projectPath={projectPath} apiSettings={apiSettings} />
+            )}
+          </div>
+        )}
+
+        {currentPhase === 'phase3' && (
+          <MaintenancePanel projectPath={projectPath} apiSettings={apiSettings} />
+        )}
+
+        {currentPhase !== 'phase1' && currentPhase !== 'phase3' && (
+          <>
+          <div className="flex-1 flex flex-col">
+            {/* Saved notification */}
+            {savedNotification && (
+              <div className="mx-5 mt-3 px-4 py-2 bg-green-50 border border-green-200 rounded-lg text-sm text-green-700 flex items-center gap-2 shrink-0">
+                <svg className="w-4 h-4 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
+                </svg>
+                已自动保存
+              </div>
+            )}
+
+            {/* Prompt template selector */}
+            <div className="px-5 py-3 bg-white border-b border-gray-100 shrink-0">
+              <div className="flex items-center gap-2">
+                <span className="text-xs text-gray-500 shrink-0">AI 提示词模板:</span>
+                <div className="flex gap-2 flex-wrap">
+                  {PROMPT_TEMPLATES.map((template) => (
+                    <button
+                      key={template.id}
+                      onClick={() => handleTemplateSelect(template.id)}
+                      title={template.description}
+                      className={`px-3 py-1 text-xs rounded-full border transition-all ${
+                        selectedTemplateId === template.id
+                          ? 'bg-indigo-600 border-indigo-600 text-white'
+                          : 'bg-white border-gray-200 text-gray-600 hover:border-indigo-300 hover:text-indigo-600'
+                      }`}
+                    >
+                      {template.label}
+                    </button>
+                  ))}
+                </div>
+              </div>
+            </div>
+
+            {/* Start new chapter + Marathon toggle */}
+            <div className="px-5 py-4 shrink-0 space-y-3">
+              <button
+                onClick={handleStartNewChapter}
+                className="flex items-center gap-2 w-full px-5 py-2.5 bg-indigo-600 text-white text-sm font-medium rounded-xl hover:bg-indigo-700 transition-colors shadow-sm"
+              >
+                <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
+                </svg>
+                开始新章节(第 {chapters.length + 1} 章)
+              </button>
+              <label className="flex items-center gap-2 px-3 py-2 bg-gray-50 border border-gray-200 rounded-lg cursor-pointer hover:bg-gray-100 transition-colors">
+                <input
+                  type="checkbox"
+                  checked={marathonMode}
+                  onChange={(e) => setMarathonMode(e.target.checked)}
+                  className="w-4 h-4 text-indigo-600 rounded focus:ring-indigo-500"
+                />
+                <span className="text-xs text-gray-600">
+                  马拉松模式
+                  {marathonMode && <span className="text-indigo-600 font-medium ml-1">— 自动续写</span>}
+                </span>
+              </label>
+            </div>
+
+            {/* Tab navigation */}
+            <div className="px-5 border-b border-gray-200 bg-white shrink-0">
+              <nav className="flex gap-0" role="tablist">
+                {[
+                  { id: 'chapters' as const, label: '章节历史' },
+                  { id: 'constraints' as const, label: '写作约束' },
+                  { id: 'anti' as const, label: '反同质化' },
+                ].map((tab) => (
+                  <button
+                    key={tab.id}
+                    role="tab"
+                    aria-selected={activeTab === tab.id}
+                    onClick={() => setActiveTab(tab.id)}
+                    className={`px-4 py-2.5 text-sm font-medium border-b-2 transition-colors ${
+                      activeTab === tab.id
+                        ? 'border-indigo-600 text-indigo-600'
+                        : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
+                    }`}
+                  >
+                    {tab.label}
+                  </button>
+                ))}
+              </nav>
+            </div>
+
+            {/* Tab content */}
+            <div className="flex-1 overflow-hidden">
+              {activeTab === 'chapters' && (
+                <ChapterHistoryList projectPath={projectPath} />
+              )}
+
+              {activeTab === 'constraints' && (
+                <div className="h-full overflow-y-auto p-5">
+                  <div className="max-w-2xl">
+                    <h2 className="text-sm font-semibold text-gray-700 mb-1">写作约束</h2>
+                    <p className="text-xs text-gray-400 mb-4">
+                      设置每章必须满足的写作要求,这些约束将注入 AI 提示词中。
+                    </p>
+                    <WritingConstraintManager
+                      constraints={writingConstraints}
+                      onChange={handleConstraintsChange}
+                    />
+                  </div>
+                </div>
+              )}
+
+              {activeTab === 'anti' && (
+                <div className="h-full overflow-y-auto p-5">
+                  <div className="max-w-2xl">
+                    <h2 className="text-sm font-semibold text-gray-700 mb-1">反同质化策略</h2>
+                    <p className="text-xs text-gray-400 mb-4">
+                      选择用于避免内容同质化的写作策略,这些策略将注入 AI 提示词中。
+                    </p>
+                    <AntiHomogenizationPanel
+                      strategies={antiHomogenizationStrategies}
+                      onChange={handleStrategiesChange}
+                    />
+                  </div>
+                </div>
+              )}
+            </div>
+          </div>
+          </>
+        )}
+      </div>
+    </div>
+  );
+}

+ 16 - 1
src/app/features/novel-writer/components/WorldEditor.tsx

@@ -8,11 +8,26 @@ import { useWorldState } from '@/app/hooks/useWorldState';
 interface WorldEditorProps {
   projectPath: string;
   apiSettings: { apiUrl: string; apiKey: string; model: string } | null;
+  initialFileName?: string;
 }
 
-export default function WorldEditor({ projectPath, apiSettings }: WorldEditorProps) {
+export default function WorldEditor({ projectPath, apiSettings, initialFileName }: WorldEditorProps) {
   const { worldStateFiles, loreFiles, updateFile } = useWorldState(projectPath);
   const [selectedFile, setSelectedFile] = useState<WorldStateFile | null>(null);
+
+  // Auto-select the file navigated from sidebar
+  useEffect(() => {
+    if (!initialFileName) return;
+    const allFiles = [
+      ...worldStateFiles.map(f => ({ ...f, category: 'world_state' as const })),
+      ...loreFiles.map(f => ({ ...f, category: 'lore' as const })),
+    ];
+    const target = allFiles.find(f => f.fileName === initialFileName);
+    if (target) {
+      setSelectedFile(target);
+      setEditContent(target.content);
+    }
+  }, [initialFileName, worldStateFiles, loreFiles]);
   const [editContent, setEditContent] = useState('');
   const [isAiExpanding, setIsAiExpanding] = useState(false);
   const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved'>('idle');

+ 4 - 4
src/app/features/novel-writer/components/WorldStateSidebar.tsx

@@ -128,12 +128,12 @@ function FileEntry({ file, isLore, projectPath, onFileEdit }: FileEntryProps) {
           </svg>
         </button>
 
-        {/* Edit button */}
+        {/* Edit button — opens in Phase 1 WorldEditor */}
         <button
           onClick={() => onFileEdit(file, isLore)}
-          className="shrink-0 p-1 text-gray-400 hover:text-blue-500 transition-colors rounded"
-          title="编辑"
-          aria-label="编辑文件"
+          className="shrink-0 p-1 text-gray-400 hover:text-indigo-500 transition-colors rounded"
+          title="在世界构建中编辑"
+          aria-label="在世界构建中编辑文件"
         >
           <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
             <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />