This commit is contained in:
Zacharias-Brohn
2026-01-14 21:33:52 +01:00
parent 2e0432fd35
commit 2d68b5bff7
2 changed files with 185 additions and 13 deletions
+50 -13
View File
@@ -33,6 +33,14 @@ import { useDisclosure } from '@mantine/hooks';
import { getInstalledModels, type OllamaModel } from '@/app/actions/ollama'; import { getInstalledModels, type OllamaModel } from '@/app/actions/ollama';
import { useThemeContext } from '@/components/DynamicThemeProvider'; import { useThemeContext } from '@/components/DynamicThemeProvider';
import { SettingsModal } from '@/components/Settings/SettingsModal'; import { SettingsModal } from '@/components/Settings/SettingsModal';
import {
addMessageToLocalChat,
getLocalChat,
getLocalChats,
mergeChats,
saveLocalChat,
setLocalChats,
} from '@/lib/chatStorage';
import { MarkdownMessage } from './MarkdownMessage'; import { MarkdownMessage } from './MarkdownMessage';
import classes from './ChatLayout.module.css'; import classes from './ChatLayout.module.css';
@@ -155,22 +163,28 @@ export default function ChatLayout() {
}; };
const fetchChats = async () => { const fetchChats = async () => {
// Load from localStorage first for instant display
const localChats = getLocalChats();
if (localChats.length > 0) {
setChats(localChats);
}
// Then fetch from database and merge
setIsLoadingChats(true); setIsLoadingChats(true);
try { try {
const res = await fetch('/api/chats'); const res = await fetch('/api/chats');
if (res.ok) { if (res.ok) {
const data = await res.json(); const remoteChats = await res.json();
if (Array.isArray(data)) { if (Array.isArray(remoteChats)) {
setChats(data); // Merge local and remote, update both state and localStorage
} else { const merged = mergeChats(localChats, remoteChats);
setChats([]); setChats(merged);
setLocalChats(merged);
} }
} else {
setChats([]);
} }
} catch (e) { } catch (e) {
console.error('Failed to fetch chats', e); console.error('Failed to fetch chats from server:', e);
setChats([]); // Keep using local chats if server fails
} finally { } finally {
setIsLoadingChats(false); setIsLoadingChats(false);
} }
@@ -178,14 +192,27 @@ export default function ChatLayout() {
const handleSelectChat = (chat: Chat) => { const handleSelectChat = (chat: Chat) => {
setActiveChatId(chat.id); setActiveChatId(chat.id);
if (chat.messages) {
// Try to load from localStorage first (faster), fall back to passed chat data
const localChat = getLocalChat(chat.id);
if (localChat?.messages && localChat.messages.length > 0) {
setMessages(localChat.messages);
} else if (chat.messages) {
setMessages(chat.messages); setMessages(chat.messages);
} else { } else {
setMessages([]); setMessages([]);
} }
if (mobileOpened) { if (mobileOpened) {
toggleMobile(); toggleMobile();
} }
// Scroll to bottom after messages load
setTimeout(() => {
const viewport = scrollViewportRef.current;
if (viewport) {
viewport.scrollTop = viewport.scrollHeight;
}
}, 0);
}; };
const handleNewChat = () => { const handleNewChat = () => {
@@ -274,9 +301,9 @@ export default function ChatLayout() {
content: fullContent, content: fullContent,
}; };
// Save both user message and assistant response to database // Save to both localStorage and database
try { try {
// Save user message // Save user message to database
const userSaveRes = await fetch('/api/chats', { const userSaveRes = await fetch('/api/chats', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -287,13 +314,14 @@ export default function ChatLayout() {
}); });
const userSaveData = await userSaveRes.json(); const userSaveData = await userSaveRes.json();
const savedChatId = userSaveData.chatId; const savedChatId = userSaveData.chatId;
const chatTitle = userSaveData.title || userMessage.content.slice(0, 50);
// Update activeChatId if this was a new chat // Update activeChatId if this was a new chat
if (!activeChatId && savedChatId) { if (!activeChatId && savedChatId) {
setActiveChatId(savedChatId); setActiveChatId(savedChatId);
} }
// Save assistant response // Save assistant response to database
await fetch('/api/chats', { await fetch('/api/chats', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -303,6 +331,15 @@ export default function ChatLayout() {
}), }),
}); });
// Save to localStorage
const finalMessages = [...newMessages, responseMessage];
saveLocalChat({
id: savedChatId,
title: chatTitle,
updatedAt: new Date().toISOString(),
messages: finalMessages,
});
// Refresh chat list // Refresh chat list
fetchChats(); fetchChats();
} catch (saveError) { } catch (saveError) {
+135
View File
@@ -0,0 +1,135 @@
/**
* 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[];
}
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);
}