"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { api } from "@/lib/api/client"; import { useToast } from "@/components/Toast"; import type { StyleFingerprintResponse } from "@/lib/api/types"; import { useJobPoll } from "@/lib/jobs/useJobPoll"; import { styleLearnResult } from "@/lib/jobs/job"; import { buildLearnRequest, normalizeFingerprint, type Fingerprint, type StyleLearnMode, } from "./style"; export interface UseStyleLearn { // 提交中(POST /style 受理)或轮询中。 busy: boolean; pollStatus: ReturnType["status"] | "idle"; progress: number; fingerprint: Fingerprint | null; // 学文风:POST /style → 拿 job_id → 轮询 → done 后拉最新指纹。 learn: ( projectId: string, samples: string[], mode: StyleLearnMode, ) => Promise; } function errorCode(error: unknown): string | undefined { if (typeof error !== "object" || error === null) return undefined; const env = error as { error?: { code?: unknown } }; return typeof env.error?.code === "string" ? env.error.code : undefined; } // 学文风编排:受理(202 job_id)→ useJobPoll 轮询 → done 拉 GET /style 展示指纹。 export function useStyleLearn(initial: Fingerprint | null): UseStyleLearn { const [fingerprint, setFingerprint] = useState(initial); const [submitting, setSubmitting] = useState(false); const [pollStatus, setPollStatus] = useState< ReturnType["status"] | "idle" >("idle"); const [projectId, setProjectId] = useState(null); const poll = useJobPoll(); const toast = useToast(); // 仅在提交过一次学文风后才反映轮询状态(initialPollState.status 默认 "polling")。 const startedRef = useRef(false); // 轮询完成 → 拉最新指纹;失败 → toast。 useEffect(() => { if (!startedRef.current) return; setPollStatus(poll.status); if (poll.status === "done" && projectId) { void refetchFingerprint(projectId).then((fp) => { if (fp) setFingerprint(fp); toast("文风指纹已更新。", "success"); }); } if (poll.status === "error") { toast(`学文风失败:${poll.error ?? "未知原因"}`, "error"); } // 仅在 poll.status 变化时反应。 // eslint-disable-next-line react-hooks/exhaustive-deps }, [poll.status]); const learn = useCallback( async (pid, samples, mode) => { setSubmitting(true); setProjectId(pid); try { const { data, error } = await api.POST("/projects/{project_id}/style", { params: { path: { project_id: pid } }, body: buildLearnRequest(samples, mode), }); if (error || !data) { const code = errorCode(error); toast( code === "LLM_UNAVAILABLE" ? "未配置提供商,请先去设置页连一家。" : "学文风受理失败,请稍后重试。", "error", ); return false; } startedRef.current = true; poll.poll(data.job_id); setPollStatus("polling"); return true; } finally { setSubmitting(false); } }, [poll, toast], ); return { busy: submitting || pollStatus === "polling", pollStatus, progress: poll.progress, fingerprint, learn, }; } // 客户端拉最新指纹(done 后刷新展示);404/失败 → null。 async function refetchFingerprint( projectId: string, ): Promise { const { data, error } = await api.GET("/projects/{project_id}/style", { params: { path: { project_id: projectId } }, }); if (error || !data) return null; return normalizeFingerprint(data as StyleFingerprintResponse); } // 仅供测试/复用:保证 job done result 的版本回显(不阻断 UI)。 export function learnSummary( poll: ReturnType, ): { version: number | null; dimsCount: number | null } | null { if (poll.status !== "done" || !poll.job) return null; return styleLearnResult(poll.job); }