mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-05-17 12:20:04 +00:00
268 lines
6.7 KiB
TypeScript
268 lines
6.7 KiB
TypeScript
import type { UIMessage } from "@ai-sdk/react"
|
|
import { create } from "zustand"
|
|
import { persist, createJSONStorage } from "zustand/middleware"
|
|
import { useCallback } from "react"
|
|
import { indexedDBStorage } from "./indexeddb-storage"
|
|
|
|
/**
|
|
* Deep equality check for UIMessage arrays to prevent unnecessary state updates
|
|
*/
|
|
export function areUIMessageArraysEqual(
|
|
a: UIMessage[],
|
|
b: UIMessage[],
|
|
): boolean {
|
|
if (a === b) return true
|
|
if (a.length !== b.length) return false
|
|
|
|
for (let i = 0; i < a.length; i++) {
|
|
const msgA = a[i]
|
|
const msgB = b[i]
|
|
|
|
// Both messages should exist at this index
|
|
if (!msgA || !msgB) return false
|
|
|
|
if (msgA === msgB) continue
|
|
|
|
if (msgA.id !== msgB.id || msgA.role !== msgB.role) {
|
|
return false
|
|
}
|
|
|
|
if (msgA.content !== msgB.content) return false
|
|
|
|
if (JSON.stringify(msgA.parts) !== JSON.stringify(msgB.parts)) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
export interface ConversationSummary {
|
|
id: string
|
|
title?: string
|
|
lastUpdated: string
|
|
}
|
|
|
|
interface ConversationRecord {
|
|
messages: UIMessage[]
|
|
title?: string
|
|
lastUpdated: string
|
|
}
|
|
|
|
interface ProjectConversationsState {
|
|
currentChatId: string | null
|
|
conversations: Record<string, ConversationRecord>
|
|
}
|
|
|
|
interface ConversationsStoreState {
|
|
byProject: Record<string, ProjectConversationsState>
|
|
setCurrentChatId: (projectId: string, chatId: string | null) => void
|
|
setConversation: (
|
|
projectId: string,
|
|
chatId: string,
|
|
messages: UIMessage[],
|
|
) => void
|
|
deleteConversation: (projectId: string, chatId: string) => void
|
|
setConversationTitle: (
|
|
projectId: string,
|
|
chatId: string,
|
|
title: string | undefined,
|
|
) => void
|
|
}
|
|
|
|
export const usePersistentChatStore = create<ConversationsStoreState>()(
|
|
persist(
|
|
(set, _get) => ({
|
|
byProject: {},
|
|
|
|
setCurrentChatId(projectId, chatId) {
|
|
set((state) => {
|
|
const project = state.byProject[projectId] ?? {
|
|
currentChatId: null,
|
|
conversations: {},
|
|
}
|
|
return {
|
|
byProject: {
|
|
...state.byProject,
|
|
[projectId]: { ...project, currentChatId: chatId },
|
|
},
|
|
}
|
|
})
|
|
},
|
|
|
|
setConversation(projectId, chatId, messages) {
|
|
const now = new Date().toISOString()
|
|
set((state) => {
|
|
const project = state.byProject[projectId] ?? {
|
|
currentChatId: null,
|
|
conversations: {},
|
|
}
|
|
const existing = project.conversations[chatId]
|
|
|
|
// Check if messages are actually different to prevent unnecessary updates
|
|
if (
|
|
existing &&
|
|
areUIMessageArraysEqual(existing.messages, messages)
|
|
) {
|
|
return state // No change needed
|
|
}
|
|
|
|
const shouldTouchLastUpdated = (() => {
|
|
if (!existing) return messages.length > 0
|
|
const previousLength = existing.messages?.length ?? 0
|
|
return messages.length > previousLength
|
|
})()
|
|
|
|
const record: ConversationRecord = {
|
|
messages,
|
|
title: existing?.title,
|
|
lastUpdated: shouldTouchLastUpdated
|
|
? now
|
|
: (existing?.lastUpdated ?? now),
|
|
}
|
|
return {
|
|
byProject: {
|
|
...state.byProject,
|
|
[projectId]: {
|
|
currentChatId: project.currentChatId,
|
|
conversations: {
|
|
...project.conversations,
|
|
[chatId]: record,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
})
|
|
},
|
|
|
|
deleteConversation(projectId, chatId) {
|
|
set((state) => {
|
|
const project = state.byProject[projectId] ?? {
|
|
currentChatId: null,
|
|
conversations: {},
|
|
}
|
|
const { [chatId]: _, ...rest } = project.conversations
|
|
const nextCurrent =
|
|
project.currentChatId === chatId ? null : project.currentChatId
|
|
return {
|
|
byProject: {
|
|
...state.byProject,
|
|
[projectId]: { currentChatId: nextCurrent, conversations: rest },
|
|
},
|
|
}
|
|
})
|
|
},
|
|
|
|
setConversationTitle(projectId, chatId, title) {
|
|
const now = new Date().toISOString()
|
|
set((state) => {
|
|
const project = state.byProject[projectId] ?? {
|
|
currentChatId: null,
|
|
conversations: {},
|
|
}
|
|
const existing = project.conversations[chatId]
|
|
if (!existing) return { byProject: state.byProject }
|
|
return {
|
|
byProject: {
|
|
...state.byProject,
|
|
[projectId]: {
|
|
currentChatId: project.currentChatId,
|
|
conversations: {
|
|
...project.conversations,
|
|
[chatId]: { ...existing, title, lastUpdated: now },
|
|
},
|
|
},
|
|
},
|
|
}
|
|
})
|
|
},
|
|
}),
|
|
{
|
|
name: "supermemory-chats",
|
|
storage: createJSONStorage(() => indexedDBStorage),
|
|
},
|
|
),
|
|
)
|
|
|
|
// Always scoped to the current project via useProject
|
|
import { useProject } from "."
|
|
|
|
export function usePersistentChat() {
|
|
const { selectedProject } = useProject()
|
|
const projectId = selectedProject
|
|
|
|
const projectState = usePersistentChatStore((s) => s.byProject[projectId])
|
|
const setCurrentChatIdRaw = usePersistentChatStore((s) => s.setCurrentChatId)
|
|
const setConversationRaw = usePersistentChatStore((s) => s.setConversation)
|
|
const deleteConversationRaw = usePersistentChatStore(
|
|
(s) => s.deleteConversation,
|
|
)
|
|
const setConversationTitleRaw = usePersistentChatStore(
|
|
(s) => s.setConversationTitle,
|
|
)
|
|
|
|
const conversations: ConversationSummary[] = (() => {
|
|
const convs = projectState?.conversations ?? {}
|
|
return Object.entries(convs).map(([id, rec]) => ({
|
|
id,
|
|
title: rec.title,
|
|
lastUpdated: rec.lastUpdated,
|
|
}))
|
|
})()
|
|
|
|
const currentChatId = projectState?.currentChatId ?? null
|
|
|
|
const setCurrentChatId = useCallback(
|
|
(chatId: string | null): void => {
|
|
setCurrentChatIdRaw(projectId, chatId)
|
|
},
|
|
[projectId, setCurrentChatIdRaw],
|
|
)
|
|
|
|
const setConversation = useCallback(
|
|
(chatId: string, messages: UIMessage[]): void => {
|
|
setConversationRaw(projectId, chatId, messages)
|
|
},
|
|
[projectId, setConversationRaw],
|
|
)
|
|
|
|
const deleteConversation = useCallback(
|
|
(chatId: string): void => {
|
|
deleteConversationRaw(projectId, chatId)
|
|
},
|
|
[projectId, deleteConversationRaw],
|
|
)
|
|
|
|
const setConversationTitle = useCallback(
|
|
(chatId: string, title: string | undefined): void => {
|
|
setConversationTitleRaw(projectId, chatId, title)
|
|
},
|
|
[projectId, setConversationTitleRaw],
|
|
)
|
|
|
|
const getCurrentConversation = useCallback((): UIMessage[] | undefined => {
|
|
const convs = projectState?.conversations ?? {}
|
|
const id = currentChatId
|
|
if (!id) return undefined
|
|
return convs[id]?.messages
|
|
}, [projectState?.conversations, currentChatId])
|
|
|
|
const getCurrentChat = useCallback((): ConversationSummary | undefined => {
|
|
const id = currentChatId
|
|
if (!id) return undefined
|
|
const rec = projectState?.conversations?.[id]
|
|
if (!rec) return undefined
|
|
return { id, title: rec.title, lastUpdated: rec.lastUpdated }
|
|
}, [currentChatId, projectState?.conversations])
|
|
|
|
return {
|
|
conversations,
|
|
currentChatId,
|
|
setCurrentChatId,
|
|
setConversation,
|
|
deleteConversation,
|
|
setConversationTitle,
|
|
getCurrentConversation,
|
|
getCurrentChat,
|
|
}
|
|
}
|