Files
hkuds--deeptutor/web/hooks/useKnowledgeHistory.ts
wehub-resource-sync e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

148 lines
4.0 KiB
TypeScript

"use client";
import { useCallback, useEffect, useState } from "react";
import type { TaskKind } from "@/hooks/useKnowledgeProgress";
export interface HistoryEntry {
id: string;
taskId: string;
kind: TaskKind;
label: string;
status: "completed" | "error";
startedAt: number;
completedAt: number;
fileCount?: number;
error?: string | null;
/** Tail of log lines (capped) for retrospective inspection. */
logTail: string[];
}
const STORAGE_KEY = "knowledge:history:v1";
const MAX_PER_KB = 20;
const MAX_LOG_LINES = 80;
const MAX_LOG_CHARS = 8 * 1024;
interface HistoryStore {
byKb: Record<string, HistoryEntry[]>;
}
function readStore(): HistoryStore {
if (typeof window === "undefined") return { byKb: {} };
try {
const raw = window.localStorage.getItem(STORAGE_KEY);
if (!raw) return { byKb: {} };
const parsed = JSON.parse(raw) as HistoryStore;
if (parsed && typeof parsed === "object" && parsed.byKb) {
return { byKb: parsed.byKb };
}
} catch {
// corrupted or unavailable — fall through
}
return { byKb: {} };
}
function writeStore(store: HistoryStore) {
if (typeof window === "undefined") return;
try {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(store));
} catch {
// quota exceeded; ignore
}
}
function trimLogs(logs: string[]): string[] {
const tail = logs.slice(-MAX_LOG_LINES);
// Also bound total size in case lines are huge.
let total = 0;
const out: string[] = [];
for (let i = tail.length - 1; i >= 0; i--) {
const line = tail[i];
total += line.length + 1;
if (total > MAX_LOG_CHARS) break;
out.unshift(line);
}
return out;
}
export function useKnowledgeHistory() {
const [store, setStore] = useState<HistoryStore>({ byKb: {} });
// Hydrate from localStorage on mount. The initial render must be empty so
// SSR and the first client render match — only after hydration can we read
// browser storage.
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setStore(readStore());
}, []);
// Cross-tab sync: refresh when storage changes elsewhere.
useEffect(() => {
if (typeof window === "undefined") return;
const handler = (event: StorageEvent) => {
if (event.key !== STORAGE_KEY) return;
setStore(readStore());
};
window.addEventListener("storage", handler);
return () => window.removeEventListener("storage", handler);
}, []);
const append = useCallback(
(kbName: string, entry: Omit<HistoryEntry, "id">) => {
setStore((prev) => {
const existing = prev.byKb[kbName] ?? [];
// Dedupe by taskId — final progress events sometimes fire twice.
if (existing.some((e) => e.taskId === entry.taskId)) {
return prev;
}
const next: HistoryEntry = {
...entry,
logTail: trimLogs(entry.logTail || []),
id: `${entry.taskId}:${entry.completedAt}`,
};
const updated = [next, ...existing].slice(0, MAX_PER_KB);
const newStore = {
byKb: { ...prev.byKb, [kbName]: updated },
};
writeStore(newStore);
return newStore;
});
},
[],
);
const renameKb = useCallback((from: string, to: string) => {
setStore((prev) => {
if (!(from in prev.byKb)) return prev;
const newByKb = { ...prev.byKb };
newByKb[to] = newByKb[from];
delete newByKb[from];
const next = { byKb: newByKb };
writeStore(next);
return next;
});
}, []);
const removeKb = useCallback((kbName: string) => {
setStore((prev) => {
if (!(kbName in prev.byKb)) return prev;
const newByKb = { ...prev.byKb };
delete newByKb[kbName];
const next = { byKb: newByKb };
writeStore(next);
return next;
});
}, []);
const clearKb = useCallback((kbName: string) => removeKb(kbName), [removeKb]);
return {
historyByKb: store.byKb,
append,
renameKb,
removeKb,
clearKb,
};
}
export type UseKnowledgeHistoryReturn = ReturnType<typeof useKnowledgeHistory>;