Files
writer-work-flow/apps/web/lib/workbench/useInjection.ts

163 lines
5.3 KiB
TypeScript
Raw Permalink 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, useEffect, useRef, useState } from "react";
import { api } from "@/lib/api/client";
import {
currentOverride,
withExcluded,
withPinToggled,
withRecentN,
withRestored,
type InjectionEntityRef,
type InjectionOverrideRequest,
type InjectionResponse,
} from "./injection";
export interface UseInjection {
data: InjectionResponse | null;
loading: boolean;
saving: boolean;
// 首次加载失败(满屏错误框)与每次保存失败(悬浮提示)独立追踪,互不覆盖。
loadError: string | null;
saveError: string | null;
clearSaveError: () => void;
reload: () => void;
retrySave: () => Promise<void>;
togglePin: (ref: InjectionEntityRef) => Promise<void>;
exclude: (ref: InjectionEntityRef) => Promise<void>;
restore: (ref: InjectionEntityRef) => Promise<void>;
setRecentN: (n: number) => Promise<void>;
}
const INJECTION_PATH =
"/projects/{project_id}/chapters/{chapter_no}/injection" as const;
// 本章注入透明B0 可控版):读确定性 SelectionTrace + 作者覆盖;
// pin/排除/recent_n 经 PUT 覆盖 → 服务端回放新的确定结果(看到的=写章用的)。
// 写失败给可读文案、不改本地状态(天然回滚),不抛。
export function useInjection(projectId: string, chapterNo: number): UseInjection {
const [data, setData] = useState<InjectionResponse | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [loadError, setLoadError] = useState<string | null>(null);
const [saveError, setSaveError] = useState<string | null>(null);
// 重载计数loadError「重试」自增 → 触发 effect 重新拉取(不需要把 loader 提到 effect 外)。
const [reloadNonce, setReloadNonce] = useState(0);
// 并发锁:保存在途时 ref 同步置位,拦下连点/快速切换造成的乱序覆盖state 异步不可靠)。
const savingRef = useRef(false);
// 记下最近一次提交的覆盖,供 saveError「重试」重放不重复构造 override
const lastOverrideRef = useRef<InjectionOverrideRequest | null>(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
setLoadError(null);
setSaveError(null);
void (async () => {
try {
const { data: body, error: err } = await api.GET(INJECTION_PATH, {
params: { path: { project_id: projectId, chapter_no: chapterNo } },
});
if (cancelled) return;
if (err || !body) {
setLoadError("注入信息暂不可用");
setData(null);
} else {
setData(body);
}
} catch {
// 网络层失败(后端不可达/CORSopenapi-fetch 直接抛而非返回 error 信封。
if (cancelled) return;
setLoadError("注入信息暂不可用");
setData(null);
} finally {
if (!cancelled) setLoading(false);
}
})();
return () => {
cancelled = true;
};
}, [projectId, chapterNo, reloadNonce]);
const save = useCallback(
async (override: InjectionOverrideRequest) => {
// 已有保存在途 → 早返回(避免并发 PUT 互相覆盖、回放结果乱序)。
if (savingRef.current) return;
lastOverrideRef.current = override;
savingRef.current = true;
setSaving(true);
setSaveError(null);
try {
const { data: body, error: err } = await api.PUT(INJECTION_PATH, {
params: { path: { project_id: projectId, chapter_no: chapterNo } },
body: override,
});
if (err || !body) {
// 保存失败:保留已加载的 data仅设保存态错误天然回滚 + 可重试)。
setSaveError("保存注入设置失败,请重试");
} else {
setData(body); // 以服务端确定结果为准(不变量 #6
}
} catch {
// 网络层失败:本地状态不动(天然回滚),给可读文案、不抛。
setSaveError("保存注入设置失败,请重试");
} finally {
savingRef.current = false;
setSaving(false);
}
},
[projectId, chapterNo],
);
const mutate = useCallback(
(fn: (o: InjectionOverrideRequest) => InjectionOverrideRequest) =>
async (): Promise<void> => {
if (!data) return;
await save(fn(currentOverride(data)));
},
[data, save],
);
const togglePin = useCallback(
(ref: InjectionEntityRef) => mutate((o) => withPinToggled(o, ref))(),
[mutate],
);
const exclude = useCallback(
(ref: InjectionEntityRef) => mutate((o) => withExcluded(o, ref))(),
[mutate],
);
const restore = useCallback(
(ref: InjectionEntityRef) => mutate((o) => withRestored(o, ref))(),
[mutate],
);
const setRecentN = useCallback(
(n: number) => mutate((o) => withRecentN(o, n))(),
[mutate],
);
const clearSaveError = useCallback(() => setSaveError(null), []);
const reload = useCallback(() => setReloadNonce((n) => n + 1), []);
const retrySave = useCallback(async () => {
const override = lastOverrideRef.current;
if (!override) return;
await save(override);
}, [save]);
return {
data,
loading,
saving,
loadError,
saveError,
clearSaveError,
reload,
retrySave,
togglePin,
exclude,
restore,
setRecentN,
};
}