changes
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user