1
- import type { UIMessage } from "@ai-sdk/react" ;
2
- import { create } from "zustand" ;
3
- import { persist } from "zustand/middleware" ;
1
+ import type { UIMessage } from "@ai-sdk/react"
2
+ import { create } from "zustand"
3
+ import { persist } from "zustand/middleware"
4
+
5
+ /**
6
+ * Deep equality check for UIMessage arrays to prevent unnecessary state updates
7
+ */
8
+ function areUIMessageArraysEqual ( a : UIMessage [ ] , b : UIMessage [ ] ) : boolean {
9
+ if ( a === b ) return true
10
+ if ( a . length !== b . length ) return false
11
+
12
+ for ( let i = 0 ; i < a . length ; i ++ ) {
13
+ const msgA = a [ i ]
14
+ const msgB = b [ i ]
15
+
16
+ // Both messages should exist at this index
17
+ if ( ! msgA || ! msgB ) return false
18
+
19
+ if ( msgA === msgB ) continue
20
+
21
+ if ( msgA . id !== msgB . id || msgA . role !== msgB . role ) {
22
+ return false
23
+ }
24
+
25
+ // Compare the entire message using JSON serialization as a fallback
26
+ // This handles all properties including parts, toolInvocations, etc.
27
+ if ( JSON . stringify ( msgA ) !== JSON . stringify ( msgB ) ) {
28
+ return false
29
+ }
30
+ }
31
+
32
+ return true
33
+ }
4
34
5
35
export interface ConversationSummary {
6
- id : string ;
7
- title ?: string ;
8
- lastUpdated : string ;
36
+ id : string
37
+ title ?: string
38
+ lastUpdated : string
9
39
}
10
40
11
41
interface ConversationRecord {
12
- messages : UIMessage [ ] ;
13
- title ?: string ;
14
- lastUpdated : string ;
42
+ messages : UIMessage [ ]
43
+ title ?: string
44
+ lastUpdated : string
15
45
}
16
46
17
47
interface ProjectConversationsState {
18
- currentChatId : string | null ;
19
- conversations : Record < string , ConversationRecord > ;
48
+ currentChatId : string | null
49
+ conversations : Record < string , ConversationRecord >
20
50
}
21
51
22
52
interface ConversationsStoreState {
23
- byProject : Record < string , ProjectConversationsState > ;
24
- setCurrentChatId : ( projectId : string , chatId : string | null ) => void ;
53
+ byProject : Record < string , ProjectConversationsState >
54
+ setCurrentChatId : ( projectId : string , chatId : string | null ) => void
25
55
setConversation : (
26
56
projectId : string ,
27
57
chatId : string ,
28
58
messages : UIMessage [ ] ,
29
- ) => void ;
30
- deleteConversation : ( projectId : string , chatId : string ) => void ;
59
+ ) => void
60
+ deleteConversation : ( projectId : string , chatId : string ) => void
31
61
setConversationTitle : (
32
62
projectId : string ,
33
63
chatId : string ,
34
64
title : string | undefined ,
35
- ) => void ;
65
+ ) => void
36
66
}
37
67
38
68
export const usePersistentChatStore = create < ConversationsStoreState > ( ) (
39
69
persist (
40
- ( set , get ) => ( {
70
+ ( set , _get ) => ( {
41
71
byProject : { } ,
42
72
43
73
setCurrentChatId ( projectId , chatId ) {
44
74
set ( ( state ) => {
45
75
const project = state . byProject [ projectId ] ?? {
46
76
currentChatId : null ,
47
77
conversations : { } ,
48
- } ;
78
+ }
49
79
return {
50
80
byProject : {
51
81
...state . byProject ,
52
82
[ projectId ] : { ...project , currentChatId : chatId } ,
53
83
} ,
54
- } ;
55
- } ) ;
84
+ }
85
+ } )
56
86
} ,
57
87
58
88
setConversation ( projectId , chatId , messages ) {
59
- const now = new Date ( ) . toISOString ( ) ;
89
+ const now = new Date ( ) . toISOString ( )
60
90
set ( ( state ) => {
61
91
const project = state . byProject [ projectId ] ?? {
62
92
currentChatId : null ,
63
93
conversations : { } ,
64
- } ;
65
- const existing = project . conversations [ chatId ] ;
94
+ }
95
+ const existing = project . conversations [ chatId ]
96
+
97
+ // Check if messages are actually different to prevent unnecessary updates
98
+ if (
99
+ existing &&
100
+ areUIMessageArraysEqual ( existing . messages , messages )
101
+ ) {
102
+ return state // No change needed
103
+ }
104
+
66
105
const shouldTouchLastUpdated = ( ( ) => {
67
- if ( ! existing ) return messages . length > 0 ;
68
- const previousLength = existing . messages ?. length ?? 0 ;
69
- return messages . length > previousLength ;
70
- } ) ( ) ;
106
+ if ( ! existing ) return messages . length > 0
107
+ const previousLength = existing . messages ?. length ?? 0
108
+ return messages . length > previousLength
109
+ } ) ( )
71
110
72
111
const record : ConversationRecord = {
73
112
messages,
74
113
title : existing ?. title ,
75
114
lastUpdated : shouldTouchLastUpdated
76
115
? now
77
116
: ( existing ?. lastUpdated ?? now ) ,
78
- } ;
117
+ }
79
118
return {
80
119
byProject : {
81
120
...state . byProject ,
@@ -87,37 +126,37 @@ export const usePersistentChatStore = create<ConversationsStoreState>()(
87
126
} ,
88
127
} ,
89
128
} ,
90
- } ;
91
- } ) ;
129
+ }
130
+ } )
92
131
} ,
93
132
94
133
deleteConversation ( projectId , chatId ) {
95
134
set ( ( state ) => {
96
135
const project = state . byProject [ projectId ] ?? {
97
136
currentChatId : null ,
98
137
conversations : { } ,
99
- } ;
100
- const { [ chatId ] : _ , ...rest } = project . conversations ;
138
+ }
139
+ const { [ chatId ] : _ , ...rest } = project . conversations
101
140
const nextCurrent =
102
- project . currentChatId === chatId ? null : project . currentChatId ;
141
+ project . currentChatId === chatId ? null : project . currentChatId
103
142
return {
104
143
byProject : {
105
144
...state . byProject ,
106
145
[ projectId ] : { currentChatId : nextCurrent , conversations : rest } ,
107
146
} ,
108
- } ;
109
- } ) ;
147
+ }
148
+ } )
110
149
} ,
111
150
112
151
setConversationTitle ( projectId , chatId , title ) {
113
- const now = new Date ( ) . toISOString ( ) ;
152
+ const now = new Date ( ) . toISOString ( )
114
153
set ( ( state ) => {
115
154
const project = state . byProject [ projectId ] ?? {
116
155
currentChatId : null ,
117
156
conversations : { } ,
118
- } ;
119
- const existing = project . conversations [ chatId ] ;
120
- if ( ! existing ) return { byProject : state . byProject } ;
157
+ }
158
+ const existing = project . conversations [ chatId ]
159
+ if ( ! existing ) return { byProject : state . byProject }
121
160
return {
122
161
byProject : {
123
162
...state . byProject ,
@@ -129,76 +168,76 @@ export const usePersistentChatStore = create<ConversationsStoreState>()(
129
168
} ,
130
169
} ,
131
170
} ,
132
- } ;
133
- } ) ;
171
+ }
172
+ } )
134
173
} ,
135
174
} ) ,
136
175
{
137
176
name : "supermemory-chats" ,
138
177
} ,
139
178
) ,
140
- ) ;
179
+ )
141
180
142
181
// Always scoped to the current project via useProject
143
- import { useProject } from "." ;
182
+ import { useProject } from "."
144
183
145
184
export function usePersistentChat ( ) {
146
- const { selectedProject } = useProject ( ) ;
147
- const projectId = selectedProject ;
185
+ const { selectedProject } = useProject ( )
186
+ const projectId = selectedProject
148
187
149
- const projectState = usePersistentChatStore ( ( s ) => s . byProject [ projectId ] ) ;
150
- const setCurrentChatIdRaw = usePersistentChatStore ( ( s ) => s . setCurrentChatId ) ;
151
- const setConversationRaw = usePersistentChatStore ( ( s ) => s . setConversation ) ;
188
+ const projectState = usePersistentChatStore ( ( s ) => s . byProject [ projectId ] )
189
+ const setCurrentChatIdRaw = usePersistentChatStore ( ( s ) => s . setCurrentChatId )
190
+ const setConversationRaw = usePersistentChatStore ( ( s ) => s . setConversation )
152
191
const deleteConversationRaw = usePersistentChatStore (
153
192
( s ) => s . deleteConversation ,
154
- ) ;
193
+ )
155
194
const setConversationTitleRaw = usePersistentChatStore (
156
195
( s ) => s . setConversationTitle ,
157
- ) ;
196
+ )
158
197
159
198
const conversations : ConversationSummary [ ] = ( ( ) => {
160
- const convs = projectState ?. conversations ?? { } ;
199
+ const convs = projectState ?. conversations ?? { }
161
200
return Object . entries ( convs ) . map ( ( [ id , rec ] ) => ( {
162
201
id,
163
202
title : rec . title ,
164
203
lastUpdated : rec . lastUpdated ,
165
- } ) ) ;
166
- } ) ( ) ;
204
+ } ) )
205
+ } ) ( )
167
206
168
- const currentChatId = projectState ?. currentChatId ?? null ;
207
+ const currentChatId = projectState ?. currentChatId ?? null
169
208
170
209
function setCurrentChatId ( chatId : string | null ) : void {
171
- setCurrentChatIdRaw ( projectId , chatId ) ;
210
+ setCurrentChatIdRaw ( projectId , chatId )
172
211
}
173
212
174
213
function setConversation ( chatId : string , messages : UIMessage [ ] ) : void {
175
- setConversationRaw ( projectId , chatId , messages ) ;
214
+ setConversationRaw ( projectId , chatId , messages )
176
215
}
177
216
178
217
function deleteConversation ( chatId : string ) : void {
179
- deleteConversationRaw ( projectId , chatId ) ;
218
+ deleteConversationRaw ( projectId , chatId )
180
219
}
181
220
182
221
function setConversationTitle (
183
222
chatId : string ,
184
223
title : string | undefined ,
185
224
) : void {
186
- setConversationTitleRaw ( projectId , chatId , title ) ;
225
+ setConversationTitleRaw ( projectId , chatId , title )
187
226
}
188
227
189
228
function getCurrentConversation ( ) : UIMessage [ ] | undefined {
190
- const convs = projectState ?. conversations ?? { } ;
191
- const id = currentChatId ;
192
- if ( ! id ) return undefined ;
193
- return convs [ id ] ?. messages ;
229
+ const convs = projectState ?. conversations ?? { }
230
+ const id = currentChatId
231
+ if ( ! id ) return undefined
232
+ return convs [ id ] ?. messages
194
233
}
195
234
196
235
function getCurrentChat ( ) : ConversationSummary | undefined {
197
- const id = currentChatId ;
198
- if ( ! id ) return undefined ;
199
- const rec = projectState ?. conversations ?. [ id ] ;
200
- if ( ! rec ) return undefined ;
201
- return { id, title : rec . title , lastUpdated : rec . lastUpdated } ;
236
+ const id = currentChatId
237
+ if ( ! id ) return undefined
238
+ const rec = projectState ?. conversations ?. [ id ]
239
+ if ( ! rec ) return undefined
240
+ return { id, title : rec . title , lastUpdated : rec . lastUpdated }
202
241
}
203
242
204
243
return {
@@ -210,5 +249,5 @@ export function usePersistentChat() {
210
249
setConversationTitle,
211
250
getCurrentConversation,
212
251
getCurrentChat,
213
- } ;
252
+ }
214
253
}
0 commit comments