Files
storkit/frontend/src/hooks/useChatHistory.ts

118 lines
3.1 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from "react";
import type { Message } from "../types";
const STORAGE_KEY_PREFIX = "storykit-chat-history:";
const LIMIT_KEY_PREFIX = "storykit-chat-history-limit:";
const DEFAULT_LIMIT = 200;
function storageKey(projectPath: string): string {
return `${STORAGE_KEY_PREFIX}${projectPath}`;
}
function limitKey(projectPath: string): string {
return `${LIMIT_KEY_PREFIX}${projectPath}`;
}
function loadLimit(projectPath: string): number {
try {
const raw = localStorage.getItem(limitKey(projectPath));
if (raw === null) return DEFAULT_LIMIT;
const parsed = Number(raw);
if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_LIMIT;
return Math.floor(parsed);
} catch {
return DEFAULT_LIMIT;
}
}
function saveLimit(projectPath: string, limit: number): void {
try {
localStorage.setItem(limitKey(projectPath), String(limit));
} catch {
// Ignore — quota or security errors.
}
}
function loadMessages(projectPath: string): Message[] {
try {
const raw = localStorage.getItem(storageKey(projectPath));
if (!raw) return [];
const parsed: unknown = JSON.parse(raw);
if (!Array.isArray(parsed)) return [];
return parsed as Message[];
} catch {
return [];
}
}
function pruneMessages(messages: Message[], limit: number): Message[] {
if (limit === 0 || messages.length <= limit) return messages;
return messages.slice(-limit);
}
function saveMessages(
projectPath: string,
messages: Message[],
limit: number,
): void {
try {
const pruned = pruneMessages(messages, limit);
if (pruned.length === 0) {
localStorage.removeItem(storageKey(projectPath));
} else {
localStorage.setItem(storageKey(projectPath), JSON.stringify(pruned));
}
} catch (e) {
console.warn("Failed to persist chat history to localStorage:", e);
}
}
export function useChatHistory(projectPath: string) {
const [messages, setMessagesState] = useState<Message[]>(() =>
loadMessages(projectPath),
);
const [maxMessages, setMaxMessagesState] = useState<number>(() =>
loadLimit(projectPath),
);
const projectPathRef = useRef(projectPath);
// Keep the ref in sync so the effect closure always has the latest path.
projectPathRef.current = projectPath;
// Persist whenever messages or limit change.
useEffect(() => {
saveMessages(projectPathRef.current, messages, maxMessages);
}, [messages, maxMessages]);
const setMessages = useCallback(
(update: Message[] | ((prev: Message[]) => Message[])) => {
setMessagesState(update);
},
[],
);
const setMaxMessages = useCallback((limit: number) => {
setMaxMessagesState(limit);
saveLimit(projectPathRef.current, limit);
}, []);
const clearMessages = useCallback(() => {
setMessagesState([]);
// Eagerly remove from storage so clearSession doesn't depend on the
// effect firing before the component unmounts or re-renders.
try {
localStorage.removeItem(storageKey(projectPathRef.current));
} catch {
// Ignore — quota or security errors.
}
}, []);
return {
messages,
setMessages,
clearMessages,
maxMessages,
setMaxMessages,
} as const;
}