Files
writer-work-flow/apps/web/components/toolbox/ToolboxPage.tsx
Yaojia Wang 33a9b7656c 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、漂移不盲替换。
2026-07-09 17:27:14 +02:00

103 lines
3.5 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 { useCallback, useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import { Sparkles } from "lucide-react";
import { AppShell } from "@/components/AppShell";
import { EmptyState } from "@/components/ui/EmptyState";
import { PageHeader } from "@/components/ui/PageHeader";
import type { ProjectResponse, ToolDescriptorView } from "@/lib/api/types";
import { isLegacyTool, resolveLegacyRoute } from "@/lib/toolbox/toolbox";
import { useAiConversation } from "@/lib/workbench/useAiConversation";
import { ToolCard } from "./ToolCard";
import { GeneratorRunner } from "./GeneratorRunner";
interface ToolboxPageProps {
project: ProjectResponse;
tools: ToolDescriptorView[];
// 命令面板 action-gen-<key> 深链:进页直接打开该工具的 runner仅新工具
initialOpenKey?: string;
}
// 创作工具箱(通用生成器框架):声明驱动的卡片栅格。
// legacy 工具(世界观/人设/大纲)跳现有页面保其更丰富的入库流;新工具开 GeneratorRunner。
export function ToolboxPage({
project,
tools,
initialOpenKey,
}: ToolboxPageProps) {
const router = useRouter();
// 工具箱页无抽屉silent 直投——项目级(chapter_no=null)留痕,不 GET、失败不 toast。
const conversation = useAiConversation(project.id, 0, { silent: true });
const findOpenable = useCallback(
(key: string | undefined): ToolDescriptorView | null => {
if (!key) return null;
const tool = tools.find((t) => t.key === key);
return tool && !isLegacyTool(tool) ? tool : null;
},
[tools],
);
const [active, setActive] = useState<ToolDescriptorView | null>(() =>
findOpenable(initialOpenKey),
);
const onOpen = useCallback(
(tool: ToolDescriptorView): void => {
if (isLegacyTool(tool) && tool.legacy_route) {
router.push(resolveLegacyRoute(tool.legacy_route, project.id));
return;
}
setActive(tool);
},
[router, project.id],
);
const sorted = useMemo(
// 新工具靠前更想被发现legacy 收后;同组保持后端顺序。
() => [...tools].sort((a, b) => Number(a.is_legacy) - Number(b.is_legacy)),
[tools],
);
return (
<AppShell
title={`${project.title}`}
subtitle="工具箱"
projectId={project.id}
activeNav="toolbox"
>
<div className="mx-auto flex max-w-5xl flex-col gap-6 p-6">
<PageHeader
title="创作工具箱"
eyebrow="generator toolbox"
description="脑洞、书名、简介、名字、金手指、词条、黄金开篇与细纲。每个工具都先生成结构化预览,可入库内容会经过一致性预检。"
/>
{active ? (
<GeneratorRunner
projectId={project.id}
tool={active}
appendMessages={conversation.appendMessages}
logChapterNo={null}
onClose={() => setActive(null)}
/>
) : sorted.length === 0 ? (
<EmptyState
icon={Sparkles}
title="暂无可用生成器"
description="后端暂未返回工具描述符。工具箱会在描述符可用后自动渲染卡片与输入表单。"
/>
) : (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{sorted.map((tool) => (
<ToolCard key={tool.key} tool={tool} onOpen={onOpen} />
))}
</div>
)}
</div>
</AppShell>
);
}