Enhancement: Persist Chat History Using Dexie.js for Client-Side Storage
- Dominant language
- TypeScript
- Stars
- 3
- Forks
- 0
- PR merge metrics
- No merged PRs in 30d
Description
## Background / Motivation
Currently our application keeps all chat interactions in-memory, which means that when a user refreshes the page or navigates away, they lose their conversation context. We need to persist both user and AI assistant messages for each chat topic directly in the user's browser.
Key benefits:
- Improve user experience by preserving context across sessions
- Support multiple topics/conversations per user
- Enable offline access to previous conversations
- Reduce server load and storage costs by keeping history local
- Enhance privacy by storing sensitive conversations on user's device
## Proposed Solution
We will implement a client-side persistence layer using Dexie.js (wrapper for IndexedDB):
1. Use Dexie.js to create a structured, indexed database in the browser
2. Design a schema for multi-topic conversations
3. Enable efficient queries for recent messages and topics
4. Add full-text search capabilities for messages
5. Implement export/import functionality for backup purposes
## Data Model
### Dexie Database Schema
```js
const db = new Dexie('ChatHistoryDB');
db.version(1).stores({
topics: '++id, userId, title, lastActive',
messages: '++id, topicId, timestamp, role, [topicId+timestamp]',
wordIndex: '[word+topicId], word'
});
```
### Message Object Structure
```json
{
"id": 123,
"topicId": 456,
"timestamp": 1715432567890,
"role": "user|assistant",
"content": "message text",
"type": "text|image|etc",
"metadata": {},
"tokens": 123,
"modelVersion": "gpt-4-turbo"
}
```
### Topic Object Structure
```json
{
"id": 456,
"userId": "user123",
"title": "Support Chat",
"createdAt": 1715432567890,
"lastActive": 1715432590123,
"messageCnt": 342,
"userMessageCnt": 171,
"assistantMessageCnt": 171,
"totalTokens": 45678,
"model": "gpt-4-turbo",
"systemPrompt": "You are a helpful assistant...",
"pinnedState": true|false
}
```
## Client-Side API
```js
// Topic management
async function createTopic(title, systemPrompt)
async function listTopics(sortBy = 'lastActive', limit = 20)
async function updateTopic(topicId, updates)
async function deleteTopic(topicId)
// Message management
async function addMessage(topicId, message)
async function getMessages(topicId, limit = 50, offset = 0)
async function getThread(topicId) // All messages in order
// Search functionality
async function searchMessages(query, topicId = null)
// Import/Export
async function exportData(topicId = null) // null = all topics
async function importData(data)
```
## Word Indexing Strategy
To enable effective search while maintaining performance:
1. **Word Selection:**
- Convert message text to lowercase and tokenize into words
- Filter out stop words (common words with little search value)
- Only index words with length >= 3 characters
- Exclude pure numbers and extremely long tokens
2. **Implementation:**
```js
function indexMessageContent(content, topicId, messageId) {
// Define stop words
const STOP_WORDS = new Set([
'a', 'an', 'the', 'and', 'or', 'but', 'is', 'are', 'was',
// Add more common words to filter
]);
// Tokenize and filter
const words = content.toLowerCase()
.split(/\W+/)
.filter(word =>
word.length >= 3 &&
word.length <= 30 &&
!STOP_WORDS.has(word) &&
!/^\d+$/.test(word)
);
// Store in word index
return db.transaction('rw', db.wordIndex, async () => {
const uniqueWords = [...new Set(words)];
await Promise.all(uniqueWords.map(word =>
db.wordIndex.put({
word,
topicId,
messageId
})
));
});
}
```
## Dexie.js Optimizations
1. **Compound Indexes**
- Use compound indexes like `[topicId+timestamp]` for efficient range queries
- Enable sorting messages by timestamp within a topic
2. **Transactions**
```js
await db.transaction('rw', [db.topics, db.messages], async () => {
// Perform multiple operations atomically
const topicId = await db.topics.put({/*...*/});
await db.messages.put({topicId, /*...*/});
});
```
3. **Bulk Operations**
```js
// Efficient bulk imports
await db.messages.bulkPut(messagesArray);
```
## Key Operations
### Adding a Message
```js
async function addMessage(topicId, message) {
return db.transaction('rw', [db.topics, db.messages, db.wordIndex], async () => {
// Add timestamp if not provided
const timestamp = message.timestamp || Date.now();
message.timestamp = timestamp;
message.topicId = topicId;
// Save message
const messageId = await db.messages.put(message);
// Update topic metadata
const topic = await db.topics.get(topicId);
if (topic) {
topic.lastActive = timestamp;
topic.messageCnt = (topic.messageCnt || 0) + 1;
if (message.role === 'user') {
topic.userMessageCnt = (topic.userMessageCnt || 0) + 1;
} else {
topic.assistantMessageCnt = (topic.assistantMessageCnt || 0) + 1;
}
if (message.tokens) {
topic.totalTokens = (topic.totalTokens || 0) + message.tokens;
}
await db.topics.put(topic);
}
// Index words for search
if (message.content) {
await indexMessageContent(message.content, topicId, messageId);
}
return messageId;
});
}
```
### Getting User's Conversations
```js
async function listTopics(sortBy = 'lastActive', limit = 20) {
return db.topics
.orderBy(sortBy)
.reverse() // newest first
.limit(limit)
.toArray();
}
```
### Search Implementation
```js
async function searchMessages(query, topicId = null) {
const words = query.toLowerCase()
.split(/\W+/)
.filter(word => word.length >= 3);
if (words.length === 0) return [];
// First find matching messageIds from word index
let matchingMessages = [];
await db.transaction('r', [db.wordIndex, db.messages], async () => {
const messageIds = new Set();
// For each search word, find matching messages
for (const word of words) {
let collection = db.wordIndex.where('word').equals(word);
// Filter by topic if specified
if (topicId !== null) {
collection = collection.and(entry => entry.topicId === topicId);
}
const entries = await collection.toArray();
entries.forEach(entry => messageIds.add(entry.messageId));
}
// Fetch the actual messages
if (messageIds.size > 0) {
matchingMessages = await db.messages
.where('id')
.anyOf([...messageIds])
.toArray();
}
});
// Sort by relevance (matching multiple words) then by recency
return matchingMessages
.sort((a, b) => b.timestamp - a.timestamp);
}
```
## Browser Storage Considerations
1. **Storage Limits:**
- Implement checks for available storage (typically 2-10MB on mobile, more on desktop)
- Add graceful degradation when limits are reached
- Provide user-configurable message retention options
2. **Offline Support:**
- Queue new messages when offline
- Sync with server when connection is restored
3. **Privacy & Security:**
- Offer encryption for sensitive conversations
- Add export functionality for user data ownership
- Implement clear data options
## User Stories
- **As a returning user**, I want to see all my previous chat topics/conversations even after closing my browser.
- **As an active user**, I want to maintain multiple separate chat conversations locally.
- **As a privacy-conscious user**, I want my chat history stored on my device, not the server.
- **As a mobile user**, I want my app to work with limited storage.
## Acceptance Criteria
- [x] Chat messages persist in the browser's IndexedDB via Dexie.js.
- [x] Users can create and maintain multiple chat topics locally.
- [x] On app load, the user's previous topics and messages are retrieved from local storage.
- [x] Conversation threads display in chronological order.
- [x] Message search works across the user's local conversations.
- [x] The app handles storage quota limits gracefully.
- [x] Users can export and import their chat history.
- [x] Local data can be cleared by the user.
- [ ] The solution works across major browsers (Chrome, Firefox, Safari, Edge).
## Dependencies
- Add Dexie.js as a dependency:
```bash
npm install dexie
```
- Add a word tokenization and search utility:
```bash
npm install lunr
```
## Testing / QA
- Test persistence across browser restarts and page refreshes
- Verify proper handling of storage quotas
- Test with large conversation volumes
- Verify search functionality with various query patterns
- Test in private/incognito browsing modes
- Validate behavior on low-end mobile devices
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.