This commit is contained in:
hupenglong1
2026-05-20 15:04:19 +08:00
parent c4b935692c
commit 1a94cec822
27 changed files with 4831 additions and 1693 deletions
+228
View File
@@ -0,0 +1,228 @@
"use client";
import { useCallback, useEffect, useState } from "react";
export const SESSION_UPDATED_EVENT = "claw-session-updated";
const APPLIED_STORAGE_PREFIX = "claw.training.applied:";
const MODEL_ITERATION_SKILL_NAMES = new Set(["model-iteration"]);
const SKILL_POLL_MS = 8000;
export function useModelIterationEnabled(): {
enabled: boolean;
loading: boolean;
} {
const [enabled, setEnabled] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
const tick = async () => {
try {
const res = await fetch("/api/claw/skills", { cache: "no-store" });
if (!res.ok) {
if (!cancelled) {
setEnabled(false);
setLoading(false);
}
return;
}
const skills = await res.json();
if (!Array.isArray(skills)) {
if (!cancelled) {
setEnabled(false);
setLoading(false);
}
return;
}
const isOn = skills.some(
(s: { name?: string; enabled?: boolean }) =>
MODEL_ITERATION_SKILL_NAMES.has(String(s?.name ?? "")) &&
s?.enabled !== false,
);
if (!cancelled) {
setEnabled(isOn);
setLoading(false);
}
} catch {
if (!cancelled) {
setEnabled(false);
setLoading(false);
}
}
};
tick();
const id = window.setInterval(tick, SKILL_POLL_MS);
const onFocus = () => tick();
window.addEventListener("focus", onFocus);
return () => {
cancelled = true;
window.clearInterval(id);
window.removeEventListener("focus", onFocus);
};
}, []);
return { enabled, loading };
}
// applied 状态用 localStorage(按 session id)持久化:
// - 关 tab / 浏览器重启都不丢
// - 刷新页面也保持
// 不走后端 PATCH 是为了避开 __LOCALID_* 临时 session id 导致 404
export function useAppliedTraining(sessionId: string | null) {
const [applied, setApplied] = useState(false);
useEffect(() => {
if (!sessionId || typeof window === "undefined") {
setApplied(false);
return;
}
const stored =
window.localStorage.getItem(APPLIED_STORAGE_PREFIX + sessionId) === "1";
setApplied(stored);
}, [sessionId]);
const setAppliedPersist = useCallback(
(next: boolean) => {
setApplied(next);
if (typeof window === "undefined") return;
if (!sessionId) return;
const key = APPLIED_STORAGE_PREFIX + sessionId;
if (next) {
window.localStorage.setItem(key, "1");
} else {
window.localStorage.removeItem(key);
}
},
[sessionId],
);
return { applied, setApplied: setAppliedPersist };
}
export type ProgramStatus = {
available: boolean;
path?: string;
last_modified_ms?: number | null;
error?: string;
};
const PROGRAM_STATUS_POLL_MS = 5000;
export function useProgramStatus(): ProgramStatus & { loading: boolean } {
const [status, setStatus] = useState<ProgramStatus>({ available: false });
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
const tick = async () => {
try {
const res = await fetch("/api/claw/training/program-status", {
cache: "no-store",
});
if (!res.ok) {
if (!cancelled) {
setStatus({ available: false });
setLoading(false);
}
return;
}
const payload = (await res.json()) as ProgramStatus;
if (!cancelled) {
setStatus(payload);
setLoading(false);
}
} catch {
if (!cancelled) {
setStatus({ available: false });
setLoading(false);
}
}
};
tick();
const id = window.setInterval(tick, PROGRAM_STATUS_POLL_MS);
const onFocus = () => tick();
window.addEventListener("focus", onFocus);
return () => {
cancelled = true;
window.clearInterval(id);
window.removeEventListener("focus", onFocus);
};
}, []);
return { ...status, loading };
}
type SessionUpdatedDetail = {
sessionId: string;
is_training?: boolean;
};
export function dispatchSessionUpdated(detail: SessionUpdatedDetail) {
if (typeof window === "undefined") return;
window.dispatchEvent(
new CustomEvent<SessionUpdatedDetail>(SESSION_UPDATED_EVENT, { detail }),
);
}
export function useTrainingMode(sessionId: string | null) {
const [isTraining, setIsTraining] = useState(false);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
setLoaded(false);
if (!sessionId) {
setIsTraining(false);
return;
}
let cancelled = false;
fetch(`/api/claw/sessions/${encodeURIComponent(sessionId)}`, {
cache: "no-store",
})
.then(async (res) => {
if (!res.ok) return null;
return res.json();
})
.then((payload) => {
if (cancelled) return;
const next = Boolean(payload?.is_training);
setIsTraining(next);
setLoaded(true);
})
.catch(() => {
if (!cancelled) setLoaded(true);
});
return () => {
cancelled = true;
};
}, [sessionId]);
const setTraining = useCallback(
async (next: boolean) => {
if (!sessionId) return;
const previous = isTraining;
setIsTraining(next);
try {
const res = await fetch(
`/api/claw/sessions/${encodeURIComponent(sessionId)}`,
{
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ is_training: next }),
},
);
if (!res.ok) {
setIsTraining(previous);
return;
}
dispatchSessionUpdated({ sessionId, is_training: next });
} catch {
setIsTraining(previous);
}
},
[sessionId, isTraining],
);
return { isTraining, setTraining, loaded };
}