Skip to content

Commit a9bd64c

Browse files
committed
hot-fix: chat conversation reaching max depth with update (#442)
1 parent 9f548a4 commit a9bd64c

File tree

1 file changed

+107
-68
lines changed

1 file changed

+107
-68
lines changed

apps/web/stores/chat.ts

Lines changed: 107 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,120 @@
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+
}
434

535
export interface ConversationSummary {
6-
id: string;
7-
title?: string;
8-
lastUpdated: string;
36+
id: string
37+
title?: string
38+
lastUpdated: string
939
}
1040

1141
interface ConversationRecord {
12-
messages: UIMessage[];
13-
title?: string;
14-
lastUpdated: string;
42+
messages: UIMessage[]
43+
title?: string
44+
lastUpdated: string
1545
}
1646

1747
interface ProjectConversationsState {
18-
currentChatId: string | null;
19-
conversations: Record<string, ConversationRecord>;
48+
currentChatId: string | null
49+
conversations: Record<string, ConversationRecord>
2050
}
2151

2252
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
2555
setConversation: (
2656
projectId: string,
2757
chatId: string,
2858
messages: UIMessage[],
29-
) => void;
30-
deleteConversation: (projectId: string, chatId: string) => void;
59+
) => void
60+
deleteConversation: (projectId: string, chatId: string) => void
3161
setConversationTitle: (
3262
projectId: string,
3363
chatId: string,
3464
title: string | undefined,
35-
) => void;
65+
) => void
3666
}
3767

3868
export const usePersistentChatStore = create<ConversationsStoreState>()(
3969
persist(
40-
(set, get) => ({
70+
(set, _get) => ({
4171
byProject: {},
4272

4373
setCurrentChatId(projectId, chatId) {
4474
set((state) => {
4575
const project = state.byProject[projectId] ?? {
4676
currentChatId: null,
4777
conversations: {},
48-
};
78+
}
4979
return {
5080
byProject: {
5181
...state.byProject,
5282
[projectId]: { ...project, currentChatId: chatId },
5383
},
54-
};
55-
});
84+
}
85+
})
5686
},
5787

5888
setConversation(projectId, chatId, messages) {
59-
const now = new Date().toISOString();
89+
const now = new Date().toISOString()
6090
set((state) => {
6191
const project = state.byProject[projectId] ?? {
6292
currentChatId: null,
6393
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+
66105
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+
})()
71110

72111
const record: ConversationRecord = {
73112
messages,
74113
title: existing?.title,
75114
lastUpdated: shouldTouchLastUpdated
76115
? now
77116
: (existing?.lastUpdated ?? now),
78-
};
117+
}
79118
return {
80119
byProject: {
81120
...state.byProject,
@@ -87,37 +126,37 @@ export const usePersistentChatStore = create<ConversationsStoreState>()(
87126
},
88127
},
89128
},
90-
};
91-
});
129+
}
130+
})
92131
},
93132

94133
deleteConversation(projectId, chatId) {
95134
set((state) => {
96135
const project = state.byProject[projectId] ?? {
97136
currentChatId: null,
98137
conversations: {},
99-
};
100-
const { [chatId]: _, ...rest } = project.conversations;
138+
}
139+
const { [chatId]: _, ...rest } = project.conversations
101140
const nextCurrent =
102-
project.currentChatId === chatId ? null : project.currentChatId;
141+
project.currentChatId === chatId ? null : project.currentChatId
103142
return {
104143
byProject: {
105144
...state.byProject,
106145
[projectId]: { currentChatId: nextCurrent, conversations: rest },
107146
},
108-
};
109-
});
147+
}
148+
})
110149
},
111150

112151
setConversationTitle(projectId, chatId, title) {
113-
const now = new Date().toISOString();
152+
const now = new Date().toISOString()
114153
set((state) => {
115154
const project = state.byProject[projectId] ?? {
116155
currentChatId: null,
117156
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 }
121160
return {
122161
byProject: {
123162
...state.byProject,
@@ -129,76 +168,76 @@ export const usePersistentChatStore = create<ConversationsStoreState>()(
129168
},
130169
},
131170
},
132-
};
133-
});
171+
}
172+
})
134173
},
135174
}),
136175
{
137176
name: "supermemory-chats",
138177
},
139178
),
140-
);
179+
)
141180

142181
// Always scoped to the current project via useProject
143-
import { useProject } from ".";
182+
import { useProject } from "."
144183

145184
export function usePersistentChat() {
146-
const { selectedProject } = useProject();
147-
const projectId = selectedProject;
185+
const { selectedProject } = useProject()
186+
const projectId = selectedProject
148187

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)
152191
const deleteConversationRaw = usePersistentChatStore(
153192
(s) => s.deleteConversation,
154-
);
193+
)
155194
const setConversationTitleRaw = usePersistentChatStore(
156195
(s) => s.setConversationTitle,
157-
);
196+
)
158197

159198
const conversations: ConversationSummary[] = (() => {
160-
const convs = projectState?.conversations ?? {};
199+
const convs = projectState?.conversations ?? {}
161200
return Object.entries(convs).map(([id, rec]) => ({
162201
id,
163202
title: rec.title,
164203
lastUpdated: rec.lastUpdated,
165-
}));
166-
})();
204+
}))
205+
})()
167206

168-
const currentChatId = projectState?.currentChatId ?? null;
207+
const currentChatId = projectState?.currentChatId ?? null
169208

170209
function setCurrentChatId(chatId: string | null): void {
171-
setCurrentChatIdRaw(projectId, chatId);
210+
setCurrentChatIdRaw(projectId, chatId)
172211
}
173212

174213
function setConversation(chatId: string, messages: UIMessage[]): void {
175-
setConversationRaw(projectId, chatId, messages);
214+
setConversationRaw(projectId, chatId, messages)
176215
}
177216

178217
function deleteConversation(chatId: string): void {
179-
deleteConversationRaw(projectId, chatId);
218+
deleteConversationRaw(projectId, chatId)
180219
}
181220

182221
function setConversationTitle(
183222
chatId: string,
184223
title: string | undefined,
185224
): void {
186-
setConversationTitleRaw(projectId, chatId, title);
225+
setConversationTitleRaw(projectId, chatId, title)
187226
}
188227

189228
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
194233
}
195234

196235
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 }
202241
}
203242

204243
return {
@@ -210,5 +249,5 @@ export function usePersistentChat() {
210249
setConversationTitle,
211250
getCurrentConversation,
212251
getCurrentChat,
213-
};
252+
}
214253
}

0 commit comments

Comments
 (0)