206 lines
4.7 KiB
TypeScript
206 lines
4.7 KiB
TypeScript
/**
|
|
* Local storage utilities for chat persistence
|
|
* Provides fast local access while syncing with the database
|
|
*/
|
|
|
|
interface Message {
|
|
id: string;
|
|
role: 'user' | 'assistant';
|
|
content: string;
|
|
}
|
|
|
|
interface Chat {
|
|
id: string;
|
|
title: string;
|
|
updatedAt: string;
|
|
messages: Message[];
|
|
pinned?: boolean;
|
|
}
|
|
|
|
const CHATS_STORAGE_KEY = 'chat-gpz-chats';
|
|
|
|
/**
|
|
* Get all chats from localStorage
|
|
*/
|
|
export function getLocalChats(): Chat[] {
|
|
if (typeof window === 'undefined') {
|
|
return [];
|
|
}
|
|
try {
|
|
const stored = localStorage.getItem(CHATS_STORAGE_KEY);
|
|
if (!stored) {
|
|
return [];
|
|
}
|
|
return JSON.parse(stored) as Chat[];
|
|
} catch (e) {
|
|
console.error('Failed to parse local chats:', e);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save all chats to localStorage
|
|
*/
|
|
export function setLocalChats(chats: Chat[]): void {
|
|
if (typeof window === 'undefined') {
|
|
return;
|
|
}
|
|
try {
|
|
localStorage.setItem(CHATS_STORAGE_KEY, JSON.stringify(chats));
|
|
} catch (e) {
|
|
console.error('Failed to save local chats:', e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a single chat by ID from localStorage
|
|
*/
|
|
export function getLocalChat(chatId: string): Chat | null {
|
|
const chats = getLocalChats();
|
|
return chats.find((c) => c.id === chatId) || null;
|
|
}
|
|
|
|
/**
|
|
* Save or update a single chat in localStorage
|
|
*/
|
|
export function saveLocalChat(chat: Chat): void {
|
|
const chats = getLocalChats();
|
|
const existingIndex = chats.findIndex((c) => c.id === chat.id);
|
|
|
|
if (existingIndex >= 0) {
|
|
chats[existingIndex] = chat;
|
|
} else {
|
|
chats.unshift(chat); // Add to beginning (most recent)
|
|
}
|
|
|
|
setLocalChats(chats);
|
|
}
|
|
|
|
/**
|
|
* Add a message to an existing chat in localStorage
|
|
*/
|
|
export function addMessageToLocalChat(chatId: string, message: Message): void {
|
|
const chats = getLocalChats();
|
|
const chat = chats.find((c) => c.id === chatId);
|
|
|
|
if (chat) {
|
|
chat.messages.push(message);
|
|
chat.updatedAt = new Date().toISOString();
|
|
setLocalChats(chats);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete a chat from localStorage
|
|
*/
|
|
export function deleteLocalChat(chatId: string): void {
|
|
const chats = getLocalChats();
|
|
const filtered = chats.filter((c) => c.id !== chatId);
|
|
setLocalChats(filtered);
|
|
}
|
|
|
|
/**
|
|
* Merge remote chats with local chats
|
|
* Remote chats take precedence for conflicts (based on updatedAt)
|
|
*/
|
|
export function mergeChats(localChats: Chat[], remoteChats: Chat[]): Chat[] {
|
|
const chatMap = new Map<string, Chat>();
|
|
|
|
// Add local chats first
|
|
for (const chat of localChats) {
|
|
chatMap.set(chat.id, chat);
|
|
}
|
|
|
|
// Override with remote chats (they're the source of truth)
|
|
for (const chat of remoteChats) {
|
|
const local = chatMap.get(chat.id);
|
|
if (!local || new Date(chat.updatedAt) >= new Date(local.updatedAt)) {
|
|
chatMap.set(chat.id, chat);
|
|
}
|
|
}
|
|
|
|
// Sort by updatedAt descending
|
|
return Array.from(chatMap.values()).sort(
|
|
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Clear all local chats (useful for logout)
|
|
*/
|
|
export function clearLocalChats(): void {
|
|
if (typeof window === 'undefined') {
|
|
return;
|
|
}
|
|
localStorage.removeItem(CHATS_STORAGE_KEY);
|
|
}
|
|
|
|
// ============================================
|
|
// Scroll Position Storage
|
|
// ============================================
|
|
|
|
const SCROLL_POSITIONS_KEY = 'chat-gpz-scroll-positions';
|
|
|
|
interface ScrollPositions {
|
|
[chatId: string]: number;
|
|
}
|
|
|
|
/**
|
|
* Get all stored scroll positions
|
|
*/
|
|
function getScrollPositions(): ScrollPositions {
|
|
if (typeof window === 'undefined') {
|
|
return {};
|
|
}
|
|
try {
|
|
const stored = localStorage.getItem(SCROLL_POSITIONS_KEY);
|
|
if (!stored) {
|
|
return {};
|
|
}
|
|
return JSON.parse(stored) as ScrollPositions;
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save scroll positions to localStorage
|
|
*/
|
|
function setScrollPositions(positions: ScrollPositions): void {
|
|
if (typeof window === 'undefined') {
|
|
return;
|
|
}
|
|
try {
|
|
localStorage.setItem(SCROLL_POSITIONS_KEY, JSON.stringify(positions));
|
|
} catch {
|
|
// Ignore storage errors
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get scroll position for a specific chat
|
|
* Returns null if no position saved (indicating should scroll to bottom for new chats)
|
|
*/
|
|
export function getScrollPosition(chatId: string): number | null {
|
|
const positions = getScrollPositions();
|
|
return positions[chatId] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Save scroll position for a specific chat
|
|
*/
|
|
export function saveScrollPosition(chatId: string, position: number): void {
|
|
const positions = getScrollPositions();
|
|
positions[chatId] = position;
|
|
setScrollPositions(positions);
|
|
}
|
|
|
|
/**
|
|
* Delete scroll position for a chat (when chat is deleted)
|
|
*/
|
|
export function deleteScrollPosition(chatId: string): void {
|
|
const positions = getScrollPositions();
|
|
delete positions[chatId];
|
|
setScrollPositions(positions);
|
|
}
|