import {
isModelId,
type ChatMessage,
type Conversation,
type MessageRole,
type MessageStatus,
} from "@/lib/chat/types";
const STORAGE_PREFIX = "ai-control-center:chats:";
const MAX_CONVERSATIONS = 50;
const ISO_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
const messageRoles = new Set<MessageRole>(["user", "assistant"]);
const messageStatuses = new Set<MessageStatus>(["streaming", "complete", "error", "stopped"]);
export function storageKey(userId: string): string {
return `${STORAGE_PREFIX}${encodeURIComponent(userId)}`;
}
export function loadConversations(userId: string): Conversation[] {
try {
if (typeof window === "undefined") return [];
const serialized = window.localStorage.getItem(storageKey(userId));
if (serialized === null) return [];
const value: unknown = JSON.parse(serialized);
if (!Array.isArray(value) || !value.every(isConversation)) return [];
return value.toSorted((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
} catch {
return [];
}
}
export function saveConversations(userId: string, conversations: Conversation[]): void {
try {
if (typeof window === "undefined") return;
const latestConversations = conversations
.filter(isConversation)
.toSorted((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))
.slice(0, MAX_CONVERSATIONS);
window.localStorage.setItem(storageKey(userId), JSON.stringify(latestConversations));
} catch {
// Browser storage can be disabled or unavailable; persistence is best effort.
}
}
function isConversation(value: unknown): value is Conversation {
if (!isRecord(value)) return false;
return (
isNonEmptyString(value.id) &&
typeof value.title === "string" &&
isModelId(value.modelId) &&
isIsoDate(value.updatedAt) &&
Array.isArray(value.messages) &&
value.messages.every(isChatMessage)
);
}
function isChatMessage(value: unknown): value is ChatMessage {
if (!isRecord(value)) return false;
return (
isNonEmptyString(value.id) &&
isMessageRole(value.role) &&
typeof value.content === "string" &&
isIsoDate(value.createdAt) &&
isMessageStatus(value.status)
);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isNonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.length > 0;
}
function isIsoDate(value: unknown): value is string {
if (typeof value !== "string" || !ISO_TIMESTAMP.test(value)) return false;
const date = new Date(value);
return !Number.isNaN(date.getTime()) && date.toISOString() === value;
}
function isMessageRole(value: unknown): value is MessageRole {
return typeof value === "string" && messageRoles.has(value as MessageRole);
}
function isMessageStatus(value: unknown): value is MessageStatus {
return typeof value === "string" && messageStatuses.has(value as MessageStatus);
}