refactor: change TTD persistence to iDB (#10662)
refactor: change ttd persistence to iDB
This commit is contained in:
@@ -46,6 +46,7 @@ export const STORAGE_KEYS = {
|
|||||||
VERSION_FILES: "version-files",
|
VERSION_FILES: "version-files",
|
||||||
|
|
||||||
IDB_LIBRARY: "excalidraw-library",
|
IDB_LIBRARY: "excalidraw-library",
|
||||||
|
IDB_TTD_CHATS: "excalidraw-ttd-chats",
|
||||||
|
|
||||||
// do not use apart from migrations
|
// do not use apart from migrations
|
||||||
__LEGACY_LOCAL_STORAGE_LIBRARY: "excalidraw-library",
|
__LEGACY_LOCAL_STORAGE_LIBRARY: "excalidraw-library",
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import { safelyParseJSON } from "@excalidraw/common";
|
|||||||
|
|
||||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
import { TTDIndexedDBAdapter } from "../data/TTDStorage";
|
||||||
|
|
||||||
export const AIComponents = ({
|
export const AIComponents = ({
|
||||||
excalidrawAPI,
|
excalidrawAPI,
|
||||||
}: {
|
}: {
|
||||||
@@ -116,6 +118,7 @@ export const AIComponents = ({
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}}
|
}}
|
||||||
|
persistenceAdapter={TTDIndexedDBAdapter}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { createStore, get, set } from "idb-keyval";
|
||||||
|
|
||||||
|
import type { SavedChats } from "@excalidraw/excalidraw/components/TTDDialog/types";
|
||||||
|
|
||||||
|
import { STORAGE_KEYS } from "../app_constants";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IndexedDB adapter for TTD chat storage.
|
||||||
|
* Implements TTDPersistenceAdapter interface.
|
||||||
|
*/
|
||||||
|
export class TTDIndexedDBAdapter {
|
||||||
|
/** IndexedDB database name */
|
||||||
|
private static idb_name = STORAGE_KEYS.IDB_TTD_CHATS;
|
||||||
|
/** Store key for chat data */
|
||||||
|
private static key = "ttdChats";
|
||||||
|
|
||||||
|
private static store = createStore(
|
||||||
|
`${TTDIndexedDBAdapter.idb_name}-db`,
|
||||||
|
`${TTDIndexedDBAdapter.idb_name}-store`,
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load saved chats from IndexedDB.
|
||||||
|
* @returns Promise resolving to saved chats array (empty if none found)
|
||||||
|
*/
|
||||||
|
static async loadChats(): Promise<SavedChats> {
|
||||||
|
try {
|
||||||
|
const data = await get<SavedChats>(
|
||||||
|
TTDIndexedDBAdapter.key,
|
||||||
|
TTDIndexedDBAdapter.store,
|
||||||
|
);
|
||||||
|
return data || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to load TTD chats from IndexedDB:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save chats to IndexedDB.
|
||||||
|
* @param chats - The chats array to persist
|
||||||
|
*/
|
||||||
|
static async saveChats(chats: SavedChats): Promise<void> {
|
||||||
|
try {
|
||||||
|
await set(TTDIndexedDBAdapter.key, chats, TTDIndexedDBAdapter.store);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to save TTD chats to IndexedDB:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,13 +15,18 @@ import { TTDDialogTab } from "./TTDDialogTab";
|
|||||||
|
|
||||||
import "./TTDDialog.scss";
|
import "./TTDDialog.scss";
|
||||||
|
|
||||||
import type { MermaidToExcalidrawLibProps, TTTDDialog } from "./types";
|
import type {
|
||||||
|
MermaidToExcalidrawLibProps,
|
||||||
|
TTDPersistenceAdapter,
|
||||||
|
TTTDDialog,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
export const TTDDialog = (
|
export const TTDDialog = (
|
||||||
props:
|
props:
|
||||||
| {
|
| {
|
||||||
onTextSubmit: TTTDDialog.onTextSubmit;
|
onTextSubmit: TTTDDialog.onTextSubmit;
|
||||||
renderWarning?: TTTDDialog.renderWarning;
|
renderWarning?: TTTDDialog.renderWarning;
|
||||||
|
persistenceAdapter: TTDPersistenceAdapter;
|
||||||
}
|
}
|
||||||
| { __fallback: true },
|
| { __fallback: true },
|
||||||
) => {
|
) => {
|
||||||
@@ -50,6 +55,7 @@ const TTDDialogBase = withInternalFallback(
|
|||||||
props: TTTDDialog.OnTextSubmitProps,
|
props: TTTDDialog.OnTextSubmitProps,
|
||||||
): Promise<TTTDDialog.OnTextSubmitRetValue>;
|
): Promise<TTTDDialog.OnTextSubmitRetValue>;
|
||||||
renderWarning?: TTTDDialog.renderWarning;
|
renderWarning?: TTTDDialog.renderWarning;
|
||||||
|
persistenceAdapter: TTDPersistenceAdapter;
|
||||||
}
|
}
|
||||||
| { __fallback: true }
|
| { __fallback: true }
|
||||||
)) => {
|
)) => {
|
||||||
@@ -105,6 +111,7 @@ const TTDDialogBase = withInternalFallback(
|
|||||||
mermaidToExcalidrawLib={mermaidToExcalidrawLib}
|
mermaidToExcalidrawLib={mermaidToExcalidrawLib}
|
||||||
onTextSubmit={rest.onTextSubmit}
|
onTextSubmit={rest.onTextSubmit}
|
||||||
renderWarning={rest.renderWarning}
|
renderWarning={rest.renderWarning}
|
||||||
|
persistenceAdapter={rest.persistenceAdapter}
|
||||||
/>
|
/>
|
||||||
</TTDDialogTab>
|
</TTDDialogTab>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -25,18 +25,25 @@ import { TTDPreviewPanel } from "./TTDPreviewPanel";
|
|||||||
import { getLastAssistantMessage } from "./utils/chat";
|
import { getLastAssistantMessage } from "./utils/chat";
|
||||||
|
|
||||||
import type { BinaryFiles } from "../../types";
|
import type { BinaryFiles } from "../../types";
|
||||||
import type { MermaidToExcalidrawLibProps, TChat, TTTDDialog } from "./types";
|
import type {
|
||||||
|
MermaidToExcalidrawLibProps,
|
||||||
|
TChat,
|
||||||
|
TTDPersistenceAdapter,
|
||||||
|
TTTDDialog,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
const TextToDiagramContent = ({
|
const TextToDiagramContent = ({
|
||||||
mermaidToExcalidrawLib,
|
mermaidToExcalidrawLib,
|
||||||
onTextSubmit,
|
onTextSubmit,
|
||||||
renderWarning,
|
renderWarning,
|
||||||
|
persistenceAdapter,
|
||||||
}: {
|
}: {
|
||||||
mermaidToExcalidrawLib: MermaidToExcalidrawLibProps;
|
mermaidToExcalidrawLib: MermaidToExcalidrawLibProps;
|
||||||
onTextSubmit: (
|
onTextSubmit: (
|
||||||
props: TTTDDialog.OnTextSubmitProps,
|
props: TTTDDialog.OnTextSubmitProps,
|
||||||
) => Promise<TTTDDialog.OnTextSubmitRetValue>;
|
) => Promise<TTTDDialog.OnTextSubmitRetValue>;
|
||||||
renderWarning?: TTTDDialog.renderWarning;
|
renderWarning?: TTTDDialog.renderWarning;
|
||||||
|
persistenceAdapter: TTDPersistenceAdapter;
|
||||||
}) => {
|
}) => {
|
||||||
const app = useApp();
|
const app = useApp();
|
||||||
const setAppState = useExcalidrawSetAppState();
|
const setAppState = useExcalidrawSetAppState();
|
||||||
@@ -46,7 +53,7 @@ const TextToDiagramContent = ({
|
|||||||
const [chatHistory, setChatHistory] = useAtom(chatHistoryAtom);
|
const [chatHistory, setChatHistory] = useAtom(chatHistoryAtom);
|
||||||
const showPreview = useAtomValue(showPreviewAtom);
|
const showPreview = useAtomValue(showPreviewAtom);
|
||||||
|
|
||||||
const { savedChats } = useTTDChatStorage();
|
const { savedChats } = useTTDChatStorage({ persistenceAdapter });
|
||||||
|
|
||||||
const lastAssistantMessage = getLastAssistantMessage(chatHistory);
|
const lastAssistantMessage = getLastAssistantMessage(chatHistory);
|
||||||
|
|
||||||
@@ -68,7 +75,7 @@ const TextToDiagramContent = ({
|
|||||||
handleNewChat,
|
handleNewChat,
|
||||||
handleMenuToggle,
|
handleMenuToggle,
|
||||||
handleMenuClose,
|
handleMenuClose,
|
||||||
} = useChatManagement();
|
} = useChatManagement({ persistenceAdapter });
|
||||||
|
|
||||||
const onViewAsMermaid = () => {
|
const onViewAsMermaid = () => {
|
||||||
if (typeof lastAssistantMessage?.content === "string") {
|
if (typeof lastAssistantMessage?.content === "string") {
|
||||||
@@ -231,18 +238,21 @@ export const TextToDiagram = ({
|
|||||||
mermaidToExcalidrawLib,
|
mermaidToExcalidrawLib,
|
||||||
onTextSubmit,
|
onTextSubmit,
|
||||||
renderWarning,
|
renderWarning,
|
||||||
|
persistenceAdapter,
|
||||||
}: {
|
}: {
|
||||||
mermaidToExcalidrawLib: MermaidToExcalidrawLibProps;
|
mermaidToExcalidrawLib: MermaidToExcalidrawLibProps;
|
||||||
onTextSubmit(
|
onTextSubmit(
|
||||||
props: TTTDDialog.OnTextSubmitProps,
|
props: TTTDDialog.OnTextSubmitProps,
|
||||||
): Promise<TTTDDialog.OnTextSubmitRetValue>;
|
): Promise<TTTDDialog.OnTextSubmitRetValue>;
|
||||||
renderWarning?: TTTDDialog.renderWarning;
|
renderWarning?: TTTDDialog.renderWarning;
|
||||||
|
persistenceAdapter: TTDPersistenceAdapter;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<TextToDiagramContent
|
<TextToDiagramContent
|
||||||
mermaidToExcalidrawLib={mermaidToExcalidrawLib}
|
mermaidToExcalidrawLib={mermaidToExcalidrawLib}
|
||||||
onTextSubmit={onTextSubmit}
|
onTextSubmit={onTextSubmit}
|
||||||
renderWarning={renderWarning}
|
renderWarning={renderWarning}
|
||||||
|
persistenceAdapter={persistenceAdapter}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
import { useAtom, useSetAtom } from "../../../editor-jotai";
|
import { useAtom, useSetAtom } from "../../../editor-jotai";
|
||||||
|
|
||||||
@@ -8,81 +8,100 @@ import { useTTDChatStorage } from "../useTTDChatStorage";
|
|||||||
|
|
||||||
import { getLastAssistantMessage } from "../utils/chat";
|
import { getLastAssistantMessage } from "../utils/chat";
|
||||||
|
|
||||||
import type { SavedChat } from "../types";
|
import type { SavedChat, TTDPersistenceAdapter } from "../types";
|
||||||
|
|
||||||
export const useChatManagement = () => {
|
interface UseChatManagementProps {
|
||||||
|
persistenceAdapter: TTDPersistenceAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useChatManagement = ({
|
||||||
|
persistenceAdapter,
|
||||||
|
}: UseChatManagementProps) => {
|
||||||
const setError = useSetAtom(errorAtom);
|
const setError = useSetAtom(errorAtom);
|
||||||
const [chatHistory, setChatHistory] = useAtom(chatHistoryAtom);
|
const [chatHistory, setChatHistory] = useAtom(chatHistoryAtom);
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
|
|
||||||
const { restoreChat, deleteChat, createNewChatId } = useTTDChatStorage();
|
const { restoreChat, deleteChat, createNewChatId } = useTTDChatStorage({
|
||||||
|
persistenceAdapter,
|
||||||
|
});
|
||||||
|
|
||||||
const resetChatState = () => {
|
const applyChatToState = useCallback(
|
||||||
const newSessionId = createNewChatId();
|
(chat: SavedChat) => {
|
||||||
|
const restoredMessages = chat.messages.map((msg) => ({
|
||||||
|
...msg,
|
||||||
|
timestamp:
|
||||||
|
msg.timestamp instanceof Date
|
||||||
|
? msg.timestamp
|
||||||
|
: new Date(msg.timestamp),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const history = {
|
||||||
|
id: chat.id,
|
||||||
|
messages: restoredMessages,
|
||||||
|
currentPrompt: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const lastAssistantMsg = getLastAssistantMessage(history);
|
||||||
|
|
||||||
|
setError(
|
||||||
|
lastAssistantMsg?.error ? new Error(lastAssistantMsg?.error) : null,
|
||||||
|
);
|
||||||
|
setChatHistory(history);
|
||||||
|
},
|
||||||
|
[setError, setChatHistory],
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetChatState = useCallback(async () => {
|
||||||
|
const newSessionId = await createNewChatId();
|
||||||
setChatHistory({
|
setChatHistory({
|
||||||
id: newSessionId,
|
id: newSessionId,
|
||||||
messages: [],
|
messages: [],
|
||||||
currentPrompt: "",
|
currentPrompt: "",
|
||||||
});
|
});
|
||||||
setError(null);
|
setError(null);
|
||||||
};
|
}, [createNewChatId, setChatHistory, setError]);
|
||||||
|
|
||||||
const applyChatToState = (chat: SavedChat) => {
|
const onRestoreChat = useCallback(
|
||||||
const restoredMessages = chat.messages.map((msg) => ({
|
(chat: SavedChat) => {
|
||||||
...msg,
|
const restoredChat = restoreChat(chat);
|
||||||
timestamp:
|
applyChatToState(restoredChat);
|
||||||
msg.timestamp instanceof Date ? msg.timestamp : new Date(msg.timestamp),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const history = {
|
setIsMenuOpen(false);
|
||||||
id: chat.id,
|
},
|
||||||
messages: restoredMessages,
|
[restoreChat, applyChatToState],
|
||||||
currentPrompt: "",
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const lastAssistantMsg = getLastAssistantMessage(history);
|
const handleDeleteChat = useCallback(
|
||||||
|
async (chatId: string, event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
setError(
|
const isDeletingActiveChat = chatId === chatHistory.id;
|
||||||
lastAssistantMsg?.error ? new Error(lastAssistantMsg?.error) : null,
|
const updatedChats = await deleteChat(chatId);
|
||||||
);
|
|
||||||
setChatHistory(history);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRestoreChat = (chat: SavedChat) => {
|
if (isDeletingActiveChat) {
|
||||||
const restoredChat = restoreChat(chat);
|
if (updatedChats.length > 0) {
|
||||||
applyChatToState(restoredChat);
|
const nextChat = updatedChats[0];
|
||||||
|
applyChatToState(nextChat);
|
||||||
setIsMenuOpen(false);
|
} else {
|
||||||
};
|
await resetChatState();
|
||||||
|
}
|
||||||
const handleDeleteChat = (chatId: string, event: React.MouseEvent) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
const isDeletingActiveChat = chatId === chatHistory.id;
|
|
||||||
const updatedChats = deleteChat(chatId);
|
|
||||||
|
|
||||||
if (isDeletingActiveChat) {
|
|
||||||
if (updatedChats.length > 0) {
|
|
||||||
const nextChat = updatedChats[0];
|
|
||||||
applyChatToState(nextChat);
|
|
||||||
} else {
|
|
||||||
resetChatState();
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
[chatHistory.id, deleteChat, applyChatToState, resetChatState],
|
||||||
|
);
|
||||||
|
|
||||||
const handleNewChat = () => {
|
const handleNewChat = useCallback(async () => {
|
||||||
resetChatState();
|
await resetChatState();
|
||||||
setIsMenuOpen(false);
|
setIsMenuOpen(false);
|
||||||
};
|
}, [resetChatState]);
|
||||||
|
|
||||||
const handleMenuToggle = () => {
|
const handleMenuToggle = useCallback(() => {
|
||||||
setIsMenuOpen((prev) => !prev);
|
setIsMenuOpen((prev) => !prev);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleMenuClose = () => {
|
const handleMenuClose = useCallback(() => {
|
||||||
setIsMenuOpen(false);
|
setIsMenuOpen(false);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isMenuOpen,
|
isMenuOpen,
|
||||||
|
|||||||
@@ -53,6 +53,24 @@ export interface SavedChat {
|
|||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SavedChats = SavedChat[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for TTD chat persistence. Preferably should be stable
|
||||||
|
* (e.g. static class/singleton)
|
||||||
|
*/
|
||||||
|
export interface TTDPersistenceAdapter {
|
||||||
|
/**
|
||||||
|
* Load saved chats from storage.
|
||||||
|
*/
|
||||||
|
loadChats(): Promise<SavedChats>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save chats to storage.
|
||||||
|
*/
|
||||||
|
saveChats(chats: SavedChats): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MermaidToExcalidrawLibProps {
|
export interface MermaidToExcalidrawLibProps {
|
||||||
loaded: boolean;
|
loaded: boolean;
|
||||||
api: Promise<{
|
api: Promise<{
|
||||||
|
|||||||
@@ -1,44 +1,24 @@
|
|||||||
import { useEffect } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
import { randomId } from "@excalidraw/common";
|
import { randomId } from "@excalidraw/common";
|
||||||
|
|
||||||
import { atom, useAtom } from "../../editor-jotai";
|
import { atom, useAtom } from "../../editor-jotai";
|
||||||
|
|
||||||
import { chatHistoryAtom } from "./TTDContext";
|
import { chatHistoryAtom } from "./TTDContext";
|
||||||
|
|
||||||
import type { SavedChat } from "./types";
|
import type { SavedChat, SavedChats, TTDPersistenceAdapter } from "./types";
|
||||||
|
|
||||||
const TTD_CHATS_STORAGE_KEY = "excalidraw-ttd-chats";
|
interface UseTTDChatStorageProps {
|
||||||
|
persistenceAdapter: TTDPersistenceAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
interface UseTTDChatStorageReturn {
|
interface UseTTDChatStorageReturn {
|
||||||
savedChats: SavedChats;
|
savedChats: SavedChats;
|
||||||
saveCurrentChat: () => void;
|
saveCurrentChat: () => Promise<void>;
|
||||||
deleteChat: (chatId: string) => SavedChats;
|
deleteChat: (chatId: string) => Promise<SavedChats>;
|
||||||
restoreChat: (chat: SavedChat) => SavedChat;
|
restoreChat: (chat: SavedChat) => SavedChat;
|
||||||
createNewChatId: () => string;
|
createNewChatId: () => Promise<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SavedChats = SavedChat[];
|
|
||||||
|
|
||||||
const saveChatsToStorage = (chats: SavedChats) => {
|
|
||||||
try {
|
|
||||||
window.localStorage.setItem(TTD_CHATS_STORAGE_KEY, JSON.stringify(chats));
|
|
||||||
} catch (error: any) {
|
|
||||||
console.warn(`Failed to save chats to localStorage: ${error.message}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadChatsFromStorage = (): SavedChats => {
|
|
||||||
try {
|
|
||||||
const data = window.localStorage.getItem(TTD_CHATS_STORAGE_KEY);
|
|
||||||
if (data) {
|
|
||||||
return JSON.parse(data) as SavedChats;
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.warn(`Failed to load chats from localStorage: ${error.message}`);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateChatTitle = (firstMessage: string): string => {
|
const generateChatTitle = (firstMessage: string): string => {
|
||||||
const trimmed = firstMessage.trim();
|
const trimmed = firstMessage.trim();
|
||||||
if (trimmed.length <= 50) {
|
if (trimmed.length <= 50) {
|
||||||
@@ -47,17 +27,60 @@ const generateChatTitle = (firstMessage: string): string => {
|
|||||||
return `${trimmed.substring(0, 47)}...`;
|
return `${trimmed.substring(0, 47)}...`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Shared atom for saved chats - initialized once from localStorage
|
// Shared atom for saved chats - starts empty, populated via onLoadChats
|
||||||
export const savedChatsAtom = atom<SavedChats>(loadChatsFromStorage());
|
export const savedChatsAtom = atom<SavedChats>([]);
|
||||||
|
export const isLoadingChatsAtom = atom<boolean>(false);
|
||||||
|
export const chatsLoadedAtom = atom<boolean>(false);
|
||||||
|
|
||||||
export const useTTDChatStorage = (): UseTTDChatStorageReturn => {
|
export const useTTDChatStorage = ({
|
||||||
|
persistenceAdapter,
|
||||||
|
}: UseTTDChatStorageProps): UseTTDChatStorageReturn => {
|
||||||
const [chatHistory] = useAtom(chatHistoryAtom);
|
const [chatHistory] = useAtom(chatHistoryAtom);
|
||||||
const [savedChats, setSavedChats] = useAtom(savedChatsAtom);
|
const [savedChats, setSavedChats] = useAtom(savedChatsAtom);
|
||||||
|
const [isLoading, setIsLoading] = useAtom(isLoadingChatsAtom);
|
||||||
|
const [chatsLoaded, setChatsLoaded] = useAtom(chatsLoadedAtom);
|
||||||
|
|
||||||
|
// Ref to track latest savedChats for async operations
|
||||||
|
const savedChatsRef = useRef(savedChats);
|
||||||
|
savedChatsRef.current = savedChats;
|
||||||
|
|
||||||
const lastMessageInHistory =
|
const lastMessageInHistory =
|
||||||
chatHistory?.messages[chatHistory?.messages.length - 1];
|
chatHistory?.messages[chatHistory?.messages.length - 1];
|
||||||
|
|
||||||
const saveCurrentChat = () => {
|
// Load chats on-demand
|
||||||
|
const loadChats = useCallback(async () => {
|
||||||
|
if (chatsLoaded || isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const chats = await persistenceAdapter.loadChats();
|
||||||
|
setSavedChats(chats);
|
||||||
|
setChatsLoaded(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to load chats:", error);
|
||||||
|
setSavedChats([]);
|
||||||
|
setChatsLoaded(true);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
chatsLoaded,
|
||||||
|
isLoading,
|
||||||
|
setSavedChats,
|
||||||
|
setIsLoading,
|
||||||
|
setChatsLoaded,
|
||||||
|
persistenceAdapter,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// INITIAL LOAD
|
||||||
|
useEffect(() => {
|
||||||
|
loadChats();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const saveCurrentChat = useCallback(async () => {
|
||||||
if (chatHistory.messages.length === 0) {
|
if (chatHistory.messages.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -71,7 +94,7 @@ export const useTTDChatStorage = (): UseTTDChatStorageReturn => {
|
|||||||
|
|
||||||
const title = generateChatTitle(firstUserMessage.content);
|
const title = generateChatTitle(firstUserMessage.content);
|
||||||
|
|
||||||
const currentSavedChats = loadChatsFromStorage();
|
const currentSavedChats = savedChatsRef.current;
|
||||||
const existingChat = currentSavedChats.find(
|
const existingChat = currentSavedChats.find(
|
||||||
(chat) => chat.id === chatHistory.id,
|
(chat) => chat.id === chatHistory.id,
|
||||||
);
|
);
|
||||||
@@ -111,9 +134,15 @@ export const useTTDChatStorage = (): UseTTDChatStorageReturn => {
|
|||||||
.slice(0, 10);
|
.slice(0, 10);
|
||||||
|
|
||||||
setSavedChats(updatedChats);
|
setSavedChats(updatedChats);
|
||||||
saveChatsToStorage(updatedChats);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
await persistenceAdapter.saveChats(updatedChats);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to save chats:", error);
|
||||||
|
}
|
||||||
|
}, [chatHistory, setSavedChats, persistenceAdapter]);
|
||||||
|
|
||||||
|
// Auto-save when generation completes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!lastMessageInHistory?.isGenerating) {
|
if (!lastMessageInHistory?.isGenerating) {
|
||||||
saveCurrentChat();
|
saveCurrentChat();
|
||||||
@@ -125,23 +154,33 @@ export const useTTDChatStorage = (): UseTTDChatStorageReturn => {
|
|||||||
lastMessageInHistory?.isGenerating,
|
lastMessageInHistory?.isGenerating,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const deleteChat = (chatId: string): SavedChats => {
|
const deleteChat = useCallback(
|
||||||
const updatedChats = savedChats.filter((chat) => chat.id !== chatId);
|
async (chatId: string): Promise<SavedChats> => {
|
||||||
setSavedChats(updatedChats);
|
const updatedChats = savedChatsRef.current.filter(
|
||||||
saveChatsToStorage(updatedChats);
|
(chat) => chat.id !== chatId,
|
||||||
|
);
|
||||||
|
setSavedChats(updatedChats);
|
||||||
|
|
||||||
return updatedChats;
|
try {
|
||||||
};
|
await persistenceAdapter.saveChats(updatedChats);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to save after delete:", error);
|
||||||
|
}
|
||||||
|
|
||||||
const restoreChat = (chat: SavedChat): SavedChat => {
|
return updatedChats;
|
||||||
saveCurrentChat();
|
},
|
||||||
|
[setSavedChats, persistenceAdapter],
|
||||||
|
);
|
||||||
|
|
||||||
|
const restoreChat = useCallback((chat: SavedChat): SavedChat => {
|
||||||
|
// Save is handled by the caller after state update
|
||||||
return chat;
|
return chat;
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const createNewChatId = (): string => {
|
const createNewChatId = useCallback(async (): Promise<string> => {
|
||||||
saveCurrentChat();
|
await saveCurrentChat();
|
||||||
return randomId();
|
return randomId();
|
||||||
};
|
}, [saveCurrentChat]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
savedChats,
|
savedChats,
|
||||||
|
|||||||
@@ -290,6 +290,11 @@ export { DefaultSidebar } from "./components/DefaultSidebar";
|
|||||||
export { TTDDialog } from "./components/TTDDialog/TTDDialog";
|
export { TTDDialog } from "./components/TTDDialog/TTDDialog";
|
||||||
export { TTDDialogTrigger } from "./components/TTDDialog/TTDDialogTrigger";
|
export { TTDDialogTrigger } from "./components/TTDDialog/TTDDialogTrigger";
|
||||||
export { TTDStreamFetch } from "./components/TTDDialog/utils/TTDStreamFetch";
|
export { TTDStreamFetch } from "./components/TTDDialog/utils/TTDStreamFetch";
|
||||||
|
export type {
|
||||||
|
TTDPersistenceAdapter,
|
||||||
|
SavedChat,
|
||||||
|
SavedChats,
|
||||||
|
} from "./components/TTDDialog/types";
|
||||||
|
|
||||||
export { zoomToFitBounds } from "./actions/actionCanvas";
|
export { zoomToFitBounds } from "./actions/actionCanvas";
|
||||||
export {
|
export {
|
||||||
|
|||||||
Reference in New Issue
Block a user