diff options
| -rw-r--r-- | apps/web/components/graph-dialog.tsx | 3 | ||||
| -rw-r--r-- | apps/web/components/memories.tsx | 99 | ||||
| -rw-r--r-- | apps/web/components/views/chat/chat-messages.tsx | 55 | ||||
| -rw-r--r-- | apps/web/stores/chat.ts | 2 | ||||
| -rw-r--r-- | packages/lib/auth-context.tsx | 30 | ||||
| -rw-r--r-- | packages/lib/posthog.tsx | 29 | ||||
| -rw-r--r-- | packages/ui/memory-graph/graph-webgl-canvas.tsx | 1 | ||||
| -rw-r--r-- | packages/ui/pages/login.tsx | 56 |
8 files changed, 191 insertions, 84 deletions
diff --git a/apps/web/components/graph-dialog.tsx b/apps/web/components/graph-dialog.tsx index 9378b592..2e74b3b1 100644 --- a/apps/web/components/graph-dialog.tsx +++ b/apps/web/components/graph-dialog.tsx @@ -62,6 +62,9 @@ export function GraphDialog() { return response.data }, getNextPageParam: (lastPage, allPages) => { + if (!lastPage || !lastPage.pagination) return undefined + if (!Array.isArray(allPages)) return undefined + const loaded = allPages.reduce( (acc, p) => acc + (p.documents?.length ?? 0), 0, diff --git a/apps/web/components/memories.tsx b/apps/web/components/memories.tsx index 161d4f14..faf146c5 100644 --- a/apps/web/components/memories.tsx +++ b/apps/web/components/memories.tsx @@ -60,6 +60,9 @@ export function Memories() { return response.data }, getNextPageParam: (lastPage, allPages) => { + if (!lastPage || !lastPage.pagination) return undefined + if (!Array.isArray(allPages)) return undefined + const loaded = allPages.reduce( (acc, p) => acc + (p.documents?.length ?? 0), 0, @@ -177,42 +180,22 @@ export function Memories() { } return ( - <> - <div className="relative h-full mx-4 md:mx-24"> - <MasonryMemoryList - documents={allDocuments} - error={error} - hasMore={hasMore} - isLoading={isPending} - isLoadingMore={isLoadingMore} - loadMoreDocuments={loadMoreDocuments} - totalLoaded={totalLoaded} - > - <div className="absolute inset-0 flex items-center justify-center"> - {!isMobile ? ( - <ConnectAIModal - onOpenChange={setShowConnectAIModal} - open={showConnectAIModal} - > - <div className="rounded-xl overflow-hidden cursor-pointer hover:bg-white/5 transition-colors p-6"> - <div className="relative z-10 text-slate-200 text-center"> - <div className="flex flex-col gap-3"> - <button - className="text-sm text-blue-400 hover:text-blue-300 transition-colors underline" - onClick={(e) => { - e.stopPropagation() - setShowAddMemoryView(true) - setShowConnectAIModal(false) - }} - type="button" - > - Add your first memory - </button> - </div> - </div> - </div> - </ConnectAIModal> - ) : ( + <div className="relative h-full mx-4 md:mx-24"> + <MasonryMemoryList + documents={allDocuments} + error={error} + hasMore={hasMore} + isLoading={isPending} + isLoadingMore={isLoadingMore} + loadMoreDocuments={loadMoreDocuments} + totalLoaded={totalLoaded} + > + <div className="absolute inset-0 flex items-center justify-center"> + {!isMobile ? ( + <ConnectAIModal + onOpenChange={setShowConnectAIModal} + open={showConnectAIModal} + > <div className="rounded-xl overflow-hidden cursor-pointer hover:bg-white/5 transition-colors p-6"> <div className="relative z-10 text-slate-200 text-center"> <div className="flex flex-col gap-3"> @@ -221,6 +204,7 @@ export function Memories() { onClick={(e) => { e.stopPropagation() setShowAddMemoryView(true) + setShowConnectAIModal(false) }} type="button" > @@ -229,17 +213,34 @@ export function Memories() { </div> </div> </div> - )} - </div> - </MasonryMemoryList> - - {showAddMemoryView && ( - <AddMemoryView - initialTab="note" - onClose={() => setShowAddMemoryView(false)} - /> - )} - </div> - </> + </ConnectAIModal> + ) : ( + <div className="rounded-xl overflow-hidden cursor-pointer hover:bg-white/5 transition-colors p-6"> + <div className="relative z-10 text-slate-200 text-center"> + <div className="flex flex-col gap-3"> + <button + className="text-sm text-blue-400 hover:text-blue-300 transition-colors underline" + onClick={(e) => { + e.stopPropagation() + setShowAddMemoryView(true) + }} + type="button" + > + Add your first memory + </button> + </div> + </div> + </div> + )} + </div> + </MasonryMemoryList> + + {showAddMemoryView && ( + <AddMemoryView + initialTab="note" + onClose={() => setShowAddMemoryView(false)} + /> + )} + </div> ) -}
\ No newline at end of file +} diff --git a/apps/web/components/views/chat/chat-messages.tsx b/apps/web/components/views/chat/chat-messages.tsx index ff4eca34..0c795ec0 100644 --- a/apps/web/components/views/chat/chat-messages.tsx +++ b/apps/web/components/views/chat/chat-messages.tsx @@ -1,6 +1,6 @@ "use client" -import { useChat, useCompletion } from "@ai-sdk/react" +import { useChat, useCompletion, type UIMessage } from "@ai-sdk/react" import { cn } from "@lib/utils" import { Button } from "@ui/components/button" import { DefaultChatTransport } from "ai" @@ -21,6 +21,7 @@ import { usePersistentChat, useProject } from "@/stores" import { useGraphHighlights } from "@/stores/highlights" import { modelNames, ModelIcon } from "@/lib/models" import { Spinner } from "../../spinner" +import { areUIMessageArraysEqual } from "@/stores/chat" interface MemoryResult { documentId?: string @@ -247,6 +248,10 @@ export function ChatMessages() { const activeChatIdRef = useRef<string | null>(null) const shouldGenerateTitleRef = useRef<boolean>(false) const hasRunInitialMessageRef = useRef<boolean>(false) + const lastSavedMessagesRef = useRef<UIMessage[] | null>(null) + const lastSavedActiveIdRef = useRef<string | null>(null) + const lastLoadedChatIdRef = useRef<string | null>(null) + const lastLoadedMessagesRef = useRef<UIMessage[] | null>(null) const { setDocumentIds } = useGraphHighlights() @@ -283,6 +288,10 @@ export function ChatMessages() { }) useEffect(() => { + lastLoadedMessagesRef.current = messages + }, [messages]) + + useEffect(() => { activeChatIdRef.current = currentChatId ?? id ?? null }, [currentChatId, id]) @@ -326,20 +335,56 @@ export function ChatMessages() { }, [id, currentChatId, setCurrentChatId]) useEffect(() => { + if (currentChatId !== lastLoadedChatIdRef.current) { + lastLoadedMessagesRef.current = null + lastSavedMessagesRef.current = null + } + + if (currentChatId === lastLoadedChatIdRef.current) { + setInput("") + return + } + const msgs = getCurrentConversation() + if (msgs && msgs.length > 0) { - setMessages(msgs) + const currentMessages = lastLoadedMessagesRef.current + if (!currentMessages || !areUIMessageArraysEqual(currentMessages, msgs)) { + lastLoadedMessagesRef.current = msgs + setMessages(msgs) + } } else if (!currentChatId) { - setMessages([]) + if ( + lastLoadedMessagesRef.current && + lastLoadedMessagesRef.current.length > 0 + ) { + lastLoadedMessagesRef.current = [] + setMessages([]) + } } + + lastLoadedChatIdRef.current = currentChatId setInput("") }, [currentChatId, getCurrentConversation, setMessages]) useEffect(() => { const activeId = currentChatId ?? id - if (activeId && messages.length > 0) { - setConversation(activeId, messages) + if (!activeId || messages.length === 0) { + return } + + if (activeId !== lastSavedActiveIdRef.current) { + lastSavedMessagesRef.current = null + lastSavedActiveIdRef.current = activeId + } + + const lastSaved = lastSavedMessagesRef.current + if (lastSaved && areUIMessageArraysEqual(lastSaved, messages)) { + return + } + + lastSavedMessagesRef.current = messages + setConversation(activeId, messages) }, [messages, currentChatId, id, setConversation]) const { complete } = useCompletion({ diff --git a/apps/web/stores/chat.ts b/apps/web/stores/chat.ts index f1701139..24f4084b 100644 --- a/apps/web/stores/chat.ts +++ b/apps/web/stores/chat.ts @@ -7,7 +7,7 @@ import { indexedDBStorage } from "./indexeddb-storage" /** * Deep equality check for UIMessage arrays to prevent unnecessary state updates */ -function areUIMessageArraysEqual(a: UIMessage[], b: UIMessage[]): boolean { +export function areUIMessageArraysEqual(a: UIMessage[], b: UIMessage[]): boolean { if (a === b) return true if (a.length !== b.length) return false diff --git a/packages/lib/auth-context.tsx b/packages/lib/auth-context.tsx index 7e6462fa..db24ec45 100644 --- a/packages/lib/auth-context.tsx +++ b/packages/lib/auth-context.tsx @@ -38,17 +38,23 @@ export function AuthProvider({ children }: { children: ReactNode }) { // biome-ignore lint/correctness/useExhaustiveDependencies: ignoring the setActiveOrg dependency useEffect(() => { if (session?.session.activeOrganizationId) { - authClient.organization.getFullOrganization().then((org) => { - // TODO: Uncomment this when we have a way to handle consumer organizations better way - //if (org.metadata?.isConsumer === true) { - setOrg(org) - //} else { - // const consumerOrg = orgs?.find((o) => o.metadata?.isConsumer === true) - // if (consumerOrg) { - // setActiveOrg(consumerOrg.slug) - // } - //} - }) + authClient.organization + .getFullOrganization() + .then((org) => { + // TODO: Uncomment this when we have a way to handle consumer organizations better way + //if (org.metadata?.isConsumer === true) { + setOrg(org) + //} else { + // const consumerOrg = orgs?.find((o) => o.metadata?.isConsumer === true) + // if (consumerOrg) { + // setActiveOrg(consumerOrg.slug) + // } + //} + }) + .catch((error) => { + // Silently handle organization fetch failures to prevent unhandled rejections + console.error("Failed to fetch organization:", error) + }) } }, [session?.session.activeOrganizationId, orgs]) @@ -68,7 +74,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { if (pendingMethod) { const now = Date.now() - const ts = pendingTsRaw ? Number.parseInt(pendingTsRaw, 10) : NaN + const ts = pendingTsRaw ? Number.parseInt(pendingTsRaw, 10) : Number.NaN const isFresh = Number.isFinite(ts) && now - ts < 10 * 60 * 1000 // 10 minutes TTL if (isFresh) { diff --git a/packages/lib/posthog.tsx b/packages/lib/posthog.tsx index 62a60e09..0c436e1e 100644 --- a/packages/lib/posthog.tsx +++ b/packages/lib/posthog.tsx @@ -22,7 +22,7 @@ function PostHogPageTracking() { $current_url: url, path: pathname, search_params: searchParams.toString(), - page_type: getPageType(pathname), + page_type: getPageType(), org_slug: getOrgSlug(pathname), } @@ -39,16 +39,21 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) { useEffect(() => { if (typeof window !== "undefined") { const posthogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY - if (posthogKey){ - posthog.init(posthogKey, { - api_host: process.env.NEXT_PUBLIC_BACKEND_URL + "/orange", - ui_host: "https://us.i.posthog.com", - person_profiles: "identified_only", - capture_pageview: false, - capture_pageleave: true, - })} - else{ - console.warn("PostHog API key is not set. PostHog will not be initialized.") + const backendUrl = + process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" + + if (posthogKey) { + posthog.init(posthogKey, { + api_host: `${backendUrl}/orange`, + ui_host: "https://us.i.posthog.com", + person_profiles: "identified_only", + capture_pageview: false, + capture_pageleave: true, + }) + } else { + console.warn( + "PostHog API key is not set. PostHog will not be initialized.", + ) } } }, []) @@ -75,7 +80,7 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) { ) } -function getPageType(pathname: string): string { +function getPageType(): string { return "other" } diff --git a/packages/ui/memory-graph/graph-webgl-canvas.tsx b/packages/ui/memory-graph/graph-webgl-canvas.tsx index 480d1d6b..af13eefc 100644 --- a/packages/ui/memory-graph/graph-webgl-canvas.tsx +++ b/packages/ui/memory-graph/graph-webgl-canvas.tsx @@ -761,6 +761,7 @@ export const GraphWebGLCanvas = memo<GraphCanvasProps>( }} > <Application + preference="webgl" antialias autoDensity backgroundColor={0x0f1419} diff --git a/packages/ui/pages/login.tsx b/packages/ui/pages/login.tsx index 4d823d76..8846a906 100644 --- a/packages/ui/pages/login.tsx +++ b/packages/ui/pages/login.tsx @@ -80,6 +80,27 @@ export function LoginPage({ } catch {} } + function isNetworkError(error: unknown): boolean { + if (!(error instanceof Error)) return false; + const message = error.message.toLowerCase(); + return ( + message.includes("load failed") || + message.includes("networkerror") || + message.includes("failed to fetch") || + message.includes("network request failed") + ); + } + + function getErrorMessage(error: unknown): string { + if (isNetworkError(error)) { + return "Network error. Please check your connection and try again."; + } + if (error instanceof Error) { + return error.message; + } + return "An unexpected error occurred. Please try again."; + } + // If we land back on this page with an error, clear any pending marker useEffect(() => { if (params.get("error")) { @@ -121,13 +142,10 @@ export function LoginPage({ method: "magic_link", error: error instanceof Error ? error.message : "Unknown error", email_domain: email.split("@")[1] || "unknown", + is_network_error: isNetworkError(error), }); - setError( - error instanceof Error - ? error.message - : "Failed to send login link. Please try again.", - ); + setError(getErrorMessage(error)); setIsLoading(false); setIsLoadingEmail(false); return; @@ -321,6 +339,7 @@ export function LoginPage({ onClick={() => { if (isLoading) return; setIsLoading(true); + setError(null); posthog.capture("login_attempt", { method: "social", provider: "google", @@ -331,6 +350,19 @@ export function LoginPage({ callbackURL: getCallbackURL(), provider: "google", }) + .catch((error) => { + console.error("Google login error:", error); + posthog.capture("login_failed", { + method: "social", + provider: "google", + error: + error instanceof Error + ? error.message + : "Unknown error", + is_network_error: isNetworkError(error), + }); + setError(getErrorMessage(error)); + }) .finally(() => { setIsLoading(false); }); @@ -385,6 +417,7 @@ export function LoginPage({ onClick={() => { if (isLoading) return; setIsLoading(true); + setError(null); posthog.capture("login_attempt", { method: "social", provider: "github", @@ -395,6 +428,19 @@ export function LoginPage({ callbackURL: getCallbackURL(), provider: "github", }) + .catch((error) => { + console.error("GitHub login error:", error); + posthog.capture("login_failed", { + method: "social", + provider: "github", + error: + error instanceof Error + ? error.message + : "Unknown error", + is_network_error: isNetworkError(error), + }); + setError(getErrorMessage(error)); + }) .finally(() => { setIsLoading(false); }); |