feat(frontend): 编辑器内联工具箱——就地调生成器(含金手指),结果一键插入正文
Phase 1(写作工作台重构):编辑器「工具箱」按钮打开内联面板,不离开写作界面即可调用 全部生成器(含金手指——后端 golden-finger 早已齐全,此处首次露出)。复用 GeneratorRunner, 为其加可选 onInsertText:文本类产物(开篇/简介/续写/扩写等)点「插入正文」直接追加到章末, 可入库类仍走既有 ingest。useToolboxTools 客户端拉描述符(已单测)。润色/续写/工具箱三卡互斥。
This commit is contained in:
52
apps/web/lib/toolbox/useToolboxTools.test.ts
Normal file
52
apps/web/lib/toolbox/useToolboxTools.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// @vitest-environment jsdom
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { useToolboxTools } from "./useToolboxTools";
|
||||
|
||||
const get = vi.fn();
|
||||
const toast = vi.fn();
|
||||
vi.mock("@/lib/api/client", () => ({
|
||||
api: { GET: (...a: unknown[]) => get(...a) },
|
||||
}));
|
||||
vi.mock("@/components/Toast", () => ({ useToast: () => toast }));
|
||||
|
||||
describe("useToolboxTools", () => {
|
||||
beforeEach(() => {
|
||||
get.mockReset();
|
||||
toast.mockReset();
|
||||
});
|
||||
afterEach(() => vi.clearAllMocks());
|
||||
|
||||
it("加载成功:拉全量描述符,status=ready", async () => {
|
||||
const tools = [{ key: "golden-finger", title: "金手指生成器", is_legacy: false }];
|
||||
get.mockResolvedValue({ data: { tools }, error: null });
|
||||
|
||||
const { result } = renderHook(() => useToolboxTools());
|
||||
await waitFor(() => expect(result.current.status).toBe("ready"));
|
||||
expect(result.current.tools).toEqual(tools);
|
||||
expect(get).toHaveBeenCalledWith("/skills/toolbox", {});
|
||||
expect(toast).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("data.tools 缺省时回退空数组", async () => {
|
||||
get.mockResolvedValue({ data: { tools: null }, error: null });
|
||||
const { result } = renderHook(() => useToolboxTools());
|
||||
await waitFor(() => expect(result.current.status).toBe("ready"));
|
||||
expect(result.current.tools).toEqual([]);
|
||||
});
|
||||
|
||||
it("后端 error:status=error、弹错误 toast", async () => {
|
||||
get.mockResolvedValue({ data: null, error: { detail: "boom" } });
|
||||
const { result } = renderHook(() => useToolboxTools());
|
||||
await waitFor(() => expect(result.current.status).toBe("error"));
|
||||
expect(toast).toHaveBeenCalledWith(expect.any(String), "error");
|
||||
});
|
||||
|
||||
it("请求抛异常:status=error、弹网络异常 toast", async () => {
|
||||
get.mockRejectedValue(new Error("network down"));
|
||||
const { result } = renderHook(() => useToolboxTools());
|
||||
await waitFor(() => expect(result.current.status).toBe("error"));
|
||||
expect(toast).toHaveBeenCalledWith("工具箱加载异常,请检查网络。", "error");
|
||||
});
|
||||
});
|
||||
48
apps/web/lib/toolbox/useToolboxTools.ts
Normal file
48
apps/web/lib/toolbox/useToolboxTools.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { api } from "@/lib/api/client";
|
||||
import { useToast } from "@/components/Toast";
|
||||
import type { ToolDescriptorView } from "@/lib/api/types";
|
||||
|
||||
// 客户端拉取创作工具箱全量描述符(GET /skills/toolbox),供编辑器内联工具箱调用。
|
||||
// 描述符只读、少变,进面板时拉一次即可。
|
||||
export type ToolboxStatus = "loading" | "ready" | "error";
|
||||
|
||||
export interface UseToolboxTools {
|
||||
status: ToolboxStatus;
|
||||
tools: ToolDescriptorView[];
|
||||
}
|
||||
|
||||
export function useToolboxTools(): UseToolboxTools {
|
||||
const [status, setStatus] = useState<ToolboxStatus>("loading");
|
||||
const [tools, setTools] = useState<ToolDescriptorView[]>([]);
|
||||
const toast = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void (async (): Promise<void> => {
|
||||
try {
|
||||
const { data, error } = await api.GET("/skills/toolbox", {});
|
||||
if (cancelled) return;
|
||||
if (error || !data) {
|
||||
setStatus("error");
|
||||
toast("工具箱加载失败,请重试。", "error");
|
||||
return;
|
||||
}
|
||||
setTools(data.tools ?? []);
|
||||
setStatus("ready");
|
||||
} catch {
|
||||
if (cancelled) return;
|
||||
setStatus("error");
|
||||
toast("工具箱加载异常,请检查网络。", "error");
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [toast]);
|
||||
|
||||
return { status, tools };
|
||||
}
|
||||
Reference in New Issue
Block a user