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

Switch from LocalStorage to IndexedDB for Chat History Storage #158

Merged
merged 3 commits into from
Sep 19, 2023
Merged
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
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@vercel/analytics": "^1.0.2",
"browser-fs-access": "^0.34.1",
"eventsource-parser": "^1.0.0",
"idb-keyval": "^6.2.1",
"next": "^13.4.19",
"pdfjs-dist": "3.9.179",
"plantuml-encoder": "^1.4.0",
Expand Down
11 changes: 4 additions & 7 deletions src/apps/chat/components/applayout/ChatDrawerItems.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import * as React from 'react';
import { shallow } from 'zustand/shallow';

import { Box, ListDivider, ListItemDecorator, MenuItem, Tooltip, Typography } from '@mui/joy';
import { Box, ListDivider, ListItemDecorator, MenuItem, Typography } from '@mui/joy';
import AddIcon from '@mui/icons-material/Add';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import FileUploadIcon from '@mui/icons-material/FileUpload';

import { MAX_CONVERSATIONS, useChatStore } from '~/common/state/store-chats';
import { useChatStore } from '~/common/state/store-chats';
import { setLayoutDrawerAnchor } from '~/common/layout/store-applayout';
import { useUIPreferencesStore } from '~/common/state/store-ui';

Expand Down Expand Up @@ -44,7 +44,6 @@ export function ChatDrawerItems(props: {

const hasChats = conversationIDs.length > 0;
const singleChat = conversationIDs.length === 1;
const maxReached = conversationIDs.length >= MAX_CONVERSATIONS;

const closeDrawerMenu = () => setLayoutDrawerAnchor(null);

Expand All @@ -68,8 +67,6 @@ export function ChatDrawerItems(props: {
deleteConversation(conversationId);
}, [deleteConversation, singleChat]);

const NewPrefix = maxReached && <Tooltip title={`Maximum limit: ${MAX_CONVERSATIONS} chats. Proceeding will remove the oldest chat.`}><Box sx={{ mr: 2 }}>⚠️</Box></Tooltip>;

// grouping
let sortedIds = conversationIDs;
if (grouping === 'persona') {
Expand Down Expand Up @@ -98,9 +95,9 @@ export function ChatDrawerItems(props: {
{/* </Typography>*/}
{/*</ListItem>*/}

<MenuItem disabled={maxReached || (!!topNewConversationId && topNewConversationId === props.conversationId)} onClick={handleNew}>
<MenuItem disabled={!!topNewConversationId && topNewConversationId === props.conversationId} onClick={handleNew}>
<ListItemDecorator><AddIcon /></ListItemDecorator>
{NewPrefix}New
New
</MenuItem>

<ListDivider sx={{ mb: 0 }} />
Expand Down
7 changes: 3 additions & 4 deletions src/apps/chat/components/applayout/ChatMenuItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import FileDownloadIcon from '@mui/icons-material/FileDownload';
import ForkRightIcon from '@mui/icons-material/ForkRight';
import SettingsSuggestIcon from '@mui/icons-material/SettingsSuggest';

import { MAX_CONVERSATIONS, useChatStore } from '~/common/state/store-chats';
import { useChatStore } from '~/common/state/store-chats';
import { setLayoutMenuAnchor } from '~/common/layout/store-applayout';
import { useUIPreferencesStore } from '~/common/state/store-ui';

Expand All @@ -28,7 +28,6 @@ export function ChatMenuItems(props: {
const { showSystemMessages, setShowSystemMessages } = useUIPreferencesStore(state => ({
showSystemMessages: state.showSystemMessages, setShowSystemMessages: state.setShowSystemMessages,
}), shallow);
const maxConversationsReached: boolean = useChatStore(state => state.conversations.length >= MAX_CONVERSATIONS);

// derived state
const disabled = !props.conversationId || props.isConversationEmpty;
Expand Down Expand Up @@ -82,13 +81,13 @@ export function ChatMenuItems(props: {

<ListDivider inset='startContent' />

<MenuItem disabled={disabled || maxConversationsReached} onClick={handleConversationDuplicate}>
<MenuItem disabled={disabled} onClick={handleConversationDuplicate}>
<ListItemDecorator>
{/*<Badge size='sm' color='success'>*/}
<ForkRightIcon color='success' />
{/*</Badge>*/}
</ListItemDecorator>
Duplicate{maxConversationsReached && ' (max reached)'}
Duplicate
</MenuItem>

<MenuItem disabled={disabled} onClick={handleConversationFlatten}>
Expand Down
46 changes: 37 additions & 9 deletions src/common/state/store-chats.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { devtools, persist, createJSONStorage, StateStorage } from 'zustand/middleware';
import { get, set, del } from 'idb-keyval'
import { v4 as uuidv4 } from 'uuid';

import { DLLMId } from '~/modules/llms/llm.types';
Expand All @@ -9,10 +10,6 @@ import { countModelTokens } from '../util/token-counter';
import { defaultSystemPurposeId, SystemPurposeId } from '../../data';


// configuration
export const MAX_CONVERSATIONS = 20;


/**
* Conversation, a list of messages between humans and bots
* Future:
Expand Down Expand Up @@ -107,6 +104,34 @@ export function createDEphemeral(title: string, initialText: string): DEphemeral
};
}

const storage: StateStorage = {
getItem: async (name: string): Promise<string | null> => {
return (await get(name)) || null
},
setItem: async (name: string, value: string): Promise<void> => {
await set(name, value)
},
removeItem: async (name: string): Promise<void> => {
await del(name)
},
}

function _migrateLocalStorageToIndexedDB(state: ChatStore) {
const key = "app-chats"
const value = localStorage.getItem(key);

// Check if migration has already been done
if (!value) return state;

// Mark migration as done
localStorage.removeItem(key);

// Migrate data to IndexedDB
const localStorageState = JSON.parse(value)?.state;

state.conversations = localStorageState?.conversations;
state.activeConversationId = localStorageState?.activeConversationId;
}

/// Conversations Store

Expand Down Expand Up @@ -160,7 +185,7 @@ export const useChatStore = create<ChatStore>()(devtools(
return {
conversations: [
conversation,
...state.conversations.slice(0, MAX_CONVERSATIONS - 1),
...state.conversations,
],
activeConversationId: conversation.id,
};
Expand Down Expand Up @@ -189,7 +214,7 @@ export const useChatStore = create<ChatStore>()(devtools(
return {
conversations: [
duplicate,
...state.conversations, // DISABLED: can inadvertendly lose data - check upstream instead - .slice(0, MAX_CONVERSATIONS - 1),
...state.conversations,
],
activeConversationId: duplicate.id,
};
Expand All @@ -203,7 +228,7 @@ export const useChatStore = create<ChatStore>()(devtools(
// NOTE: the .filter below is superfluous (we delete the conversation above), but it's a reminder that we don't want to corrupt the state
conversations: [
conversation,
...state.conversations.filter(other => other.id !== conversation.id).slice(0, MAX_CONVERSATIONS - 1),
...state.conversations.filter((other: DConversation) => other.id !== conversation.id),
],
activeConversationId: conversation.id,
};
Expand Down Expand Up @@ -406,7 +431,9 @@ export const useChatStore = create<ChatStore>()(devtools(
// version history:
// - 1: [2023-03-18] app launch, single chat
// - 2: [2023-04-10] multi-chat version - invalidating data to be sure
version: 2,
// - 3: [2023-08-30] switch to IndexedDB
version: 3,
storage: createJSONStorage(() => storage),

// omit the transient property from the persisted state
partialize: (state) => ({
Expand All @@ -422,6 +449,7 @@ export const useChatStore = create<ChatStore>()(devtools(

onRehydrateStorage: () => (state) => {
if (state) {
_migrateLocalStorageToIndexedDB(state);
// if nothing is selected, select the first conversation
if (!state.activeConversationId && state.conversations.length)
state.activeConversationId = state.conversations[0].id;
Expand Down