Browse Source

feat: improve marathon mode with chapter target, pause/resume, and progress display

- Add chapter target number input next to marathon checkbox (0 = no limit)
- Show progress as "第 X/Y 章" when marathon is active
- Add pause/resume toggle button during marathon mode
- Stop automatically when target chapter count is reached with alert notification
- Increased auto-advance delay from 1s to 2s for user review time
- Add pulsing visual indicator when marathon is active between chapters
- Respect pause state before auto-starting next chapter

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nonvioce 6 months ago
parent
commit
060d39a1ea
1 changed files with 66 additions and 14 deletions
  1. 66 14
      src/app/features/novel-writer/components/NovelContextManager.tsx

+ 66 - 14
src/app/features/novel-writer/components/NovelContextManager.tsx

@@ -104,6 +104,8 @@ export default function NovelContextManager({
   const [showPipeline, setShowPipeline] = useState(false);
   const [showCloseConfirm, setShowCloseConfirm] = useState(false);
   const [marathonMode, setMarathonMode] = useState(false);
+  const [marathonTarget, setMarathonTarget] = useState(0); // 0 = no limit
+  const [marathonPaused, setMarathonPaused] = useState(false);
   const [selectedTemplateId, setSelectedTemplateId] = useState<string>(() => {
     if (typeof window !== 'undefined') {
       return localStorage.getItem(TEMPLATE_STORAGE_KEY) || PROMPT_TEMPLATES[0].id;
@@ -232,13 +234,22 @@ export default function NovelContextManager({
 
         // Marathon mode: auto-start next chapter after short delay
         if (marathonMode) {
-          setTimeout(() => setShowPipeline(true), 1000);
+          const savedChapterIndex = chapters.length + 1;
+          if (marathonTarget > 0 && savedChapterIndex >= marathonTarget) {
+            // Target reached — stop marathon mode
+            setMarathonMode(false);
+            setMarathonTarget(0);
+            setMarathonPaused(false);
+            window.alert(`马拉松模式完成!已续写至第 ${marathonTarget} 章`);
+          } else if (!marathonPaused) {
+            setTimeout(() => setShowPipeline(true), 2000);
+          }
         }
       } catch {
         // silently ignore — chapter was still written
       }
     },
-    [saveChapter, marathonMode, chapters.length, apiSettings, projectPath]
+    [saveChapter, marathonMode, marathonTarget, marathonPaused, chapters.length, apiSettings, projectPath]
   );
 
   const handlePipelineCancel = () => {
@@ -476,18 +487,59 @@ export default function NovelContextManager({
                 </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>
+              {marathonMode ? (
+                /* Active marathon mode — show pause button + progress */
+                <div className="flex items-center gap-3 px-3 py-2 bg-indigo-50 border border-indigo-200 rounded-lg">
+                  <button
+                    onClick={() => setMarathonPaused((p) => !p)}
+                    className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border transition-colors bg-white border-indigo-300 text-indigo-700 hover:bg-indigo-100"
+                  >
+                    {marathonPaused ? '▶ 继续' : '⏸ 暂停'}
+                  </button>
+                  <span className="text-xs text-indigo-600 font-medium">
+                    马拉松模式 — {marathonPaused ? '已暂停' : '自动续写中'}(第 {chapters.length + 1}/{marathonTarget > 0 ? marathonTarget : '∞'} 章)
+                  </span>
+                </div>
+              ) : (
+                /* Inactive marathon mode — checkbox + target input */
+                <div className="flex items-center gap-3 px-3 py-2 bg-gray-50 border border-gray-200 rounded-lg">
+                  <label className="flex items-center gap-2 cursor-pointer">
+                    <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">马拉松模式</span>
+                  </label>
+                  <div className="flex items-center gap-1.5">
+                    <span className="text-xs text-gray-500">目标:</span>
+                    <input
+                      type="number"
+                      min={0}
+                      value={marathonTarget || ''}
+                      onChange={(e) => {
+                        const val = parseInt(e.target.value, 10);
+                        setMarathonTarget(isNaN(val) || val < 0 ? 0 : val);
+                      }}
+                      placeholder="∞"
+                      className="w-16 px-2 py-1 text-xs text-gray-700 border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-indigo-500"
+                    />
+                    <span className="text-xs text-gray-500">章</span>
+                  </div>
+                </div>
+              )}
+
+              {/* Pulsing indicator when marathon is active and pipeline is not showing */}
+              {marathonMode && !showPipeline && (
+                <div className="flex items-center gap-2 text-xs text-indigo-500 animate-pulse">
+                  <span className="relative flex h-2 w-2">
+                    <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-indigo-400 opacity-75" />
+                    <span className="relative inline-flex rounded-full h-2 w-2 bg-indigo-500" />
+                  </span>
+                  {marathonPaused ? '马拉松模式已暂停 — 点击"继续"恢复自动写作' : '自动写作中 — 即将开始下一章'}
+                </div>
+              )}
             </div>
 
             {/* Tab navigation */}