|
|
@@ -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 */}
|