- 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、漂移不盲替换。
103 lines
3.5 KiB
TypeScript
103 lines
3.5 KiB
TypeScript
"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>
|
||
);
|
||
}
|