Files
writer-work-flow/apps/web/lib/outline/useOutline.ts

69 lines
2.6 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, useState } from "react";
import { api } from "@/lib/api/client";
import { useToast } from "@/components/Toast";
import {
friendlyFromApiError,
type FriendlyError,
} from "@/lib/errors/messages";
import type { OutlineChapterView } from "@/lib/api/types";
export type OutlineStatus = "idle" | "generating" | "ready" | "error";
export interface UseOutline {
chapters: OutlineChapterView[];
status: OutlineStatus;
// 直接透传 friendlyFromApiError 的结果(含 text + 可选 actionHref/actionLabel
// 不再拍平成固定的内部 codeUI 据 actionHref 是否存在决定显示跳转链接。
error: FriendlyError | null;
generate: (projectId: string, volume: number) => Promise<void>;
}
// 生成大纲POST .../outlinebody {volume})。无凭据/网关失败 → 503 LLM_UNAVAILABLE
// (流前 JSON 信封),经 error 检出、引导去设置(同 draft/review 范式)。
export function useOutline(initial: OutlineChapterView[]): UseOutline {
const [chapters, setChapters] = useState<OutlineChapterView[]>(initial);
const [status, setStatus] = useState<OutlineStatus>(
initial.length > 0 ? "ready" : "idle",
);
const [error, setError] = useState<FriendlyError | null>(null);
const toast = useToast();
const generate = useCallback<UseOutline["generate"]>(
async (projectId, volume) => {
setStatus("generating");
setError(null);
const { data, error: apiError } = await api.POST(
"/projects/{project_id}/outline",
{
params: { path: { project_id: projectId } },
body: { volume },
},
);
if (apiError || !data) {
// surface 真实错因(如 LLM_UNAVAILABLE→去设置 provider422→校验文案
// 而非永远的通用「大纲生成失败」QA #2原 env.error.code 漏读 422 的 detail 信封)。
// 直接透传 friendly保留 actionHref/actionLabel不再拍平成固定 OUTLINE_FAILED。
const friendly = friendlyFromApiError(apiError);
setError(friendly);
setStatus("error");
toast(friendly.text, "error");
return;
}
// 只替换目标卷的条目保留其它卷「AI 排大纲」是单卷操作,整列覆盖会误删别卷)。
const generated = data.chapters ?? [];
setChapters((prev) => [
...prev.filter((ch) => ch.volume !== volume),
...generated,
]);
setStatus("ready");
toast("大纲已生成", "success");
},
[toast],
);
return { chapters, status, error, generate };
}