Files
writer-work-flow/apps/web/components/workbench/ContinuePanel.tsx
Yaojia Wang 9195cf36f3 feat(frontend): 编辑器内「续写」——读本章前文生成多候选,点选插入正文
Phase 1(写作工作台重构):编辑器「续写」按钮复用 continue 生成器(with_prior_chapter
自动读该章已写正文承接),每次产出一条候选并累加成多候选(useContinue,已单测),
「再来一个」可多生成几版;作者点某条「插入正文」才追加到章末(HITL,不静默写入)。
续写前 flush 自动保存,确保读到最新草稿。润色/续写结果卡互斥不并占。
2026-07-07 19:41:57 +02:00

105 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useEffect, useRef } from "react";
import { Check, Plus, X } from "lucide-react";
import { ThinkingIndicator } from "@/components/ThinkingIndicator";
import { Button } from "@/components/ui/Button";
import { SectionHeader } from "@/components/ui/SectionHeader";
import { StatusNote } from "@/components/ui/StatusNote";
import { useContinue } from "@/lib/workbench/useContinue";
interface ContinuePanelProps {
projectId: string;
chapterNo: number;
// 插入选中候选到正文(追加到章末)。
onInsert: (text: string) => void;
onClose: () => void;
}
// 续写结果卡(内联,非模态):打开即读该章前文续写一条候选;可「再来一个」累加多候选,
// 作者点某条「插入正文」才落回草稿HITL不静默写入
export function ContinuePanel({
projectId,
chapterNo,
onInsert,
onClose,
}: ContinuePanelProps) {
const { status, candidates, generate } = useContinue();
const ranRef = useRef(false);
useEffect(() => {
if (ranRef.current) return;
ranRef.current = true;
void generate(projectId, chapterNo);
}, [projectId, chapterNo, generate]);
const busy = status === "generating";
return (
<div className="border-b border-line bg-panel px-4 py-3 sm:px-6">
<div className="flex items-center justify-between gap-3">
<SectionHeader
title="续写候选"
description="承接本章前文续写;点「再来一个」多生成几版,选中意的插入正文。"
/>
<Button onClick={onClose} variant="ghost" size="icon" aria-label="关闭续写">
<X className="h-4 w-4" aria-hidden="true" />
</Button>
</div>
{candidates.length === 0 && busy ? (
<div className="mt-3">
<ThinkingIndicator label="续写中" className="text-xs text-cinnabar" />
</div>
) : null}
{candidates.length === 0 && status === "error" ? (
<StatusNote variant="danger" className="mt-3 text-xs" title="续写失败">
<button
type="button"
onClick={() => void generate(projectId, chapterNo)}
className="mt-1 inline-flex items-center gap-1 text-cinnabar hover:underline"
>
</button>
</StatusNote>
) : null}
<ul className="mt-3 space-y-2">
{candidates.map((text, i) => (
<li
key={i}
className="rounded border border-line bg-bg p-3"
>
<div className="mb-1 flex items-center justify-between gap-2">
<span className="text-2xs text-ink-soft"> {i + 1}</span>
<Button
onClick={() => onInsert(text)}
variant="primary"
size="sm"
>
<Check className="h-4 w-4" aria-hidden="true" />
</Button>
</div>
<p className="whitespace-pre-wrap text-sm text-ink">{text}</p>
</li>
))}
</ul>
<div className="mt-3">
<Button
onClick={() => void generate(projectId, chapterNo)}
disabled={busy}
variant="secondary"
size="sm"
>
<Plus className="h-4 w-4" aria-hidden="true" />
</Button>
</div>
</div>
);
}