Skip to content

Commit bbe5c88

Browse files
feat(chat): save messages in session storage (#6710)
1 parent 9e79c1f commit bbe5c88

File tree

2 files changed

+121
-1
lines changed

2 files changed

+121
-1
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { ChatState, CACHE_KEY } from '../chat';
2+
3+
// mock AbstractChat to avoid "TypeError: Class extends value undefined is not a constructor or null"
4+
jest.mock('ai', () => {
5+
return {
6+
AbstractChat: class {},
7+
};
8+
});
9+
10+
describe('ChatState', () => {
11+
beforeAll(() => {
12+
// Mock sessionStorage for the tests
13+
const sessionStorageMock = (() => {
14+
const store: Record<string, string> = {};
15+
return {
16+
getItem(key: string) {
17+
return store[key] || null;
18+
},
19+
setItem(key: string, value: string) {
20+
store[key] = value.toString();
21+
},
22+
removeItem(key: string) {
23+
delete store[key];
24+
},
25+
};
26+
})();
27+
28+
Object.defineProperty(globalThis, 'sessionStorage', {
29+
value: sessionStorageMock,
30+
});
31+
});
32+
33+
beforeEach(() => {
34+
// Clear sessionStorage before each test
35+
sessionStorage.removeItem(CACHE_KEY);
36+
});
37+
38+
afterAll(() => {
39+
// Clean up the mock
40+
delete (globalThis as any).sessionStorage;
41+
});
42+
43+
it('should save messages to sessionStorage when status changes to ready', () => {
44+
const chatState = new ChatState<any>();
45+
const message = { role: 'user', content: 'Hello' };
46+
chatState.status = 'submitted';
47+
chatState.messages = [message];
48+
expect(sessionStorage.getItem(CACHE_KEY)).toBe(null);
49+
50+
chatState.status = 'streaming';
51+
expect(sessionStorage.getItem(CACHE_KEY)).toBe(null);
52+
53+
chatState.status = 'ready';
54+
expect(sessionStorage.getItem(CACHE_KEY)).toBe(JSON.stringify([message]));
55+
});
56+
57+
it('should load initial messages from sessionStorage', () => {
58+
const initialMessages = [
59+
{ role: 'user', content: 'Hello' },
60+
{ role: 'bot', content: 'Hi there!' },
61+
];
62+
sessionStorage.setItem(CACHE_KEY, JSON.stringify(initialMessages));
63+
64+
const chatState = new ChatState();
65+
expect(chatState.messages).toEqual(initialMessages);
66+
});
67+
68+
it('should not save messages to sessionStorage when status is not ready', () => {
69+
const chatState = new ChatState<any>();
70+
const message = { role: 'user', content: 'Hello' };
71+
chatState.status = 'submitted';
72+
chatState.messages = [message];
73+
expect(sessionStorage.getItem(CACHE_KEY)).toBe(null);
74+
75+
chatState.status = 'streaming';
76+
expect(sessionStorage.getItem(CACHE_KEY)).toBe(null);
77+
chatState.status = 'error';
78+
expect(sessionStorage.getItem(CACHE_KEY)).toBe(null);
79+
});
80+
81+
it('should handle sessionStorage being unavailable', () => {
82+
// eslint-disable-next-line jest/unbound-method
83+
const originalSetItem = sessionStorage.setItem;
84+
sessionStorage.setItem = () => {
85+
throw new Error('sessionStorage is full');
86+
};
87+
88+
const chatState = new ChatState<any>();
89+
const message = { role: 'user', content: 'Hello' };
90+
chatState.status = 'submitted';
91+
chatState.messages = [message];
92+
expect(sessionStorage.getItem(CACHE_KEY)).toBe(null);
93+
chatState.status = 'ready';
94+
expect(sessionStorage.getItem(CACHE_KEY)).toBe(null);
95+
96+
sessionStorage.setItem = originalSetItem;
97+
});
98+
});

packages/instantsearch.js/src/lib/chat/chat.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ export type { UIMessage };
1111
export { AbstractChat };
1212
export { ChatInit };
1313

14+
export const CACHE_KEY = 'instantsearch-chat-initial-messages';
15+
16+
function getDefaultInitialMessages<
17+
TUIMessage extends UIMessage
18+
>(): TUIMessage[] {
19+
const initialMessages = sessionStorage.getItem(CACHE_KEY);
20+
return initialMessages ? JSON.parse(initialMessages) : [];
21+
}
22+
1423
export class ChatState<TUiMessage extends UIMessage>
1524
implements BaseChatState<TUiMessage>
1625
{
@@ -22,8 +31,21 @@ export class ChatState<TUiMessage extends UIMessage>
2231
_statusCallbacks = new Set<() => void>();
2332
_errorCallbacks = new Set<() => void>();
2433

25-
constructor(initialMessages: TUiMessage[] = []) {
34+
constructor(
35+
initialMessages: TUiMessage[] = getDefaultInitialMessages<TUiMessage>()
36+
) {
2637
this._messages = initialMessages;
38+
const saveMessagesInLocalStorage = () => {
39+
if (this.status === 'ready') {
40+
try {
41+
sessionStorage.setItem(CACHE_KEY, JSON.stringify(this.messages));
42+
} catch (e) {
43+
// Do nothing if sessionStorage is not available or full
44+
}
45+
}
46+
};
47+
this['~registerMessagesCallback'](saveMessagesInLocalStorage);
48+
this['~registerStatusCallback'](saveMessagesInLocalStorage);
2749
}
2850

2951
get status(): ChatStatus {

0 commit comments

Comments
 (0)