Files
writer-work-flow/apps/web/components/toolbox/ToolboxPage.tsx
2026-06-28 07:31:20 +02:00

98 lines
3.1 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 { 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();
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}
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>
);
}