Files
writer-work-flow/apps/web/lib/workbench/useInjection.ts
Yaojia Wang 6449c3103a feat(ux): F1 注入面板可控版 — 📌置顶/✕排除/近N章步进器 + 已排除恢复
- injection.ts 加 override 纯函数(currentOverride/withPinToggled/withExcluded/withRestored/withRecentN/isPinned,钳 1..20)+ author_pin 徽标
- useInjection 加 saving 态 + togglePin/exclude/restore/setRecentN,PUT 后以服务端确定结果为准(不变量 #6);写失败可读文案、不改本地态(天然回滚)
- ChapterAssistant:每实体置顶/排除按钮 + 「近 N 章」步进器 + 「已排除」恢复区
- 13 新 vitest(override 纯函数);前端门禁绿(lint/tsc/vitest 183/build)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 12:01:09 +02:00

110 lines
3.2 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, useEffect, 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;
error: string | null;
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 [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
setError(null);
void (async () => {
const { data: body, error: err } = await api.GET(INJECTION_PATH, {
params: { path: { project_id: projectId, chapter_no: chapterNo } },
});
if (cancelled) return;
if (err || !body) {
setError("注入信息暂不可用");
setData(null);
} else {
setData(body);
}
setLoading(false);
})();
return () => {
cancelled = true;
};
}, [projectId, chapterNo]);
const save = useCallback(
async (override: InjectionOverrideRequest) => {
setSaving(true);
setError(null);
const { data: body, error: err } = await api.PUT(INJECTION_PATH, {
params: { path: { project_id: projectId, chapter_no: chapterNo } },
body: override,
});
if (err || !body) {
setError("保存注入设置失败,请重试");
} else {
setData(body); // 以服务端确定结果为准(不变量 #6
}
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],
);
return { data, loading, saving, error, togglePin, exclude, restore, setRecentN };
}