From 3816666e2d9b5eaa6d8a0d0f0c838ede41a69f44 Mon Sep 17 00:00:00 2001 From: Mahesh Sanikommmu Date: Sat, 23 Aug 2025 00:38:57 -0700 Subject: [PATCH 1/6] ui (memory detail): improved memory detail view and open chat --- apps/web/app/page.tsx | 534 +++++++++--------- apps/web/components/memories/index.tsx | 53 ++ .../web/components/memories/memory-detail.tsx | 375 ++++++++++++ apps/web/components/memory-list-view.tsx | 497 +--------------- apps/web/lib/document-icon.tsx | 54 ++ packages/ui/components/sheet.tsx | 2 +- 6 files changed, 757 insertions(+), 758 deletions(-) create mode 100644 apps/web/components/memories/index.tsx create mode 100644 apps/web/components/memories/memory-detail.tsx create mode 100644 apps/web/lib/document-icon.tsx diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 335d79e3..d6edc122 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -388,284 +388,284 @@ const MemoryGraphPage = () => { }, []); return ( -
- {/* Main content area */} - - - -
- handleViewModeChange("graph")} - transition={{ duration: 0.2 }} - whileHover={{ scale: 1.02 }} - whileTap={{ scale: 0.98 }} - > - {viewMode === "graph" && ( - - )} - - - Graph - - +
+ {/* Main content area */} + + + +
+ handleViewModeChange('graph')} + transition={{ duration: 0.2 }} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > + {viewMode === 'graph' && ( + + )} + + + Graph + + - handleViewModeChange("list")} - transition={{ duration: 0.2 }} - whileHover={{ scale: 1.02 }} - whileTap={{ scale: 0.98 }} - > - {viewMode === "list" && ( - - )} - - - List - - -
-
+ handleViewModeChange('list')} + transition={{ duration: 0.2 }} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > + {viewMode === 'list' && ( + + )} + + + List + + +
+ - {/* Animated content switching */} - - {viewMode === "graph" ? ( - - -
-
-
-

- No Memories to Visualize -

- -
-
-
-
-
- ) : ( - - -
-
-
-

- No Memories to Visualize -

- -
-
-
-
-
- )} -
+ {/* Animated content switching */} + + {viewMode === 'graph' ? ( + + +
+
+
+

+ No Memories to Visualize +

+ +
+
+
+
+
+ ) : ( + + +
+
+
+

+ No Memories to Visualize +

+ +
+
+
+
+
+ )} +
- {/* Top Bar */} -
-
- - - - + {/* Top Bar */} +
+
+ + + + -
- -
+
+ +
- - - -
+ + + +
-
- -
-
+
+ +
+
- {/* Floating Open Chat Button */} - {!isOpen && !isMobile && ( - - - - )} - + {/* Floating Open Chat Button */} + {!isOpen && !isMobile && ( + + + + )} + - {/* Chat panel - positioned absolutely */} - - - - - + {/* Chat panel - positioned absolutely */} + + + + + - {showAddMemoryView && ( - setShowAddMemoryView(false)} - /> - )} + {showAddMemoryView && ( + setShowAddMemoryView(false)} + /> + )} - {/* Tour Alert Dialog */} - + {/* Tour Alert Dialog */} + - {/* Referral/Upgrade Modal */} - setShowReferralModal(false)} - /> -
- ); + {/* Referral/Upgrade Modal */} + setShowReferralModal(false)} + /> +
+ ); }; // Wrapper component to handle auth and waitlist checks diff --git a/apps/web/components/memories/index.tsx b/apps/web/components/memories/index.tsx new file mode 100644 index 00000000..97ef57bd --- /dev/null +++ b/apps/web/components/memories/index.tsx @@ -0,0 +1,53 @@ +import type { DocumentWithMemories } from "@ui/memory-graph/types"; + +export const formatDate = (date: string | Date) => { + const dateObj = new Date(date); + const now = new Date(); + const currentYear = now.getFullYear(); + const dateYear = dateObj.getFullYear(); + + const monthNames = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + const month = monthNames[dateObj.getMonth()]; + const day = dateObj.getDate(); + + const getOrdinalSuffix = (n: number) => { + const s = ["th", "st", "nd", "rd"]; + const v = n % 100; + return n + (s[(v - 20) % 10] || s[v] || s[0]!); + }; + + const formattedDay = getOrdinalSuffix(day); + + if (dateYear !== currentYear) { + return `${month} ${formattedDay}, ${dateYear}`; + } + + return `${month} ${formattedDay}`; +}; + +export const getSourceUrl = (document: DocumentWithMemories) => { + if (document.type === "google_doc" && document.customId) { + return `https://docs.google.com/document/d/${document.customId}`; + } + if (document.type === "google_sheet" && document.customId) { + return `https://docs.google.com/spreadsheets/d/${document.customId}`; + } + if (document.type === "google_slide" && document.customId) { + return `https://docs.google.com/presentation/d/${document.customId}`; + } + // Fallback to existing URL for all other document types + return document.url; +}; \ No newline at end of file diff --git a/apps/web/components/memories/memory-detail.tsx b/apps/web/components/memories/memory-detail.tsx new file mode 100644 index 00000000..eeef6d89 --- /dev/null +++ b/apps/web/components/memories/memory-detail.tsx @@ -0,0 +1,375 @@ +import { getDocumentIcon } from '@/lib/document-icon'; +import { + Drawer, + DrawerContent, + DrawerHeader, + DrawerTitle, +} from '@repo/ui/components/drawer'; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, +} from '@repo/ui/components/sheet'; +import { colors } from '@repo/ui/memory-graph/constants'; +import type { DocumentsWithMemoriesResponseSchema } from '@repo/validation/api'; +import { Badge } from '@ui/components/badge'; +import { Brain, Calendar, ExternalLink, Sparkles } from 'lucide-react'; +import { memo, useState } from 'react'; +import type { z } from 'zod'; +import { formatDate, getSourceUrl } from '.'; +import { Label1Regular } from '@ui/text/label/label-1-regular'; + +type DocumentsResponse = z.infer; +type DocumentWithMemories = DocumentsResponse['documents'][0]; +type MemoryEntry = DocumentWithMemories['memoryEntries'][0]; + +const formatDocumentType = (type: string) => { + // Special case for PDF + if (type.toLowerCase() === 'pdf') return 'PDF'; + + // Replace underscores with spaces and capitalize each word + return type + .split('_') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(' '); +}; + +const MemoryDetailItem = memo(({ memory }: { memory: MemoryEntry }) => { + return ( + + ); +}); + +export const MemoryDetail = memo( + ({ + document, + isOpen, + onClose, + isMobile, + }: { + document: DocumentWithMemories | null; + isOpen: boolean; + onClose: () => void; + isMobile: boolean; + }) => { + if (!document) return null; + + const [isSummaryExpanded, setIsSummaryExpanded] = useState(false); + const activeMemories = document.memoryEntries.filter((m) => !m.isForgotten); + const forgottenMemories = document.memoryEntries.filter( + (m) => m.isForgotten + ); + + const HeaderContent = ({ + TitleComponent, + }: { + TitleComponent: typeof SheetTitle | typeof DrawerTitle; + }) => ( +
+
+
+ {getDocumentIcon(document.type, 'w-5 h-5')} +
+
+ + {document.title || 'Untitled Document'} + +
+ {formatDocumentType(document.type)} + + {formatDate(document.createdAt)} + {document.url && ( + <> + + + + )} +
+
+
+
+ ); + + const SummarySection = () => { + if (!document.summary) return null; + + const shouldShowToggle = document.summary.length > 200; // Show toggle for longer summaries + + return ( +
+

+ {document.content} +

+ {shouldShowToggle && ( + + )} +
+ ); + }; + + const MemoryContent = () => ( +
+ {activeMemories.length > 0 && ( +
+
+ Active Memories ({activeMemories.length}) +
+
+ {activeMemories.map((memory, index) => ( +
+ +
+ ))} +
+
+ )} + + {forgottenMemories.length > 0 && ( +
+
+ Forgotten Memories ({forgottenMemories.length}) +
+
+ {forgottenMemories.map((memory) => ( + + ))} +
+
+ )} + + {activeMemories.length === 0 && forgottenMemories.length === 0 && ( +
+ +

+ No memories found for this document +

+
+ )} +
+ ); + + if (isMobile) { + return ( + + + {/* Header section with glass effect */} +
+ + + + + +
+ +
+ +
+
+
+ ); + } + + return ( + + +
+ + + + + +
+ +
+ +
+
+
+ ); + } +); diff --git a/apps/web/components/memory-list-view.tsx b/apps/web/components/memory-list-view.tsx index 8269562a..2cff96fd 100644 --- a/apps/web/components/memory-list-view.tsx +++ b/apps/web/components/memory-list-view.tsx @@ -2,42 +2,14 @@ import { useIsMobile } from "@hooks/use-mobile"; import { cn } from "@lib/utils"; -import { - GoogleDocs, - GoogleDrive, - GoogleSheets, - GoogleSlides, - MicrosoftExcel, - MicrosoftOneNote, - MicrosoftPowerpoint, - MicrosoftWord, - NotionDoc, - OneDrive, - PDF, -} from "@repo/ui/assets/icons"; import { Badge } from "@repo/ui/components/badge"; import { Card, CardContent, CardHeader } from "@repo/ui/components/card"; -import { - Drawer, - DrawerContent, - DrawerHeader, - DrawerTitle, -} from "@repo/ui/components/drawer"; -import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, -} from "@repo/ui/components/sheet"; import { colors } from "@repo/ui/memory-graph/constants"; import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"; import { useVirtualizer } from "@tanstack/react-virtual"; -import { Label1Regular } from "@ui/text/label/label-1-regular"; import { Brain, - Calendar, ExternalLink, - FileText, Sparkles, } from "lucide-react"; import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; @@ -45,9 +17,12 @@ import type { z } from "zod"; import useResizeObserver from "@/hooks/use-resize-observer"; import { analytics } from "@/lib/analytics"; +import { MemoryDetail } from "./memories/memory-detail"; +import { getDocumentIcon } from "@/lib/document-icon"; +import { formatDate, getSourceUrl } from "./memories"; + type DocumentsResponse = z.infer; type DocumentWithMemories = DocumentsResponse["documents"][0]; -type MemoryEntry = DocumentWithMemories["memoryEntries"][0]; interface MemoryListViewProps { children?: React.ReactNode; @@ -85,222 +60,6 @@ const GreetingMessage = memo(() => { ); }); -const formatDate = (date: string | Date) => { - const dateObj = new Date(date); - const now = new Date(); - const currentYear = now.getFullYear(); - const dateYear = dateObj.getFullYear(); - - const monthNames = [ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", - ]; - const month = monthNames[dateObj.getMonth()]; - const day = dateObj.getDate(); - - const getOrdinalSuffix = (n: number) => { - const s = ["th", "st", "nd", "rd"]; - const v = n % 100; - return n + (s[(v - 20) % 10] || s[v] || s[0]!); - }; - - const formattedDay = getOrdinalSuffix(day); - - if (dateYear !== currentYear) { - return `${month} ${formattedDay}, ${dateYear}`; - } - - return `${month} ${formattedDay}`; -}; - -const formatDocumentType = (type: string) => { - // Special case for PDF - if (type.toLowerCase() === "pdf") return "PDF"; - - // Replace underscores with spaces and capitalize each word - return type - .split("_") - .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) - .join(" "); -}; - -const getDocumentIcon = (type: string, className: string) => { - const iconProps = { - className, - style: { color: colors.text.muted }, - }; - - switch (type) { - case "google_doc": - return ; - case "google_sheet": - return ; - case "google_slide": - return ; - case "google_drive": - return ; - case "notion": - case "notion_doc": - return ; - case "word": - case "microsoft_word": - return ; - case "excel": - case "microsoft_excel": - return ; - case "powerpoint": - case "microsoft_powerpoint": - return ; - case "onenote": - case "microsoft_onenote": - return ; - case "onedrive": - return ; - case "pdf": - return ; - default: - return ; - } -}; - -const getSourceUrl = (document: DocumentWithMemories) => { - if (document.type === "google_doc" && document.customId) { - return `https://docs.google.com/document/d/${document.customId}`; - } - if (document.type === "google_sheet" && document.customId) { - return `https://docs.google.com/spreadsheets/d/${document.customId}`; - } - if (document.type === "google_slide" && document.customId) { - return `https://docs.google.com/presentation/d/${document.customId}`; - } - // Fallback to existing URL for all other document types - return document.url; -}; - -const MemoryDetailItem = memo(({ memory }: { memory: MemoryEntry }) => { - return ( - - ); -}); - const DocumentCard = memo( ({ document, @@ -361,12 +120,12 @@ const DocumentCard = memo( - {document.summary && ( + {document.content && (

- {document.summary} + {document.content}

)}
@@ -402,248 +161,6 @@ const DocumentCard = memo( }, ); -const DocumentDetailSheet = memo( - ({ - document, - isOpen, - onClose, - isMobile, - }: { - document: DocumentWithMemories | null; - isOpen: boolean; - onClose: () => void; - isMobile: boolean; - }) => { - if (!document) return null; - - const [isSummaryExpanded, setIsSummaryExpanded] = useState(false); - const activeMemories = document.memoryEntries.filter((m) => !m.isForgotten); - const forgottenMemories = document.memoryEntries.filter( - (m) => m.isForgotten, - ); - - const HeaderContent = ({ - TitleComponent, - }: { - TitleComponent: typeof SheetTitle | typeof DrawerTitle; - }) => ( -
-
-
- {getDocumentIcon(document.type, "w-5 h-5")} -
-
- - {document.title || "Untitled Document"} - -
- {formatDocumentType(document.type)} - - {formatDate(document.createdAt)} - {document.url && ( - <> - - - - )} -
-
-
-
- ); - - const SummarySection = () => { - if (!document.summary) return null; - - const shouldShowToggle = document.summary.length > 200; // Show toggle for longer summaries - - return ( -
-

- {document.summary} -

- {shouldShowToggle && ( - - )} -
- ); - }; - - const MemoryContent = () => ( -
- {activeMemories.length > 0 && ( -
-
- - Active Memories ({activeMemories.length}) -
-
- {activeMemories.map((memory, index) => ( -
- -
- ))} -
-
- )} - - {forgottenMemories.length > 0 && ( -
-
- Forgotten Memories ({forgottenMemories.length}) -
-
- {forgottenMemories.map((memory) => ( - - ))} -
-
- )} - - {activeMemories.length === 0 && forgottenMemories.length === 0 && ( -
- -

- No memories found for this document -

-
- )} -
- ); - - if (isMobile) { - return ( - - - {/* Header section with glass effect */} -
- - - - - -
- -
- -
-
-
- ); - } - - return ( - - - {/* Header section with glass effect */} -
- - - - - -
- -
- -
-
-
- ); - }, -); - export const MemoryListView = ({ children, documents, @@ -831,7 +348,7 @@ export const MemoryListView = ({ )}
- { + const iconProps = { + className, + style: { color: colors.text.muted }, + }; + + switch (type) { + case 'google_doc': + return ; + case 'google_sheet': + return ; + case 'google_slide': + return ; + case 'google_drive': + return ; + case 'notion': + case 'notion_doc': + return ; + case 'word': + case 'microsoft_word': + return ; + case 'excel': + case 'microsoft_excel': + return ; + case 'powerpoint': + case 'microsoft_powerpoint': + return ; + case 'onenote': + case 'microsoft_onenote': + return ; + case 'onedrive': + return ; + case 'pdf': + return ; + default: + return ; + } +}; diff --git a/packages/ui/components/sheet.tsx b/packages/ui/components/sheet.tsx index 242a4688..fc49af38 100644 --- a/packages/ui/components/sheet.tsx +++ b/packages/ui/components/sheet.tsx @@ -83,7 +83,7 @@ function SheetContent({ function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { return (
From 51e17bfc1710ff0773627e6a52a6a9f702d01272 Mon Sep 17 00:00:00 2001 From: Mahesh Sanikommmu Date: Sun, 24 Aug 2025 10:48:01 -0700 Subject: [PATCH 2/6] added summary and memory details to memory detail view --- .../web/components/memories/memory-detail.tsx | 102 ++++++++++++------ 1 file changed, 71 insertions(+), 31 deletions(-) diff --git a/apps/web/components/memories/memory-detail.tsx b/apps/web/components/memories/memory-detail.tsx index eeef6d89..dad2a8a3 100644 --- a/apps/web/components/memories/memory-detail.tsx +++ b/apps/web/components/memories/memory-detail.tsx @@ -11,11 +11,17 @@ import { SheetHeader, SheetTitle, } from '@repo/ui/components/sheet'; +import { + Tabs, + TabsList, + TabsTrigger, + TabsContent, +} from '@repo/ui/components/tabs'; import { colors } from '@repo/ui/memory-graph/constants'; import type { DocumentsWithMemoriesResponseSchema } from '@repo/validation/api'; import { Badge } from '@ui/components/badge'; -import { Brain, Calendar, ExternalLink, Sparkles } from 'lucide-react'; -import { memo, useState } from 'react'; +import { Brain, Calendar, CircleUserRound, ExternalLink, List, Sparkles } from 'lucide-react'; +import { memo } from 'react'; import type { z } from 'zod'; import { formatDate, getSourceUrl } from '.'; import { Label1Regular } from '@ui/text/label/label-1-regular'; @@ -159,7 +165,6 @@ export const MemoryDetail = memo( }) => { if (!document) return null; - const [isSummaryExpanded, setIsSummaryExpanded] = useState(false); const activeMemories = document.memoryEntries.filter((m) => !m.isForgotten); const forgottenMemories = document.memoryEntries.filter( (m) => m.isForgotten @@ -214,35 +219,70 @@ export const MemoryDetail = memo(
); - const SummarySection = () => { - if (!document.summary) return null; + const ContentAndSummarySection = () => { + const hasContent = document.content && document.content.trim().length > 0; + const hasSummary = document.summary && document.summary.trim().length > 0; + + if (!hasContent && !hasSummary) return null; - const shouldShowToggle = document.summary.length > 200; // Show toggle for longer summaries + const defaultTab = hasContent ? 'content' : 'summary'; return ( -
-

- {document.content} -

- {shouldShowToggle && ( - - )} + {hasContent && ( + + + Original Content + + )} + {hasSummary && ( + + + Summary + + )} + + + {hasContent && ( + +
+

+ {document.content} +

+
+
+ )} + + {hasSummary && ( + +
+

+ {document.summary} +

+
+
+ )} +
); }; @@ -260,7 +300,7 @@ export const MemoryDetail = memo( Active Memories ({activeMemories.length})
- {activeMemories.map((memory, index) => ( + {activeMemories.map((memory) => (
@@ -333,7 +373,7 @@ export const MemoryDetail = memo( - +
@@ -362,7 +402,7 @@ export const MemoryDetail = memo( - +
From 8da19c977eefbfae280e912df507427083c28b0a Mon Sep 17 00:00:00 2001 From: Alex Foster <122472971+alexf37@users.noreply.github.com> Date: Mon, 25 Aug 2025 17:37:14 -0400 Subject: [PATCH 3/6] feat: add 'last used' badge to login page (#387) --- packages/lib/auth-context.tsx | 34 ++++ packages/ui/pages/login.tsx | 361 +++++++++++++++++++--------------- 2 files changed, 241 insertions(+), 154 deletions(-) diff --git a/packages/lib/auth-context.tsx b/packages/lib/auth-context.tsx index 5b2d58bc..66ff84bc 100644 --- a/packages/lib/auth-context.tsx +++ b/packages/lib/auth-context.tsx @@ -33,6 +33,40 @@ export function AuthProvider({ children }: { children: ReactNode }) { } }, [session?.session.activeOrganizationId]) + // When a session exists and there is a pending login method recorded, + // promote it to the last-used method (successful login) and clear pending. + useEffect(() => { + if (typeof window === "undefined") return + if (!session?.session) return + + try { + const pendingMethod = localStorage.getItem( + "supermemory-pending-login-method", + ) + const pendingTsRaw = localStorage.getItem( + "supermemory-pending-login-timestamp", + ) + + if (pendingMethod) { + const now = Date.now() + const ts = pendingTsRaw ? Number.parseInt(pendingTsRaw, 10) : NaN + const isFresh = Number.isFinite(ts) && now - ts < 10 * 60 * 1000 // 10 minutes TTL + + if (isFresh) { + localStorage.setItem( + "supermemory-last-login-method", + pendingMethod, + ) + } + } + } catch { } + // Always clear pending markers once a session is present + try { + localStorage.removeItem("supermemory-pending-login-method") + localStorage.removeItem("supermemory-pending-login-timestamp") + } catch { } + }, [session?.session]) + const setActiveOrg = async (slug: string) => { if (!slug) return diff --git a/packages/ui/pages/login.tsx b/packages/ui/pages/login.tsx index c14ba4ea..fcd48eae 100644 --- a/packages/ui/pages/login.tsx +++ b/packages/ui/pages/login.tsx @@ -1,25 +1,26 @@ -"use client"; +"use client" -import { signIn } from "@lib/auth"; -import { usePostHog } from "@lib/posthog"; -import { LogoFull } from "@repo/ui/assets/Logo"; -import { TextSeparator } from "@repo/ui/components/text-separator"; -import { ExternalAuthButton } from "@ui/button/external-auth"; -import { Button } from "@ui/components/button"; +import { signIn } from "@lib/auth" +import { usePostHog } from "@lib/posthog" +import { LogoFull } from "@repo/ui/assets/Logo" +import { TextSeparator } from "@repo/ui/components/text-separator" +import { ExternalAuthButton } from "@ui/button/external-auth" +import { Button } from "@ui/components/button" +import { Badge } from "@ui/components/badge" import { Carousel, CarouselContent, CarouselItem, -} from "@ui/components/carousel"; -import { LabeledInput } from "@ui/input/labeled-input"; -import { HeadingH1Medium } from "@ui/text/heading/heading-h1-medium"; -import { HeadingH3Medium } from "@ui/text/heading/heading-h3-medium"; -import { Label1Regular } from "@ui/text/label/label-1-regular"; -import { Title1Bold } from "@ui/text/title/title-1-bold"; -import Autoplay from "embla-carousel-autoplay"; -import Image from "next/image"; -import { useRouter, useSearchParams } from "next/navigation"; -import { useState } from "react"; +} from "@ui/components/carousel" +import { LabeledInput } from "@ui/input/labeled-input" +import { HeadingH1Medium } from "@ui/text/heading/heading-h1-medium" +import { HeadingH3Medium } from "@ui/text/heading/heading-h3-medium" +import { Label1Regular } from "@ui/text/label/label-1-regular" +import { Title1Bold } from "@ui/text/title/title-1-bold" +import Autoplay from "embla-carousel-autoplay" +import Image from "next/image" +import { useRouter, useSearchParams } from "next/navigation" +import { useState, useEffect } from "react" export function LoginPage({ heroText = "The unified memory API for the AI era.", @@ -28,74 +29,101 @@ export function LoginPage({ "Trusted by Open Source, enterprise and developers.", ], }) { - const [email, setEmail] = useState(""); - const [submittedEmail, setSubmittedEmail] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [isLoadingEmail, setIsLoadingEmail] = useState(false); - const [error, setError] = useState(null); - const router = useRouter(); + const [email, setEmail] = useState("") + const [submittedEmail, setSubmittedEmail] = useState(null) + const [isLoading, setIsLoading] = useState(false) + const [isLoadingEmail, setIsLoadingEmail] = useState(false) + const [error, setError] = useState(null) + const [lastUsedMethod, setLastUsedMethod] = useState(null) + const router = useRouter() + + const posthog = usePostHog() + + const params = useSearchParams() + + // Load last used method from localStorage on mount + useEffect(() => { + const savedMethod = localStorage.getItem('supermemory-last-login-method') + setLastUsedMethod(savedMethod) + }, []) + + // Record the pending login method (will be committed after successful auth) + function setPendingLoginMethod(method: string) { + try { + localStorage.setItem('supermemory-pending-login-method', method) + localStorage.setItem('supermemory-pending-login-timestamp', String(Date.now())) + } catch { } + } + + // If we land back on this page with an error, clear any pending marker + useEffect(() => { + if (params.get("error")) { + try { + localStorage.removeItem('supermemory-pending-login-method') + localStorage.removeItem('supermemory-pending-login-timestamp') + } catch { } + } + }, [params]) - const posthog = usePostHog(); - const params = useSearchParams(); const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setIsLoading(true); - setIsLoadingEmail(true); - setError(null); + e.preventDefault() + setIsLoading(true) + setIsLoadingEmail(true) + setError(null) // Track login attempt posthog.capture("login_attempt", { method: "magic_link", email_domain: email.split("@")[1] || "unknown", - }); + }) try { await signIn.magicLink({ callbackURL: window.location.origin, email, - }); - setSubmittedEmail(email); - + }) + setSubmittedEmail(email) + setPendingLoginMethod('magic_link') // Track successful magic link send posthog.capture("login_magic_link_sent", { email_domain: email.split("@")[1] || "unknown", - }); + }) } catch (error) { - console.error(error); + console.error(error) // Track login failure posthog.capture("login_failed", { method: "magic_link", error: error instanceof Error ? error.message : "Unknown error", email_domain: email.split("@")[1] || "unknown", - }); + }) setError( error instanceof Error ? error.message : "Failed to send login link. Please try again.", - ); - setIsLoading(false); - setIsLoadingEmail(false); - return; + ) + setIsLoading(false) + setIsLoadingEmail(false) + return } - setIsLoading(false); - setIsLoadingEmail(false); - }; + setIsLoading(false) + setIsLoadingEmail(false) + } const handleSubmitToken = async (event: React.FormEvent) => { - event.preventDefault(); - setIsLoading(true); + event.preventDefault() + setIsLoading(true) - const formData = new FormData(event.currentTarget); - const token = formData.get("token") as string; + const formData = new FormData(event.currentTarget) + const token = formData.get("token") as string router.push( `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/auth/magic-link/verify?token=${token}&callbackURL=${encodeURIComponent(window.location.host)}`, - ); - }; + ) + } return (
@@ -197,8 +225,8 @@ export function LoginPage({ disabled: isLoading, id: "email", onChange: (e) => { - setEmail(e.target.value); - error && setError(null); + setEmail(e.target.value) + error && setError(null) }, required: true, value: email, @@ -207,124 +235,149 @@ export function LoginPage({ label="Email" /> - +
+ + {lastUsedMethod === 'magic_link' && ( +
+ Last used +
+ )} +
{process.env.NEXT_PUBLIC_HOST_ID === "supermemory" || - !process.env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED || - !process.env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED ? ( + !process.env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED || + !process.env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED ? ( ) : null}
{process.env.NEXT_PUBLIC_HOST_ID === "supermemory" || - !process.env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED ? ( - - Google - - - - - - } - authProvider="Google" - disabled={isLoading} - onClick={() => { - if (isLoading) return; - setIsLoading(true); - posthog.capture("login_attempt", { - method: "social", - provider: "google", - }); - signIn - .social({ - callbackURL: window.location.origin, + !process.env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED ? ( +
+ + Google + + + + + + } + authProvider="Google" + className="w-full" + disabled={isLoading} + onClick={() => { + if (isLoading) return + setIsLoading(true) + posthog.capture("login_attempt", { + method: "social", provider: "google", }) - .finally(() => { - setIsLoading(false); - }); - }} - /> + setPendingLoginMethod('google') + signIn + .social({ + callbackURL: window.location.origin, + provider: "google", + }) + .finally(() => { + setIsLoading(false) + }) + }} + /> + {lastUsedMethod === 'google' && ( +
+ Last used +
+ )} +
) : null} {process.env.NEXT_PUBLIC_HOST_ID === "supermemory" || - !process.env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED ? ( - - Github - - - - - - + + Github + + - - - - } - authProvider="Github" - disabled={isLoading} - onClick={() => { - if (isLoading) return; - setIsLoading(true); - posthog.capture("login_attempt", { - method: "social", - provider: "github", - }); - signIn - .social({ - callbackURL: window.location.origin, + + + + + + + + } + authProvider="Github" + className="w-full" + disabled={isLoading} + onClick={() => { + if (isLoading) return + setIsLoading(true) + posthog.capture("login_attempt", { + method: "social", provider: "github", }) - .finally(() => { - setIsLoading(false); - }); - }} - /> + setPendingLoginMethod('github') + signIn + .social({ + callbackURL: window.location.origin, + provider: "github", + }) + .finally(() => { + setIsLoading(false) + }) + }} + /> + {lastUsedMethod === 'github' && ( +
+ Last used +
+ )} +
) : null}
@@ -350,5 +403,5 @@ export function LoginPage({ )} - ); -} + ) +} \ No newline at end of file From 3760558bf2a7a9a57eb200e5500e3f0aa06edb1a Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Mon, 25 Aug 2025 21:41:53 +0000 Subject: [PATCH 4/6] fix sentry server issue (#388) --- apps/web/instrumentation.ts | 13 ------------- apps/web/sentry.edge.config.ts | 19 ------------------- apps/web/sentry.server.config.ts | 18 ------------------ 3 files changed, 50 deletions(-) delete mode 100644 apps/web/instrumentation.ts delete mode 100644 apps/web/sentry.edge.config.ts delete mode 100644 apps/web/sentry.server.config.ts diff --git a/apps/web/instrumentation.ts b/apps/web/instrumentation.ts deleted file mode 100644 index 964f937c..00000000 --- a/apps/web/instrumentation.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as Sentry from '@sentry/nextjs'; - -export async function register() { - if (process.env.NEXT_RUNTIME === 'nodejs') { - await import('./sentry.server.config'); - } - - if (process.env.NEXT_RUNTIME === 'edge') { - await import('./sentry.edge.config'); - } -} - -export const onRequestError = Sentry.captureRequestError; diff --git a/apps/web/sentry.edge.config.ts b/apps/web/sentry.edge.config.ts deleted file mode 100644 index cff5a86d..00000000 --- a/apps/web/sentry.edge.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on). -// The config you add here will be used whenever one of the edge features is loaded. -// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally. -// https://docs.sentry.io/platforms/javascript/guides/nextjs/ - -import * as Sentry from "@sentry/nextjs"; - -Sentry.init({ - dsn: "https://2451ebfd1a7490f05fa7776482df81b6@o4508385422802944.ingest.us.sentry.io/4509872269819904", - - // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. - tracesSampleRate: 1, - - // Enable logs to be sent to Sentry - enableLogs: true, - - // Setting this option to true will print useful information to the console while you're setting up Sentry. - debug: false, -}); diff --git a/apps/web/sentry.server.config.ts b/apps/web/sentry.server.config.ts deleted file mode 100644 index 2cd5afbe..00000000 --- a/apps/web/sentry.server.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -// This file configures the initialization of Sentry on the server. -// The config you add here will be used whenever the server handles a request. -// https://docs.sentry.io/platforms/javascript/guides/nextjs/ - -import * as Sentry from "@sentry/nextjs"; - -Sentry.init({ - dsn: "https://2451ebfd1a7490f05fa7776482df81b6@o4508385422802944.ingest.us.sentry.io/4509872269819904", - - // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. - tracesSampleRate: 1, - - // Enable logs to be sent to Sentry - enableLogs: true, - - // Setting this option to true will print useful information to the console while you're setting up Sentry. - debug: false, -}); From 29e1ebdc816fb911aa19ef5fd0d8aa1c3474ad36 Mon Sep 17 00:00:00 2001 From: alexf37 <122472971+alexf37@users.noreply.github.com> Date: Tue, 26 Aug 2025 21:50:20 +0000 Subject: [PATCH 5/6] fix: missing dialog title in consumer mobile drawer (#386) --- apps/web/components/menu.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/web/components/menu.tsx b/apps/web/components/menu.tsx index 0501603f..8a2216cc 100644 --- a/apps/web/components/menu.tsx +++ b/apps/web/components/menu.tsx @@ -460,6 +460,12 @@ function Menu({ id }: { id?: string }) { + + {expandedView === "addUrl" && "Add Memory"} + {expandedView === "mcp" && "Model Context Protocol"} + {expandedView === "profile" && "Profile"} + {!expandedView && "Menu"} +
{/* Glass effect background */}
From a3955b55cb019c87498f98ee29c18fdd7ac51e9d Mon Sep 17 00:00:00 2001 From: alexf37 <122472971+alexf37@users.noreply.github.com> Date: Tue, 26 Aug 2025 21:51:39 +0000 Subject: [PATCH 6/6] fix: memory limits cutoff (#385) Previously, the badge with the memories limit in the consumer app menu was cut off on desktop, and now with this PR, it no longer is. --- apps/web/components/menu.tsx | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/apps/web/components/menu.tsx b/apps/web/components/menu.tsx index 8a2216cc..9b9abd81 100644 --- a/apps/web/components/menu.tsx +++ b/apps/web/components/menu.tsx @@ -151,7 +151,7 @@ function Menu({ id }: { id?: string }) { }, [isMobile, isMobileMenuOpen, isHovered, expandedView, setMenuExpanded]); // Calculate width based on state - const menuWidth = expandedView || isCollapsing ? 600 : isHovered ? 160 : 56; + const menuWidth = expandedView || isCollapsing ? 600 : isHovered ? 220 : 56; // Dynamic z-index for mobile based on active panel const mobileZIndex = @@ -275,26 +275,33 @@ function Menu({ id }: { id?: string }) { opacity: isHovered ? 1 : 0, x: isHovered ? 0 : -10, }} - className="drop-shadow-lg absolute left-10 whitespace-nowrap flex items-center gap-2" + className="drop-shadow-lg absolute left-10 right-16 whitespace-nowrap" initial={{ opacity: 0, x: -10 }} style={{ transform: "translateZ(0)", }} transition={{ - duration: 0.3, - delay: index * 0.03, + duration: isHovered ? 0.2 : 0.1, + delay: isHovered ? index * 0.03 : 0, ease: [0.4, 0, 0.2, 1], }} > {item.text} - {/* Show warning indicator for Add Memory when limits approached */} - {shouldShowLimitWarning && - item.key === "addUrl" && ( - - {memoriesLimit - memoriesUsed} left - - )} + {shouldShowLimitWarning && item.key === "addUrl" && ( + + {memoriesLimit - memoriesUsed} left + + )} {index === 0 && (