feat(frontend): 五类交互接入 AI 对话留痕 + Workbench 抽屉入口(AC-3)
- RefinePanel/ChapterRewritePanel:结束后 append [author(意见/答复), ai(产出)];
clarify buffer-until-concluded——澄清 Q&A(原意见→反问→答复)缓冲,随最终产出批
一起落库同 thread;纯放弃不记。refine ai 行 meta 存 version_no+原选段(每版重锚)。
- ContinuePanel:ai-only(续写无作者输入),每候选一条 ai 气泡 meta{candidate_index}。
- GeneratorRunner:成功后 append [author(字段摘要, meta{tool_key,input_fields}),
ai(渲染预览, meta{tool_key,output_kind})];空产物不记。lib/toolbox/aiSummary.ts 纯助手+单测。
- InlineToolbox→GeneratorRunner 透传 append+本章号;ToolboxPage 用 silent 直投项目级(chapter_no=null)。
- Workbench:挂 useAiConversation 单实例 + AiConversationDrawer + 底栏「AI 对话」入口(flag 灰度),
三个 DRAFT-only 再接受回调(替换整章/插入正文/回填选段),成功关抽屉+toast、漂移不盲替换。
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { Check, Plus, X } from "lucide-react";
|
||||
|
||||
import { ThinkingIndicator } from "@/components/ThinkingIndicator";
|
||||
@@ -8,12 +8,15 @@ import { Button } from "@/components/ui/Button";
|
||||
import { SectionHeader } from "@/components/ui/SectionHeader";
|
||||
import { StatusNote } from "@/components/ui/StatusNote";
|
||||
import { useContinue } from "@/lib/workbench/useContinue";
|
||||
import type { AiTurnInput } from "@/lib/workbench/useAiConversation";
|
||||
|
||||
interface ContinuePanelProps {
|
||||
projectId: string;
|
||||
chapterNo: number;
|
||||
// 插入选中候选到正文(追加到章末)。
|
||||
onInsert: (text: string) => void;
|
||||
// 留痕:每条候选记一条 ai 气泡(续写无作者输入,故无 author 气泡)。
|
||||
appendMessages: (turn: AiTurnInput) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@@ -23,16 +26,37 @@ export function ContinuePanel({
|
||||
projectId,
|
||||
chapterNo,
|
||||
onInsert,
|
||||
appendMessages,
|
||||
onClose,
|
||||
}: ContinuePanelProps) {
|
||||
const { status, candidates, generate } = useContinue();
|
||||
const ranRef = useRef(false);
|
||||
// 本面板一次会话 = 一个 thread;多条候选归此组,candidate_index 本地累加。
|
||||
const threadId = useRef(crypto.randomUUID()).current;
|
||||
const candidateIdxRef = useRef(0);
|
||||
|
||||
// 生成一条候选并(成功时)留痕:ai-only,meta 存候选序号。
|
||||
const runGenerate = useCallback(async (): Promise<void> => {
|
||||
const text = await generate(projectId, chapterNo);
|
||||
if (!text) return;
|
||||
const candidateIndex = candidateIdxRef.current;
|
||||
candidateIdxRef.current += 1;
|
||||
appendMessages({
|
||||
threadId,
|
||||
kind: "continue",
|
||||
toolKey: "continue",
|
||||
chapterNo,
|
||||
messages: [
|
||||
{ role: "ai", content: text, meta: { candidate_index: candidateIndex } },
|
||||
],
|
||||
});
|
||||
}, [generate, projectId, chapterNo, appendMessages, threadId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (ranRef.current) return;
|
||||
ranRef.current = true;
|
||||
void generate(projectId, chapterNo);
|
||||
}, [projectId, chapterNo, generate]);
|
||||
void runGenerate();
|
||||
}, [runGenerate]);
|
||||
|
||||
const busy = status === "generating";
|
||||
|
||||
@@ -58,7 +82,7 @@ export function ContinuePanel({
|
||||
<StatusNote variant="danger" className="mt-3 text-xs" title="续写失败">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void generate(projectId, chapterNo)}
|
||||
onClick={() => void runGenerate()}
|
||||
className="mt-1 inline-flex items-center gap-1 text-cinnabar hover:underline"
|
||||
>
|
||||
重试
|
||||
@@ -90,7 +114,7 @@ export function ContinuePanel({
|
||||
|
||||
<div className="mt-3">
|
||||
<Button
|
||||
onClick={() => void generate(projectId, chapterNo)}
|
||||
onClick={() => void runGenerate()}
|
||||
disabled={busy}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
|
||||
Reference in New Issue
Block a user