Files
writer-work-flow/apps/web/components/workbench/InlineToolbox.tsx
Yaojia Wang 04f4785292 feat(frontend): 编辑器内联工具箱——就地调生成器(含金手指),结果一键插入正文
Phase 1(写作工作台重构):编辑器「工具箱」按钮打开内联面板,不离开写作界面即可调用
全部生成器(含金手指——后端 golden-finger 早已齐全,此处首次露出)。复用 GeneratorRunner,
为其加可选 onInsertText:文本类产物(开篇/简介/续写/扩写等)点「插入正文」直接追加到章末,
可入库类仍走既有 ingest。useToolboxTools 客户端拉描述符(已单测)。润色/续写/工具箱三卡互斥。
2026-07-07 19:48:13 +02:00

86 lines
3.3 KiB
TypeScript

"use client";
import { useState } from "react";
import { 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 { GeneratorRunner } from "@/components/toolbox/GeneratorRunner";
import type { ToolDescriptorView } from "@/lib/api/types";
import { isLegacyTool } from "@/lib/toolbox/toolbox";
import { useToolboxTools } from "@/lib/toolbox/useToolboxTools";
interface InlineToolboxProps {
projectId: string;
// 把生成结果插入正文(追加到章末)。
onInsertText: (text: string) => void;
onClose: () => void;
}
// 编辑器内联工具箱(不离开写作界面):列出生成器(含金手指),就地生成结构化预览,
// 文本类产物点「插入正文」直接填入写作处,可入库类走既有 ingest。legacy 生成器(世界观/
// 人设/大纲)仍走各自更丰富的独立页,不塞进内联面板。
export function InlineToolbox({
projectId,
onInsertText,
onClose,
}: InlineToolboxProps) {
const { status, tools } = useToolboxTools();
const [active, setActive] = useState<ToolDescriptorView | null>(null);
const openable = tools.filter((tool) => !isLegacyTool(tool));
return (
<div className="max-h-[70vh] overflow-auto 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>
{active ? (
<div className="mt-3">
<GeneratorRunner
projectId={projectId}
tool={active}
onClose={() => setActive(null)}
onInsertText={(text) => {
if (text.trim().length > 0) onInsertText(text);
}}
/>
</div>
) : status === "loading" ? (
<div className="mt-3">
<ThinkingIndicator label="加载工具箱" className="text-xs text-cinnabar" />
</div>
) : status === "error" ? (
<StatusNote variant="danger" className="mt-3 text-xs" title="工具箱加载失败">
<p></p>
</StatusNote>
) : openable.length === 0 ? (
<p className="mt-3 text-xs text-ink-soft"></p>
) : (
<ul className="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2">
{openable.map((tool) => (
<li key={tool.key}>
<button
type="button"
onClick={() => setActive(tool)}
className="w-full rounded border border-line bg-bg p-3 text-left transition-colors hover:border-cinnabar"
>
<p className="text-sm text-ink">{tool.title}</p>
<p className="mt-0.5 text-xs text-ink-soft">{tool.subtitle}</p>
</button>
</li>
))}
</ul>
)}
</div>
);
}