Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Add IndexedDB Provider Context #663

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion app/components/chat/Messages.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { classNames } from '~/utils/classNames';
import { AssistantMessage } from './AssistantMessage';
import { UserMessage } from './UserMessage';
import { useLocation } from '@remix-run/react';
import { db, chatId } from '~/lib/persistence/useChatHistory';
import { chatId } from '~/lib/persistence/useChatHistory';
import { forkChat } from '~/lib/persistence/db';
import { toast } from 'react-toastify';
import WithTooltip from '~/components/ui/Tooltip';
import { useIndexedDB } from '~/lib/providers/IndexedDBProvider.client';

interface MessagesProps {
id?: string;
Expand All @@ -19,6 +20,7 @@ interface MessagesProps {
export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props: MessagesProps, ref) => {
const { id, isStreaming = false, messages = [] } = props;
const location = useLocation();
const { db } = useIndexedDB();

const handleRewind = (messageId: string) => {
const searchParams = new URLSearchParams(location.search);
Expand Down
6 changes: 4 additions & 2 deletions app/components/settings/chat-history/ChatHistoryTab.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { useNavigate } from '@remix-run/react';
import React, { useState } from 'react';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { db, deleteById, getAll } from '~/lib/persistence';
import { deleteById, getAll } from '~/lib/persistence';
import { classNames } from '~/utils/classNames';
import styles from '~/components/settings/Settings.module.scss';
import { useIndexedDB } from '~/lib/providers/IndexedDBProvider.client';

export default function ChatHistoryTab() {
const navigate = useNavigate();
const { db } = useIndexedDB();
const [isDeleting, setIsDeleting] = useState(false);
const downloadAsJson = (data: any, filename: string) => {
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
Expand Down
52 changes: 29 additions & 23 deletions app/components/sidebar/Menu.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from
import { ThemeSwitch } from '~/components/ui/ThemeSwitch';
import { SettingsWindow } from '~/components/settings/SettingsWindow';
import { SettingsButton } from '~/components/ui/SettingsButton';
import { db, deleteById, getAll, chatId, type ChatHistoryItem, useChatHistory } from '~/lib/persistence';
import { deleteById, getAll, chatId, type ChatHistoryItem, useChatHistory } from '~/lib/persistence';
import { cubicEasingFn } from '~/utils/easings';
import { logger } from '~/utils/logger';
import { HistoryItem } from './HistoryItem';
import { binDates } from './date-binning';
import { useSearchFilter } from '~/lib/hooks/useSearchFilter';
import { useIndexedDB } from '~/lib/providers/IndexedDBProvider.client';
import { ClientOnly } from 'remix-utils/client-only';

const menuVariants = {
closed: {
Expand All @@ -36,6 +38,7 @@ const menuVariants = {
type DialogContent = { type: 'delete'; item: ChatHistoryItem } | null;

export const Menu = () => {
const { db, isLoading: dbLoading } = useIndexedDB();
const { duplicateCurrentChat, exportChat } = useChatHistory();
const menuRef = useRef<HTMLDivElement>(null);
const [list, setList] = useState<ChatHistoryItem[]>([]);
Expand All @@ -55,27 +58,30 @@ export const Menu = () => {
.then(setList)
.catch((error) => toast.error(error.message));
}
}, []);

const deleteItem = useCallback((event: React.UIEvent, item: ChatHistoryItem) => {
event.preventDefault();

if (db) {
deleteById(db, item.id)
.then(() => {
loadEntries();

if (chatId.get() === item.id) {
// hard page navigation to clear the stores
window.location.pathname = '/';
}
})
.catch((error) => {
toast.error('Failed to delete conversation');
logger.error(error);
});
}
}, []);
}, [dbLoading, db]);

const deleteItem = useCallback(
(event: React.UIEvent, item: ChatHistoryItem) => {
event.preventDefault();

if (db) {
deleteById(db, item.id)
.then(() => {
loadEntries();

if (chatId.get() === item.id) {
// hard page navigation to clear the stores
window.location.pathname = '/';
}
})
.catch((error) => {
toast.error('Failed to delete conversation');
logger.error(error);
});
}
},
[dbLoading, db],
);

const closeDialog = () => {
setDialogContent(null);
Expand Down Expand Up @@ -208,7 +214,7 @@ export const Menu = () => {
<ThemeSwitch />
</div>
</div>
<SettingsWindow open={isSettingsOpen} onClose={() => setIsSettingsOpen(false)} />
<ClientOnly>{() => <SettingsWindow open={isSettingsOpen} onClose={() => setIsSettingsOpen(false)} />}</ClientOnly>
</motion.div>
);
};
3 changes: 2 additions & 1 deletion app/lib/hooks/useEditChatDescription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { toast } from 'react-toastify';
import {
chatId as chatIdStore,
description as descriptionStore,
db,
updateChatDescription,
getMessages,
} from '~/lib/persistence';
import { useIndexedDB } from '~/lib/providers/IndexedDBProvider.client';

interface EditChatDescriptionOptions {
initialDescription?: string;
Expand Down Expand Up @@ -45,6 +45,7 @@ export function useEditChatDescription({
syncWithGlobalStore,
}: EditChatDescriptionOptions): EditChatDescriptionHook {
const chatIdFromStore = useStore(chatIdStore);
const { db } = useIndexedDB();
const [editing, setEditing] = useState(false);
const [currentDescription, setCurrentDescription] = useState(initialDescription);

Expand Down
7 changes: 5 additions & 2 deletions app/lib/persistence/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ import type { ChatHistoryItem } from './useChatHistory';
const logger = createScopedLogger('ChatHistory');

// this is used at the top level and never rejects
export async function openDatabase(): Promise<IDBDatabase | undefined> {
export async function openDatabase(
databaseName: string = 'boltHistory',
version: number = 1,
): Promise<IDBDatabase | undefined> {
if (typeof indexedDB === 'undefined') {
console.error('indexedDB is not available in this environment.');
return undefined;
}

return new Promise((resolve) => {
const request = indexedDB.open('boltHistory', 1);
const request = indexedDB.open(databaseName, version);

request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
const db = (event.target as IDBOpenDBRequest).result;
Expand Down
24 changes: 12 additions & 12 deletions app/lib/persistence/useChatHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,8 @@ import { atom } from 'nanostores';
import type { Message } from 'ai';
import { toast } from 'react-toastify';
import { workbenchStore } from '~/lib/stores/workbench';
import {
getMessages,
getNextId,
getUrlId,
openDatabase,
setMessages,
duplicateChat,
createChatFromMessages,
} from './db';
import { getMessages, getNextId, getUrlId, setMessages, duplicateChat, createChatFromMessages } from './db';
import { useIndexedDB } from '~/lib/providers/IndexedDBProvider.client';

export interface ChatHistoryItem {
id: string;
Expand All @@ -24,13 +17,12 @@ export interface ChatHistoryItem {

const persistenceEnabled = !import.meta.env.VITE_DISABLE_PERSISTENCE;

export const db = persistenceEnabled ? await openDatabase() : undefined;

export const chatId = atom<string | undefined>(undefined);
export const description = atom<string | undefined>(undefined);

export function useChatHistory() {
const navigate = useNavigate();
const { db, error: dbError, isLoading: dbLoading } = useIndexedDB();
const { id: mixedId } = useLoaderData<{ id?: string }>();
const [searchParams] = useSearchParams();

Expand All @@ -39,6 +31,14 @@ export function useChatHistory() {
const [urlId, setUrlId] = useState<string | undefined>();

useEffect(() => {
if (dbLoading) {
return;
}

if (!db && !dbError) {
return;
} // db not initialized yet

if (!db) {
setReady(true);

Expand Down Expand Up @@ -72,7 +72,7 @@ export function useChatHistory() {
toast.error(error.message);
});
}
}, []);
}, [dbLoading]);

return {
ready: !mixedId || ready,
Expand Down
77 changes: 77 additions & 0 deletions app/lib/providers/IndexedDBProvider.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
import { type IndexedDBContextType } from './types';
import { openDatabase } from '~/lib/persistence';

const IndexedDbContext = createContext<IndexedDBContextType | null>(null);

interface IndexedDBProviderProps {
children: React.ReactNode;
databaseName?: string;
version?: number;
}

export const IndexedDbProvider = ({
children,
databaseName = 'YourDatabaseName',
version = 1,
}: IndexedDBProviderProps) => {
const [db, setDb] = useState<IDBDatabase | null>(null);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
let mounted = true;

const initDb = async () => {
try {
const newDb = await openDatabase(databaseName, version);

if (!mounted) {
return;
}

if (!newDb) {
setError('Error opening IndexedDB');
} else {
setDb(newDb);
}
} catch (err) {
if (!mounted) {
return;
}

setError(err instanceof Error ? err.message : 'Error opening IndexedDB');
} finally {
if (mounted) {
setIsLoading(false);
}
}
};

// Only initialize IndexedDB in browser environment
if (typeof window !== 'undefined') {
initDb();
}

return () => {
mounted = false;

if (db) {
db.close();
}
};
}, [databaseName, version]);

return <IndexedDbContext.Provider value={{ db, isLoading, error }}>{children}</IndexedDbContext.Provider>;
};

// Custom hook to use the IndexedDB context
export function useIndexedDB(): IndexedDBContextType {
const context = useContext(IndexedDbContext);

if (context === null) {
throw new Error('useIndexedDB must be used within an IndexedDBProvider');
}

return context;
}
5 changes: 5 additions & 0 deletions app/lib/providers/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface IndexedDBContextType {
db: IDBDatabase | null;
error: string | null;
isLoading: boolean;
}
20 changes: 18 additions & 2 deletions app/routes/_index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ClientOnly } from 'remix-utils/client-only';
import { BaseChat } from '~/components/chat/BaseChat';
import { Chat } from '~/components/chat/Chat.client';
import { Header } from '~/components/header/Header';
import { IndexedDbProvider } from '~/lib/providers/IndexedDBProvider.client';
import BackgroundRays from '~/components/ui/BackgroundRays';

export const meta: MetaFunction = () => {
Expand All @@ -15,8 +16,23 @@ export default function Index() {
return (
<div className="flex flex-col h-full w-full bg-bolt-elements-background-depth-1">
<BackgroundRays />
<Header />
<ClientOnly fallback={<BaseChat />}>{() => <Chat />}</ClientOnly>
<ClientOnly
fallback={
<>
<Header />
<BaseChat />
</>
}
>
{() => (
<>
<IndexedDbProvider databaseName="boltHistory" version={1}>
<Header />
<Chat />
</IndexedDbProvider>
</>
)}
</ClientOnly>
</div>
);
}
Loading