aboutsummaryrefslogtreecommitdiff
path: root/apps
diff options
context:
space:
mode:
Diffstat (limited to 'apps')
-rw-r--r--apps/web/app/(navigation)/chat/[id]/page.tsx39
-rw-r--r--apps/web/app/(navigation)/layout.tsx57
-rw-r--r--apps/web/app/(navigation)/page.tsx81
-rw-r--r--apps/web/app/(navigation)/settings/billing/page.tsx12
-rw-r--r--apps/web/app/(navigation)/settings/integrations/page.tsx10
-rw-r--r--apps/web/app/(navigation)/settings/layout.tsx52
-rw-r--r--apps/web/app/(navigation)/settings/page.tsx12
-rw-r--r--apps/web/app/(navigation)/settings/support/page.tsx125
-rw-r--r--apps/web/app/layout.tsx74
-rw-r--r--apps/web/app/onboarding/animated-text.tsx109
-rw-r--r--apps/web/app/onboarding/connections-form.tsx213
-rw-r--r--apps/web/app/onboarding/extension-form.tsx6
-rw-r--r--apps/web/app/onboarding/floating-orbs.tsx9
-rw-r--r--apps/web/app/onboarding/intro.tsx191
-rw-r--r--apps/web/app/onboarding/name-form.tsx1
-rw-r--r--apps/web/app/onboarding/onboarding-context.tsx2
-rw-r--r--apps/web/app/onboarding/welcome.tsx2
-rw-r--r--apps/web/app/page.tsx622
-rw-r--r--apps/web/button.tsx58
-rw-r--r--apps/web/components/chat-input.tsx80
-rw-r--r--apps/web/components/connect-ai-modal.tsx98
-rw-r--r--apps/web/components/content-cards/google-docs.tsx164
-rw-r--r--apps/web/components/content-cards/note.tsx133
-rw-r--r--apps/web/components/content-cards/tweet.tsx105
-rw-r--r--apps/web/components/content-cards/website.tsx103
-rw-r--r--apps/web/components/header.tsx175
-rw-r--r--apps/web/components/masonry-memory-list.tsx269
-rw-r--r--apps/web/components/memories-utils/html-content-renderer.tsx62
-rw-r--r--apps/web/components/memories-utils/index.tsx109
-rw-r--r--apps/web/components/memories-utils/memory-detail.tsx385
-rw-r--r--apps/web/components/memories.tsx276
-rw-r--r--apps/web/components/memories/index.tsx53
-rw-r--r--apps/web/components/memories/memory-detail.tsx415
-rw-r--r--apps/web/components/memory-list-view.tsx40
-rw-r--r--apps/web/components/menu.tsx27
-rw-r--r--apps/web/components/project-selector.tsx254
-rw-r--r--apps/web/components/referral-upgrade-modal.tsx304
-rw-r--r--apps/web/components/text-shimmer.tsx2
-rw-r--r--apps/web/components/views/add-memory/action-buttons.tsx4
-rw-r--r--apps/web/components/views/add-memory/index.tsx917
-rw-r--r--apps/web/components/views/add-memory/project-selection.tsx10
-rw-r--r--apps/web/components/views/add-memory/text-editor.tsx16
-rw-r--r--apps/web/components/views/billing.tsx200
-rw-r--r--apps/web/components/views/chat/chat-messages.tsx414
-rw-r--r--apps/web/components/views/connections-tab-content.tsx42
-rw-r--r--apps/web/components/views/integrations.tsx139
-rw-r--r--apps/web/components/views/profile.tsx444
-rw-r--r--apps/web/globals.css9
-rw-r--r--apps/web/lib/document-icon.tsx7
-rw-r--r--apps/web/lib/theme-provider.tsx8
-rw-r--r--apps/web/package.json4
-rw-r--r--apps/web/stores/chat.ts48
-rw-r--r--apps/web/stores/index.ts77
53 files changed, 3795 insertions, 3273 deletions
diff --git a/apps/web/app/(navigation)/chat/[id]/page.tsx b/apps/web/app/(navigation)/chat/[id]/page.tsx
new file mode 100644
index 00000000..9bb2038b
--- /dev/null
+++ b/apps/web/app/(navigation)/chat/[id]/page.tsx
@@ -0,0 +1,39 @@
+"use client"
+
+import { useEffect } from "react"
+import { useParams } from "next/navigation"
+import { usePersistentChat } from "@/stores"
+import { ChatMessages } from "@/components/views/chat/chat-messages"
+
+export default function ChatPage() {
+ const params = useParams()
+ const { setCurrentChatId, getCurrentChat } = usePersistentChat()
+
+ const chatId = params.id as string
+
+ useEffect(() => {
+ if (chatId) {
+ setCurrentChatId(chatId)
+ }
+ }, [chatId, setCurrentChatId])
+
+ const currentChat = getCurrentChat()
+
+ return (
+ <div className="flex flex-col w-full">
+ <div className="flex flex-col h-[93vh]">
+ <div className="flex justify-center w-full">
+ <h1 className="text-lg font-semibold flex-1 truncate text-center">
+ {currentChat?.title || "New Chat"}
+ </h1>
+ </div>
+
+ <div className="flex-1 flex justify-center min-h-0 w-full px-4">
+ <div className="flex flex-col min-h-0 w-full max-w-4xl">
+ <ChatMessages />
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+}
diff --git a/apps/web/app/(navigation)/layout.tsx b/apps/web/app/(navigation)/layout.tsx
new file mode 100644
index 00000000..da5a87fb
--- /dev/null
+++ b/apps/web/app/(navigation)/layout.tsx
@@ -0,0 +1,57 @@
+"use client"
+
+import { Header } from "@/components/header"
+import { AddMemoryView } from "@/components/views/add-memory"
+import { useEffect, useState } from "react"
+
+export default function NavigationLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ const [showAddMemoryView, setShowAddMemoryView] = useState(false)
+ useEffect(() => {
+ const handleKeydown = (event: KeyboardEvent) => {
+ const target = event.target as HTMLElement
+ const isInputField =
+ target.tagName === "INPUT" ||
+ target.tagName === "TEXTAREA" ||
+ target.isContentEditable ||
+ target.closest('[contenteditable="true"]')
+
+ if (isInputField) return
+
+ // add memory shortcut
+ if (
+ event.key === "c" &&
+ !event.ctrlKey &&
+ !event.metaKey &&
+ !event.altKey &&
+ !event.shiftKey
+ ) {
+ event.preventDefault()
+ setShowAddMemoryView(true)
+ }
+ }
+
+ document.addEventListener("keydown", handleKeydown)
+
+ return () => {
+ document.removeEventListener("keydown", handleKeydown)
+ }
+ }, [])
+ return (
+ <div className="relative min-h-screen">
+ <div className="sticky top-0 z-50 bg-background/80 backdrop-blur-md border-b border-white/10">
+ <Header onAddMemory={() => setShowAddMemoryView(true)} />
+ </div>
+ {children}
+ {showAddMemoryView && (
+ <AddMemoryView
+ initialTab="note"
+ onClose={() => setShowAddMemoryView(false)}
+ />
+ )}
+ </div>
+ )
+}
diff --git a/apps/web/app/(navigation)/page.tsx b/apps/web/app/(navigation)/page.tsx
new file mode 100644
index 00000000..b8c81985
--- /dev/null
+++ b/apps/web/app/(navigation)/page.tsx
@@ -0,0 +1,81 @@
+"use client"
+
+import { useOnboardingStorage } from "@hooks/use-onboarding-storage"
+import { useAuth } from "@lib/auth-context"
+import { ChevronsDown, LoaderIcon } from "lucide-react"
+import { useRouter } from "next/navigation"
+import { useEffect } from "react"
+import { InstallPrompt } from "@/components/install-prompt"
+import { ChatInput } from "@/components/chat-input"
+import { BackgroundPlus } from "@ui/components/grid-plus"
+import { Memories } from "@/components/memories"
+
+export default function Page() {
+ const { user, session } = useAuth()
+ const { shouldShowOnboarding, isLoading: onboardingLoading } =
+ useOnboardingStorage()
+ const router = useRouter()
+
+ useEffect(() => {
+ const url = new URL(window.location.href)
+ const authenticateChromeExtension = url.searchParams.get(
+ "extension-auth-success",
+ )
+
+ if (authenticateChromeExtension) {
+ const sessionToken = session?.token
+ const userData = {
+ email: user?.email,
+ name: user?.name,
+ userId: user?.id,
+ }
+
+ if (sessionToken && userData?.email) {
+ const encodedToken = encodeURIComponent(sessionToken)
+ window.postMessage({ token: encodedToken, userData }, "*")
+ url.searchParams.delete("extension-auth-success")
+ window.history.replaceState({}, "", url.toString())
+ }
+ }
+ }, [user, session])
+
+ useEffect(() => {
+ if (user && !onboardingLoading && shouldShowOnboarding()) {
+ router.push("/onboarding")
+ }
+ }, [user, shouldShowOnboarding, onboardingLoading, router])
+
+ if (!user || onboardingLoading) {
+ return (
+ <div className="min-h-screen flex items-center justify-center bg-[#0f1419]">
+ <div className="flex flex-col items-center gap-4">
+ <LoaderIcon className="w-8 h-8 text-orange-500 animate-spin" />
+ <p className="text-white/60">Loading...</p>
+ </div>
+ </div>
+ )
+ }
+
+ if (shouldShowOnboarding()) {
+ return null
+ }
+
+ return (
+ <div>
+ <div className="flex flex-col h-[80vh] rounded-lg overflow-hidden relative">
+ <BackgroundPlus />
+ <div className="p-4 flex-1 flex items-center justify-center">
+ <ChatInput />
+ </div>
+
+ <div className="flex items-center gap-2 text-xs text-muted-foreground justify-center py-2 opacity-75">
+ <ChevronsDown className="size-4" />
+ <p>Scroll down to see memories</p>
+ </div>
+ </div>
+ <Memories />
+
+ <InstallPrompt />
+ </div>
+ )
+}
diff --git a/apps/web/app/(navigation)/settings/billing/page.tsx b/apps/web/app/(navigation)/settings/billing/page.tsx
new file mode 100644
index 00000000..2b8e6ba0
--- /dev/null
+++ b/apps/web/app/(navigation)/settings/billing/page.tsx
@@ -0,0 +1,12 @@
+"use client"
+import { BillingView } from "@/components/views/billing"
+export default function BillingPage() {
+ return (
+ <div className="py-6 max-w-2xl">
+ <h1 className="text-2xl font-bold text-foreground mb-6">
+ Billing & Subscription
+ </h1>
+ <BillingView />
+ </div>
+ )
+} \ No newline at end of file
diff --git a/apps/web/app/(navigation)/settings/integrations/page.tsx b/apps/web/app/(navigation)/settings/integrations/page.tsx
new file mode 100644
index 00000000..7fedd143
--- /dev/null
+++ b/apps/web/app/(navigation)/settings/integrations/page.tsx
@@ -0,0 +1,10 @@
+"use client"
+import { IntegrationsView } from "@/components/views/integrations"
+export default function IntegrationsPage() {
+ return (
+ <div className="py-6 max-w-4xl">
+ <h1 className="text-2xl font-bold text-foreground mb-6">Integrations</h1>
+ <IntegrationsView />
+ </div>
+ )
+} \ No newline at end of file
diff --git a/apps/web/app/(navigation)/settings/layout.tsx b/apps/web/app/(navigation)/settings/layout.tsx
new file mode 100644
index 00000000..342e640c
--- /dev/null
+++ b/apps/web/app/(navigation)/settings/layout.tsx
@@ -0,0 +1,52 @@
+"use client"
+
+import { Button } from "@ui/components/button"
+import { useRouter, usePathname } from "next/navigation"
+import { cn } from "@repo/lib/utils"
+
+export default function SettingsPageLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ const router = useRouter()
+ const pathname = usePathname()
+
+ const navItems = [
+ { label: "Profile", path: "/settings" },
+ { label: "Integrations", path: "/settings/integrations" },
+ { label: "Billing", path: "/settings/billing" },
+ { label: "Support", path: "/settings/support" },
+ ]
+
+ return (
+ <div className="flex-1 overflow-hidden max-w-screen-lg mx-auto mt-4">
+ <div className="flex flex-col items-center">
+ <div className="w-full max-w-2xl">
+ <nav className="flex gap-[2px] px-1 py-1 text-sm rounded-[8px] bg-muted-foreground/10 text-foreground max-w-fit">
+ {navItems.map((item) => {
+ const isActive = pathname === item.path
+ return (
+ <Button
+ key={item.path}
+ onClick={() => router.push(item.path)}
+ variant="settingsNav"
+ size="sm"
+ className={cn(
+ "transition-all duration-200",
+ isActive
+ ? "opacity-100 bg-card"
+ : "opacity-60 hover:opacity-100 hover:bg-card ",
+ )}
+ >
+ {item.label}
+ </Button>
+ )
+ })}
+ </nav>
+ {children}
+ </div>
+ </div>
+ </div>
+ )
+}
diff --git a/apps/web/app/(navigation)/settings/page.tsx b/apps/web/app/(navigation)/settings/page.tsx
new file mode 100644
index 00000000..c9046fba
--- /dev/null
+++ b/apps/web/app/(navigation)/settings/page.tsx
@@ -0,0 +1,12 @@
+"use client"
+import { ProfileView } from "@/components/views/profile"
+export default function ProfilePage() {
+ return (
+ <div className="py-6 max-w-xl">
+ <h1 className="text-2xl font-bold text-foreground mb-2">
+ Profile Settings
+ </h1>
+ <ProfileView />
+ </div>
+ )
+} \ No newline at end of file
diff --git a/apps/web/app/(navigation)/settings/support/page.tsx b/apps/web/app/(navigation)/settings/support/page.tsx
new file mode 100644
index 00000000..de4b3c78
--- /dev/null
+++ b/apps/web/app/(navigation)/settings/support/page.tsx
@@ -0,0 +1,125 @@
+"use client"
+
+import { Button } from "@repo/ui/components/button"
+import { HeadingH3Bold } from "@repo/ui/text/heading/heading-h3-bold"
+import { ExternalLink, Mail, MessageCircle } from "lucide-react"
+
+export default function SupportPage() {
+ return (
+ <div className="py-6 max-w-2xl">
+ <h1 className="text-2xl font-bold text-foreground mb-6">
+ Support & Help
+ </h1>
+
+ <div className="space-y-6">
+ {/* Contact Options */}
+ <div className="bg-card border border-border rounded-lg p-6 space-y-4">
+ <HeadingH3Bold className="text-foreground">Get Help</HeadingH3Bold>
+ <p className="text-muted-foreground text-sm">
+ Need assistance? We're here to help! Choose the best way to reach
+ us.
+ </p>
+
+ <div className="space-y-3">
+ <Button
+ className="w-full justify-start bg-blue-500/20 hover:bg-blue-500/30 text-blue-600 dark:text-blue-400 border-blue-500/30"
+ onClick={() =>
+ window.open("https://x.com/supermemoryai", "_blank")
+ }
+ variant="outline"
+ >
+ <MessageCircle className="w-4 h-4 mr-2" />
+ Message us on X (Twitter)
+ <ExternalLink className="w-4 h-4 ml-auto" />
+ </Button>
+
+ <Button
+ className="w-full justify-start bg-green-500/20 hover:bg-green-500/30 text-green-600 dark:text-green-400 border-green-500/30"
+ onClick={() =>
+ window.open("mailto:[email protected]", "_blank")
+ }
+ variant="outline"
+ >
+ <Mail className="w-4 h-4 mr-2" />
+ Email us at [email protected]
+ <ExternalLink className="w-4 h-4 ml-auto" />
+ </Button>
+ </div>
+ </div>
+
+ {/* FAQ Section */}
+ <div className="bg-card border border-border rounded-lg p-6 space-y-4">
+ <HeadingH3Bold className="text-foreground">
+ Frequently Asked Questions
+ </HeadingH3Bold>
+
+ <div className="space-y-4">
+ <div className="space-y-2">
+ <h4 className="text-foreground font-medium text-sm">
+ How do I upgrade to Pro?
+ </h4>
+ <p className="text-muted-foreground text-sm">
+ Go to the Billing tab in settings and click "Upgrade to Pro".
+ You'll be redirected to our secure payment processor.
+ </p>
+ </div>
+
+ <div className="space-y-2">
+ <h4 className="text-foreground font-medium text-sm">
+ What's included in the Pro plan?
+ </h4>
+ <p className="text-muted-foreground text-sm">
+ Pro includes 5,000 memories (vs 200 in free), 10 connections to
+ external services like Google Drive and Notion, advanced search
+ features, and priority support.
+ </p>
+ </div>
+
+ <div className="space-y-2">
+ <h4 className="text-foreground font-medium text-sm">
+ How do connections work?
+ </h4>
+ <p className="text-muted-foreground text-sm">
+ Connections let you sync documents from Google Drive, Notion,
+ and OneDrive automatically. supermemory will index and make them
+ searchable.
+ </p>
+ </div>
+
+ <div className="space-y-2">
+ <h4 className="text-foreground font-medium text-sm">
+ Can I cancel my subscription anytime?
+ </h4>
+ <p className="text-muted-foreground text-sm">
+ Yes! You can cancel anytime from the Billing tab. Your Pro
+ features will remain active until the end of your billing
+ period.
+ </p>
+ </div>
+ </div>
+ </div>
+
+ {/* Feedback Section */}
+ <div className="bg-card border border-border rounded-lg p-6 space-y-4">
+ <HeadingH3Bold className="text-foreground">
+ Feedback & Feature Requests
+ </HeadingH3Bold>
+ <p className="text-muted-foreground text-sm">
+ Have ideas for new features or improvements? We'd love to hear from
+ you!
+ </p>
+
+ <Button
+ className="w-full justify-start bg-purple-500/20 hover:bg-purple-500/30 text-purple-600 dark:text-purple-400 border-purple-500/30"
+ onClick={() => window.open("https://x.com/supermemoryai", "_blank")}
+ variant="outline"
+ >
+ <MessageCircle className="w-4 h-4 mr-2" />
+ Share your feedback on X
+ <ExternalLink className="w-4 h-4 ml-auto" />
+ </Button>
+ </div>
+ </div>
+ </div>
+ )
+} \ No newline at end of file
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index 70d4916f..622eb2c1 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -1,5 +1,5 @@
import type { Metadata } from "next"
-import { Inter, JetBrains_Mono, Instrument_Serif } from "next/font/google"
+import { Space_Grotesk } from "next/font/google"
import "../globals.css"
import "@ui/globals.css"
import { AuthProvider } from "@lib/auth-context"
@@ -11,25 +11,15 @@ import { Suspense } from "react"
import { Toaster } from "sonner"
import { MobilePanelProvider } from "@/lib/mobile-panel-context"
import { NuqsAdapter } from "nuqs/adapters/next/app"
+import { ThemeProvider } from "@/lib/theme-provider"
import { ViewModeProvider } from "@/lib/view-mode-context"
-const sans = Inter({
+const font = Space_Grotesk({
subsets: ["latin"],
variable: "--font-sans",
})
-const mono = JetBrains_Mono({
- subsets: ["latin"],
- variable: "--font-mono",
-})
-
-const serif = Instrument_Serif({
- subsets: ["latin"],
- variable: "--font-serif",
- weight: ["400"],
-})
-
export const metadata: Metadata = {
metadataBase: new URL("https://app.supermemory.ai"),
description: "Your memories, wherever you are",
@@ -42,33 +32,39 @@ export default function RootLayout({
children: React.ReactNode
}>) {
return (
- <html className="dark bg-sm-black" lang="en">
- <body
- className={`${sans.variable} ${mono.variable} ${serif.variable} antialiased bg-[#0f1419] overflow-x-hidden`}
- >
- <AutumnProvider
- backendUrl={
- process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
- }
- includeCredentials={true}
+ <html lang="en" suppressHydrationWarning>
+ <body className={`${font.variable} antialiased overflow-x-hidden`}>
+ <ThemeProvider
+ attribute="class"
+ defaultTheme="system"
+ enableSystem
+ disableTransitionOnChange
>
- <QueryProvider>
- <AuthProvider>
- <ViewModeProvider>
- <MobilePanelProvider>
- <PostHogProvider>
- <ErrorTrackingProvider>
- <NuqsAdapter>
- <Suspense>{children}</Suspense>
- <Toaster richColors theme="dark" />
- </NuqsAdapter>
- </ErrorTrackingProvider>
- </PostHogProvider>
- </MobilePanelProvider>
- </ViewModeProvider>
- </AuthProvider>
- </QueryProvider>
- </AutumnProvider>
+ <AutumnProvider
+ backendUrl={
+ process.env.NEXT_PUBLIC_BACKEND_URL ??
+ "https://api.supermemory.ai"
+ }
+ includeCredentials={true}
+ >
+ <QueryProvider>
+ <AuthProvider>
+ <ViewModeProvider>
+ <MobilePanelProvider>
+ <PostHogProvider>
+ <ErrorTrackingProvider>
+ <NuqsAdapter>
+ <Suspense>{children}</Suspense>
+ <Toaster richColors />
+ </NuqsAdapter>
+ </ErrorTrackingProvider>
+ </PostHogProvider>
+ </MobilePanelProvider>
+ </ViewModeProvider>
+ </AuthProvider>
+ </QueryProvider>
+ </AutumnProvider>
+ </ThemeProvider>
</body>
</html>
)
diff --git a/apps/web/app/onboarding/animated-text.tsx b/apps/web/app/onboarding/animated-text.tsx
index c3616482..c32abb10 100644
--- a/apps/web/app/onboarding/animated-text.tsx
+++ b/apps/web/app/onboarding/animated-text.tsx
@@ -1,53 +1,62 @@
-'use client';
-import { useEffect } from 'react';
-import { TextEffect } from '@/components/text-effect';
+"use client"
+import { useEffect } from "react"
+import { TextEffect } from "@/components/text-effect"
-export function AnimatedText({ children, trigger, delay }: { children: string, trigger: boolean, delay: number }) {
- const blurSlideVariants = {
- container: {
- hidden: { opacity: 0 },
- visible: {
- opacity: 1,
- transition: { staggerChildren: 0.01 },
- },
- exit: {
- transition: { staggerChildren: 0.01, staggerDirection: 1 },
- },
- },
- item: {
- hidden: {
- opacity: 0,
- filter: 'blur(10px) brightness(0%)',
- y: 0,
- },
- visible: {
- opacity: 1,
- y: 0,
- filter: 'blur(0px) brightness(100%)',
- transition: {
- duration: 0.4,
- },
- },
- exit: {
- opacity: 0,
- y: -30,
- filter: 'blur(10px) brightness(0%)',
- transition: {
- duration: 0.3,
- },
- },
- },
- };
+export function AnimatedText({
+ children,
+ trigger,
+ delay,
+}: {
+ children: string
+ trigger: boolean
+ delay: number
+}) {
+ const blurSlideVariants = {
+ container: {
+ hidden: { opacity: 0 },
+ visible: {
+ opacity: 1,
+ transition: { staggerChildren: 0.01 },
+ },
+ exit: {
+ transition: { staggerChildren: 0.01, staggerDirection: 1 },
+ },
+ },
+ item: {
+ hidden: {
+ opacity: 0,
+ filter: "blur(10px) brightness(0%)",
+ y: 0,
+ },
+ visible: {
+ opacity: 1,
+ y: 0,
+ filter: "blur(0px) brightness(100%)",
+ transition: {
+ duration: 0.4,
+ },
+ },
+ exit: {
+ opacity: 0,
+ y: -30,
+ filter: "blur(10px) brightness(0%)",
+ transition: {
+ duration: 0.3,
+ },
+ },
+ },
+ }
- return (
- <TextEffect
- className='inline-flex'
- per='char'
- variants={blurSlideVariants}
- trigger={trigger}
- delay={delay}
- >
- {children}
- </TextEffect>
- );
+ return (
+ <TextEffect
+ className="inline-flex font-medium"
+ style={{ letterSpacing: "-3px" }}
+ per="char"
+ variants={blurSlideVariants}
+ trigger={trigger}
+ delay={delay}
+ >
+ {children}
+ </TextEffect>
+ )
}
diff --git a/apps/web/app/onboarding/connections-form.tsx b/apps/web/app/onboarding/connections-form.tsx
deleted file mode 100644
index f71f17ce..00000000
--- a/apps/web/app/onboarding/connections-form.tsx
+++ /dev/null
@@ -1,213 +0,0 @@
-"use client";
-
-import { motion, type Transition } from "framer-motion";
-import { Button } from "@ui/components/button";
-import { useOnboarding } from "./onboarding-context";
-import { $fetch } from "@lib/api";
-import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
-import type { ConnectionResponseSchema } from "@repo/validation/api";
-import type { z } from "zod";
-import { Check } from "lucide-react";
-import { toast } from "sonner";
-import { analytics } from "@/lib/analytics";
-import { useProject } from "@/stores";
-import { NavMenu } from "./nav-menu";
-
-type Connection = z.infer<typeof ConnectionResponseSchema>;
-
-const CONNECTORS = {
- "google-drive": {
- title: "Google Drive",
- description: "Supermemory can use the documents and files in your Google Drive to better understand and assist you.",
- iconSrc: "/images/gdrive.svg",
- },
- notion: {
- title: "Notion",
- description: "Help Supermemory understand how you organize your life and what you have going on by connecting your Notion account.",
- iconSrc: "/images/notion.svg",
- },
- onedrive: {
- title: "OneDrive",
- description: "By integrating with OneDrive, Supermemory can better understand both your previous and your current work.",
- iconSrc: "/images/onedrive.svg",
- },
-} as const;
-
-type ConnectorProvider = keyof typeof CONNECTORS;
-
-const containerVariants = {
- hidden: { opacity: 0 },
- visible: {
- opacity: 1,
- transition: { staggerChildren: 0.15, delayChildren: 0.1 } satisfies Transition,
- },
-};
-
-const itemVariants = {
- hidden: { opacity: 0, y: 16 },
- visible: {
- opacity: 1,
- y: 0,
- transition: { type: "spring", stiffness: 500, damping: 35, mass: 0.8 } satisfies Transition,
- },
-};
-
-function ConnectionCard({
- title,
- description,
- iconSrc,
- isConnected = false,
- onConnect,
- isConnecting = false
-}: {
- title: string;
- description: string;
- iconSrc: string;
- isConnected?: boolean;
- onConnect?: () => void;
- isConnecting?: boolean;
-}) {
- return (
- <div className="flex items-center max-sm:items-start max-sm:gap-3 max-sm:flex-col shadow-lg bg-white rounded-xl py-4 px-6 gap-6 border border-zinc-200">
- <img src={iconSrc} alt={title} className="size-10 max-sm:hidden" />
- <div className="flex flex-col gap-0.5 max-sm:gap-2">
- <div className="flex items-center gap-3">
- <img src={iconSrc} alt={title} className="size-5 sm:hidden" />
- <h3 className="text-lg font-medium">{title}</h3>
- </div>
- <p className="text-zinc-600 text-sm">{description}</p>
- </div>
- <div className="sm:ml-auto max-sm:w-full">
- {isConnected ? (
- <Button
- variant="outline"
- className="max-sm:w-full border border-green-200 bg-green-50 text-green-700 hover:bg-green-100 cursor-default"
- disabled
- >
- <Check className="w-4 h-4 mr-2" />
- Connected
- </Button>
- ) : (
- <Button
- variant="outline"
- className="max-sm:w-full border border-zinc-200! hover:text-zinc-900 text-zinc-900 cursor-pointer"
- onClick={onConnect}
- disabled={isConnecting}
- >
- Connect
- </Button>
- )}
- </div>
- </div>
- );
-}
-
-export function ConnectionsForm() {
- const { totalSteps, nextStep, getStepNumberFor } = useOnboarding();
- const { selectedProject } = useProject();
-
- const { data: connections = [] } = useQuery({
- queryKey: ["connections"],
- queryFn: async () => {
- const response = await $fetch("@post/connections/list", {
- body: {
- containerTags: [],
- },
- });
-
- if (response.error) {
- throw new Error(
- response.error?.message || "Failed to load connections",
- );
- }
-
- return response.data as Connection[];
- },
- staleTime: 30 * 1000,
- refetchInterval: 60 * 1000,
- });
-
- const addConnectionMutation = useMutation({
- mutationFn: async (provider: ConnectorProvider) => {
- const response = await $fetch("@post/connections/:provider", {
- params: { provider },
- body: {
- redirectUrl: window.location.href,
- containerTags: [selectedProject],
- },
- });
-
- // biome-ignore lint/style/noNonNullAssertion: its fine
- if ("data" in response && !("error" in response.data!)) {
- return response.data;
- }
-
- throw new Error(response.error?.message || "Failed to connect");
- },
- onSuccess: (data, provider) => {
- analytics.connectionAdded(provider);
- analytics.connectionAuthStarted();
- if (data?.authLink) {
- window.location.href = data.authLink;
- }
- },
- onError: (error, provider) => {
- analytics.connectionAuthFailed();
- toast.error(`Failed to connect ${provider}`, {
- description: error instanceof Error ? error.message : "Unknown error",
- });
- },
- });
-
- function isConnectorConnected(provider: ConnectorProvider): boolean {
- return connections.some(connection => connection.provider === provider);
- }
-
- function handleConnect(provider: ConnectorProvider) {
- addConnectionMutation.mutate(provider);
- }
-
- return (
- <div className="relative">
- <div className="space-y-4">
- <NavMenu>
- <p className="text-base text-zinc-600">
- Step {getStepNumberFor("connections")} of {totalSteps}
- </p>
- </NavMenu>
- <h1 className="max-sm:text-4xl">Connect your accounts</h1>
- <p className="text-zinc-600 text-2xl max-sm:text-lg">
- Help Supermemory get to know you and your documents better
- {/* The more context you provide, the better Supermemory becomes */}
- {/* Supermemory understands your needs and goals better with more context */}
- {/* Supermemory understands you better when it integrates with your apps */}
- </p>
- </div>
- <motion.div className="font-sans text-base mt-8 font-normal tracking-normal flex flex-col gap-4 max-w-prose" initial="hidden" animate="visible" variants={containerVariants}>
- {Object.entries(CONNECTORS).map(([provider, config]) => {
- const providerKey = provider as ConnectorProvider;
- const isConnected = isConnectorConnected(providerKey);
- const isConnecting = addConnectionMutation.isPending && addConnectionMutation.variables === providerKey;
-
- return (
- <motion.div key={provider} variants={itemVariants}>
- <ConnectionCard
- title={config.title}
- description={config.description}
- iconSrc={config.iconSrc}
- isConnected={isConnected}
- onConnect={() => handleConnect(providerKey)}
- isConnecting={isConnecting}
- />
- </motion.div>
- );
- })}
- </motion.div>
- <div className="flex justify-end mt-4">
- <Button variant="link" size="lg" className="text-zinc-900 font-medium! text-lg underline w-fit px-0! cursor-pointer" onClick={nextStep}>
- Skip For Now
- </Button>
- </div>
- </div>
- );
-} \ No newline at end of file
diff --git a/apps/web/app/onboarding/extension-form.tsx b/apps/web/app/onboarding/extension-form.tsx
index c94512e0..63e7dff1 100644
--- a/apps/web/app/onboarding/extension-form.tsx
+++ b/apps/web/app/onboarding/extension-form.tsx
@@ -636,7 +636,9 @@ export function ExtensionForm() {
Step {getStepNumberFor("extension")} of {totalSteps}
</p>
</NavMenu>
- <h1 className="text-white">Install the Chrome extension</h1>
+ <h1 className="text-white font-medium">
+ Install the Chrome extension
+ </h1>
<p className="text-white/80 text-2xl">
{/* Install the Supermemory extension to start saving and organizing everything that matters. */}
Bring Supermemory everywhere
@@ -706,7 +708,7 @@ export function ExtensionForm() {
<Button
variant="link"
size="lg"
- className="text-white/80 font-medium! text-lg underline w-fit px-0! cursor-pointer"
+ className="text-black/40 hover:text-black font-medium! text-lg underline w-fit px-0! cursor-pointer"
onClick={nextStep}
>
Continue
diff --git a/apps/web/app/onboarding/floating-orbs.tsx b/apps/web/app/onboarding/floating-orbs.tsx
index 96a8f061..6b1f9f8d 100644
--- a/apps/web/app/onboarding/floating-orbs.tsx
+++ b/apps/web/app/onboarding/floating-orbs.tsx
@@ -67,19 +67,19 @@ function FloatingOrb({ size, initialX, initialY, duration, delay, revealDelay, s
return {
x: {
duration: shouldReveal ? duration : 0,
- repeat: shouldReveal ? Infinity : 0,
+ repeat: shouldReveal ? Number.POSITIVE_INFINITY : 0,
ease: [0.42, 0, 0.58, 1],
delay: shouldReveal ? delay + revealDelay : 0,
},
y: {
duration: shouldReveal ? duration : 0,
- repeat: shouldReveal ? Infinity : 0,
+ repeat: shouldReveal ? Number.POSITIVE_INFINITY : 0,
ease: [0.42, 0, 0.58, 1],
delay: shouldReveal ? delay + revealDelay : 0,
},
scale: {
duration: shouldReveal ? duration : 0.8,
- repeat: shouldReveal ? Infinity : 0,
+ repeat: shouldReveal ? Number.POSITIVE_INFINITY : 0,
ease: shouldReveal ? [0.42, 0, 0.58, 1] : [0, 0, 0.58, 1],
delay: shouldReveal ? delay + revealDelay : revealDelay,
},
@@ -163,7 +163,8 @@ export function FloatingOrbs() {
// Generate orb configurations positioned along edges
const newOrbs = Array.from({ length: 8 }, (_, i) => {
- let x, y;
+ let x: number;
+ let y: number;
const zone = i % 4; // Rotate through 4 zones: top, right, bottom, left
switch (zone) {
diff --git a/apps/web/app/onboarding/intro.tsx b/apps/web/app/onboarding/intro.tsx
index 5c8504a5..cc242df3 100644
--- a/apps/web/app/onboarding/intro.tsx
+++ b/apps/web/app/onboarding/intro.tsx
@@ -1,114 +1,85 @@
-"use client";
+"use client"
-import { AnimatedText } from "./animated-text";
-import { motion, AnimatePresence } from "motion/react";
-import { Button } from "@repo/ui/components/button";
-import { cn } from "@lib/utils";
-import { ArrowRightIcon } from "lucide-react";
-import { useOnboarding } from "./onboarding-context";
-import { useIsMobile } from "@hooks/use-mobile";
+import { AnimatedText } from "./animated-text"
+import { motion, AnimatePresence } from "motion/react"
+import { Button } from "@repo/ui/components/button"
+import { cn } from "@lib/utils"
+import { useOnboarding } from "./onboarding-context"
export function Intro() {
- const { nextStep, introTriggers: triggers } = useOnboarding();
- const isMobile = useIsMobile();
+ const { nextStep, introTriggers: triggers } = useOnboarding()
- return (
- <motion.div
- className="flex flex-col gap-4 relative max-sm:text-4xl max-sm:w-full text-white"
- layout
- transition={{
- layout: { duration: 0.8, ease: "anticipate" }
- }}
- >
- <AnimatePresence mode="popLayout">
- {triggers.first && (
- <motion.div
- key="first"
- layout
- initial={{ opacity: 0 }}
- animate={{ opacity: 1 }}
- transition={{
- opacity: { duration: 0.3 },
- layout: { duration: 0.8, ease: "easeInOut" }
- }}
- >
- {isMobile ? (
- <div className="flex flex-col">
- <AnimatedText trigger={triggers.first} delay={0}>
- Still looking for your
- </AnimatedText>
- <AnimatedText trigger={triggers.first} delay={0.1}>
- other half?
- </AnimatedText>
- </div>
- ) : (
- <AnimatedText trigger={triggers.first} delay={0}>
- Still looking for your other half?
- </AnimatedText>
- )}
- </motion.div>
- )}
- {triggers.second && (
- <motion.div
- key="second"
- layout
- initial={{ opacity: 0 }}
- animate={{ opacity: 1 }}
- transition={{
- opacity: { duration: 0.3 },
- layout: { duration: 0.8, ease: "easeInOut" }
- }}
- >
- <AnimatedText trigger={triggers.second} delay={0.4}>
- Don't worry.
- </AnimatedText>
- </motion.div>
- )}
- {triggers.third && (
- <motion.div
- key="third"
- layout
- initial={{ opacity: 0 }}
- animate={{ opacity: 1 }}
- transition={{
- opacity: { duration: 0.3 },
- layout: { duration: 0.8, ease: "easeInOut" }
- }}
- >
- <AnimatedText trigger={triggers.third} delay={0.4}>
- It's right here.
- {/* It's right in front of you. */}
- {/* You're looking at it. */}
- </AnimatedText>
- </motion.div>
- )}
- </AnimatePresence>
- <motion.div
- key="fourth"
- className="absolute -bottom-16 left-0"
- initial={{ opacity: 0, filter: "blur(5px)" }}
- animate={{
- opacity: triggers.fourth ? 1 : 0,
- filter: triggers.fourth ? "blur(0px)" : "blur(5px)"
- }}
- transition={{
- opacity: { duration: 0.6, ease: "easeOut" },
- filter: { duration: 0.4, ease: "easeOut" }
- }}
- >
- <Button
- variant={"link"}
- size={"lg"}
- className={cn("text-white/60 flex items-center gap-2 hover:text-white/90 text-2xl underline group font-normal w-fit px-0! cursor-pointer", !triggers.fourth && "pointer-events-none")}
- style={{
- transform: triggers.fourth ? "scale(1)" : "scale(0.95)"
- }}
- onClick={nextStep}
- >
- Meet Supermemory
- <ArrowRightIcon className="w-4 h-4 group-hover:translate-x-0.5 transition-transform duration-200" />
- </Button>
- </motion.div>
- </motion.div>
- )
-} \ No newline at end of file
+ return (
+ <motion.div
+ className="flex flex-col gap-4 relative max-sm:text-4xl max-sm:w-full text-white text-center"
+ layout
+ transition={{
+ layout: { duration: 0.8, ease: "anticipate" },
+ }}
+ >
+ <AnimatePresence mode="popLayout">
+ <p className="font-medium text-base">Hey there!</p>
+ {triggers.first && (
+ <motion.div
+ key="first"
+ layout
+ initial={{ opacity: 0 }}
+ animate={{ opacity: 1 }}
+ transition={{
+ opacity: { duration: 0.3 },
+ layout: { duration: 0.8, ease: "easeInOut" },
+ }}
+ >
+ <AnimatedText trigger={triggers.first} delay={0}>
+ Intelligence without memory
+ </AnimatedText>
+ </motion.div>
+ )}
+ {triggers.second && (
+ <motion.div
+ key="second"
+ layout
+ initial={{ opacity: 0 }}
+ animate={{ opacity: 1 }}
+ transition={{
+ opacity: { duration: 0.3 },
+ layout: { duration: 0.8, ease: "easeInOut" },
+ }}
+ >
+ <AnimatedText trigger={triggers.second} delay={0.4}>
+ is just sophisticated randomness.
+ </AnimatedText>
+ </motion.div>
+ )}
+ </AnimatePresence>
+ <motion.div
+ key="fourth"
+ className="justify-center flex mt-6"
+ initial={{ opacity: 0, filter: "blur(5px)" }}
+ animate={{
+ opacity: triggers.fourth ? 1 : 0,
+ filter: triggers.fourth ? "blur(0px)" : "blur(5px)",
+ }}
+ transition={{
+ opacity: { duration: 0.6, ease: "easeOut" },
+ filter: { duration: 0.4, ease: "easeOut" },
+ }}
+ >
+ <Button
+ variant={"default"}
+ size={"sm"}
+ className={cn(
+ "bg-[#1e3a5f] hover:bg-[#2a4a75] border-2 border-[#4a7ba7]/50 text-white font-medium px-6 py-3 rounded-lg shadow-lg hover:shadow-xl transition-all duration-300",
+ !triggers.fourth && "pointer-events-none opacity-50",
+ )}
+ style={{
+ transform: triggers.fourth ? "scale(1)" : "scale(0.95)",
+ }}
+ onClick={nextStep}
+ >
+ Get Started
+ </Button>
+ </motion.div>
+ </motion.div>
+ )
+}
diff --git a/apps/web/app/onboarding/name-form.tsx b/apps/web/app/onboarding/name-form.tsx
index 2b9520c9..df734601 100644
--- a/apps/web/app/onboarding/name-form.tsx
+++ b/apps/web/app/onboarding/name-form.tsx
@@ -63,7 +63,6 @@ export function NameForm() {
<div className="relative flex flex-col">
<input
type="text"
- autoFocus
name="name"
autoComplete="name"
autoCorrect="off"
diff --git a/apps/web/app/onboarding/onboarding-context.tsx b/apps/web/app/onboarding/onboarding-context.tsx
index 260fb51e..0b0eb346 100644
--- a/apps/web/app/onboarding/onboarding-context.tsx
+++ b/apps/web/app/onboarding/onboarding-context.tsx
@@ -110,7 +110,7 @@ export function OnboardingProvider({
}, 400),
]
- return () => cleanups.forEach((cleanup) => clearTimeout(cleanup))
+ return () => cleanups.forEach(clearTimeout)
}, [currentStep])
// Set orbs as revealed once the fourth trigger is activated OR if we're on any non-intro step
diff --git a/apps/web/app/onboarding/welcome.tsx b/apps/web/app/onboarding/welcome.tsx
index 3f73e43a..93313f7a 100644
--- a/apps/web/app/onboarding/welcome.tsx
+++ b/apps/web/app/onboarding/welcome.tsx
@@ -15,7 +15,7 @@ export function Welcome() {
return (
<div className="flex flex-col gap-4 items-center text-center">
- <h1 className="text-white">Welcome to Supermemory</h1>
+ <h1 className="text-white font-medium">Welcome to Supermemory</h1>
<p className="text-white/80 text-2xl">
We're excited to have you on board.
</p>
diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx
deleted file mode 100644
index 8886067d..00000000
--- a/apps/web/app/page.tsx
+++ /dev/null
@@ -1,622 +0,0 @@
-"use client"
-
-import { useIsMobile } from "@hooks/use-mobile"
-import { useOnboardingStorage } from "@hooks/use-onboarding-storage"
-import { useAuth } from "@lib/auth-context"
-import { $fetch } from "@repo/lib/api"
-import { MemoryGraph } from "@repo/ui/memory-graph"
-import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
-import { useInfiniteQuery, useQuery } from "@tanstack/react-query"
-import { Logo, LogoFull } from "@ui/assets/Logo"
-import { Button } from "@ui/components/button"
-import { GlassMenuEffect } from "@ui/other/glass-effect"
-import {
- HelpCircle,
- LayoutGrid,
- List,
- LoaderIcon,
- MessageSquare,
- Unplug,
-} from "lucide-react"
-import { AnimatePresence, motion } from "motion/react"
-import Link from "next/link"
-import { useRouter } from "next/navigation"
-import { useCallback, useEffect, useMemo, useState } from "react"
-import type { z } from "zod"
-import { ConnectAIModal } from "@/components/connect-ai-modal"
-import { InstallPrompt } from "@/components/install-prompt"
-import { MemoryListView } from "@/components/memory-list-view"
-import Menu from "@/components/menu"
-import { ProjectSelector } from "@/components/project-selector"
-import { ReferralUpgradeModal } from "@/components/referral-upgrade-modal"
-import { AddMemoryView } from "@/components/views/add-memory"
-import { ChatRewrite } from "@/components/views/chat"
-import { useViewMode } from "@/lib/view-mode-context"
-import { useChatOpen, useProject } from "@/stores"
-import { useGraphHighlights } from "@/stores/highlights"
-
-type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
-type DocumentWithMemories = DocumentsResponse["documents"][0]
-
-const MemoryGraphPage = () => {
- const { documentIds: allHighlightDocumentIds } = useGraphHighlights()
- const isMobile = useIsMobile()
- const { viewMode, setViewMode } = useViewMode()
- const { selectedProject } = useProject()
- const { isOpen, setIsOpen } = useChatOpen()
- const [injectedDocs, setInjectedDocs] = useState<DocumentWithMemories[]>([])
- const [showAddMemoryView, setShowAddMemoryView] = useState(false)
- const [showReferralModal, setShowReferralModal] = useState(false)
- const [showConnectAIModal, setShowConnectAIModal] = useState(false)
- const [isHelpHovered, setIsHelpHovered] = useState(false)
-
- // Fetch projects meta to detect experimental flag
- const { data: projectsMeta = [] } = useQuery({
- queryKey: ["projects"],
- queryFn: async () => {
- const response = await $fetch("@get/projects")
- return response.data?.projects ?? []
- },
- staleTime: 5 * 60 * 1000,
- })
-
- const isCurrentProjectExperimental = !!projectsMeta.find(
- (p: any) => p.containerTag === selectedProject,
- )?.isExperimental
-
- // Progressive loading via useInfiniteQuery
- const IS_DEV = process.env.NODE_ENV === "development"
- const PAGE_SIZE = IS_DEV ? 100 : 100
- const MAX_TOTAL = 1000
-
- const {
- data,
- error,
- isPending,
- isFetchingNextPage,
- hasNextPage,
- fetchNextPage,
- } = useInfiniteQuery<DocumentsResponse, Error>({
- queryKey: ["documents-with-memories", selectedProject],
- initialPageParam: 1,
- queryFn: async ({ pageParam }) => {
- const response = await $fetch("@post/documents/documents", {
- body: {
- page: pageParam as number,
- limit: (pageParam as number) === 1 ? (IS_DEV ? 500 : 500) : PAGE_SIZE,
- sort: "createdAt",
- order: "desc",
- containerTags: selectedProject ? [selectedProject] : undefined,
- },
- disableValidation: true,
- })
-
- if (response.error) {
- throw new Error(response.error?.message || "Failed to fetch documents")
- }
-
- return response.data
- },
- getNextPageParam: (lastPage, allPages) => {
- const loaded = allPages.reduce(
- (acc, p) => acc + (p.documents?.length ?? 0),
- 0,
- )
- if (loaded >= MAX_TOTAL) return undefined
-
- const { currentPage, totalPages } = lastPage.pagination
- if (currentPage < totalPages) {
- return currentPage + 1
- }
- return undefined
- },
- staleTime: 5 * 60 * 1000,
- })
-
- const baseDocuments = useMemo(() => {
- return (
- data?.pages.flatMap((p: DocumentsResponse) => p.documents ?? []) ?? []
- )
- }, [data])
-
- const allDocuments = useMemo(() => {
- if (injectedDocs.length === 0) return baseDocuments
- const byId = new Map<string, DocumentWithMemories>()
- for (const d of injectedDocs) byId.set(d.id, d)
- for (const d of baseDocuments) if (!byId.has(d.id)) byId.set(d.id, d)
- return Array.from(byId.values())
- }, [baseDocuments, injectedDocs])
-
- const totalLoaded = allDocuments.length
- const hasMore = hasNextPage
- const isLoadingMore = isFetchingNextPage
-
- const loadMoreDocuments = useCallback(async (): Promise<void> => {
- if (hasNextPage && !isFetchingNextPage) {
- await fetchNextPage()
- return
- }
- return
- }, [hasNextPage, isFetchingNextPage, fetchNextPage])
-
- // Reset injected docs when project changes
- useEffect(() => {
- setInjectedDocs([])
- }, [selectedProject])
-
- // Surgical fetch of missing highlighted documents (customId-based IDs from search)
- useEffect(() => {
- if (!isOpen) return
- if (!allHighlightDocumentIds || allHighlightDocumentIds.length === 0) return
- const present = new Set<string>()
- for (const d of [...baseDocuments, ...injectedDocs]) {
- if (d.id) present.add(d.id)
- if ((d as any).customId) present.add((d as any).customId as string)
- }
- const missing = allHighlightDocumentIds.filter(
- (id: string) => !present.has(id),
- )
- if (missing.length === 0) return
- let cancelled = false
- const run = async () => {
- try {
- const resp = await $fetch("@post/documents/documents/by-ids", {
- body: {
- ids: missing,
- by: "customId",
- containerTags: selectedProject ? [selectedProject] : undefined,
- },
- disableValidation: true,
- })
- if (cancelled || (resp as any)?.error) return
- const extraDocs = (resp as any)?.data?.documents as
- | DocumentWithMemories[]
- | undefined
- if (!extraDocs || extraDocs.length === 0) return
- setInjectedDocs((prev) => {
- const seen = new Set<string>([
- ...prev.map((d) => d.id),
- ...baseDocuments.map((d) => d.id),
- ])
- const merged = [...prev]
- for (const doc of extraDocs) {
- if (!seen.has(doc.id)) {
- merged.push(doc)
- seen.add(doc.id)
- }
- }
- return merged
- })
- } catch {}
- }
- void run()
- return () => {
- cancelled = true
- }
- }, [
- isOpen,
- allHighlightDocumentIds.join("|"),
- baseDocuments,
- injectedDocs,
- selectedProject,
- $fetch,
- ])
-
- // Handle view mode change
- const handleViewModeChange = useCallback(
- (mode: "graph" | "list") => {
- setViewMode(mode)
- },
- [setViewMode],
- )
-
- useEffect(() => {
- if (allDocuments.length === 0) {
- setShowConnectAIModal(true)
- }
- }, [allDocuments.length])
-
- // Prevent body scrolling
- useEffect(() => {
- document.body.style.overflow = "hidden"
- document.body.style.height = "100vh"
- document.documentElement.style.overflow = "hidden"
- document.documentElement.style.height = "100vh"
-
- return () => {
- document.body.style.overflow = ""
- document.body.style.height = ""
- document.documentElement.style.overflow = ""
- document.documentElement.style.height = ""
- }
- }, [])
-
- return (
- <div className="relative h-screen bg-[#0f1419] overflow-hidden touch-none">
- {/* Main content area */}
- <motion.div
- animate={{
- marginRight: isOpen && !isMobile ? 600 : 0,
- }}
- className="h-full relative"
- transition={{
- duration: 0.2,
- ease: [0.4, 0, 0.2, 1], // Material Design easing - snappy but smooth
- }}
- >
- <motion.div
- animate={{ opacity: 1, y: 0 }}
- className="absolute md:top-4 md:right-4 md:bottom-auto md:left-auto bottom-8 left-6 z-20 rounded-xl overflow-hidden"
- initial={{ opacity: 0, y: -20 }}
- transition={{ type: "spring", stiffness: 300, damping: 25 }}
- >
- <GlassMenuEffect rounded="rounded-xl" />
- <div className="relative z-10 p-2 flex gap-1">
- <motion.button
- animate={{
- color: viewMode === "graph" ? "#93c5fd" : "#cbd5e1",
- }}
- className="relative h-8 px-3 flex items-center gap-2 text-sm font-medium rounded-md transition-colors"
- onClick={() => handleViewModeChange("graph")}
- transition={{ duration: 0.2 }}
- whileHover={{ scale: 1.02 }}
- whileTap={{ scale: 0.98 }}
- >
- {viewMode === "graph" && (
- <motion.div
- className="absolute inset-0 bg-blue-500/20 rounded-md"
- layoutId="activeBackground"
- transition={{
- type: "spring",
- stiffness: 400,
- damping: 30,
- }}
- />
- )}
- <span className="relative z-10 flex items-center gap-2">
- <LayoutGrid className="w-4 h-4" />
- <span className="hidden md:inline">Graph</span>
- </span>
- </motion.button>
-
- <motion.button
- animate={{
- color: viewMode === "list" ? "#93c5fd" : "#cbd5e1",
- }}
- className="relative h-8 px-3 flex items-center gap-2 text-sm font-medium rounded-md transition-colors"
- onClick={() => handleViewModeChange("list")}
- transition={{ duration: 0.2 }}
- whileHover={{ scale: 1.02 }}
- whileTap={{ scale: 0.98 }}
- >
- {viewMode === "list" && (
- <motion.div
- className="absolute inset-0 bg-blue-500/20 rounded-md"
- layoutId="activeBackground"
- transition={{
- type: "spring",
- stiffness: 400,
- damping: 30,
- }}
- />
- )}
- <span className="relative z-10 flex items-center gap-2">
- <List className="w-4 h-4" />
- <span className="hidden md:inline">List</span>
- </span>
- </motion.button>
- </div>
- </motion.div>
-
- {/* Animated content switching */}
- <AnimatePresence mode="wait">
- {viewMode === "graph" ? (
- <motion.div
- animate={{ opacity: 1, scale: 1 }}
- className="absolute inset-0"
- exit={{ opacity: 0, scale: 0.95 }}
- initial={{ opacity: 0, scale: 0.95 }}
- key="graph"
- transition={{
- type: "spring",
- stiffness: 500,
- damping: 30,
- }}
- >
- <MemoryGraph
- autoLoadOnViewport={false}
- documents={allDocuments}
- error={error}
- hasMore={hasMore}
- highlightDocumentIds={allHighlightDocumentIds}
- highlightsVisible={isOpen}
- isExperimental={isCurrentProjectExperimental}
- isLoading={isPending}
- isLoadingMore={isLoadingMore}
- legendId={undefined}
- loadMoreDocuments={loadMoreDocuments}
- occludedRightPx={isOpen && !isMobile ? 600 : 0}
- showSpacesSelector={false}
- totalLoaded={totalLoaded}
- variant="consumer"
- >
- <div className="absolute inset-0 flex items-center justify-center">
- <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">
- <p className="text-lg font-medium mb-4">
- Get Started with supermemory
- </p>
- <div className="flex flex-col gap-3">
- <p className="text-sm text-blue-400 hover:text-blue-300 transition-colors">
- Click here to set up your AI connection
- </p>
- <p className="text-xs text-white/60">or</p>
- <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>
- </MemoryGraph>
- </motion.div>
- ) : (
- <motion.div
- animate={{ opacity: 1, scale: 1 }}
- className="absolute inset-0 md:ml-18"
- exit={{ opacity: 0, scale: 0.95 }}
- initial={{ opacity: 0, scale: 0.95 }}
- key="list"
- transition={{
- type: "spring",
- stiffness: 500,
- damping: 30,
- }}
- >
- <MemoryListView
- documents={allDocuments}
- error={error}
- hasMore={hasMore}
- isLoading={isPending}
- isLoadingMore={isLoadingMore}
- loadMoreDocuments={loadMoreDocuments}
- totalLoaded={totalLoaded}
- >
- <div className="absolute inset-0 flex items-center justify-center">
- <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">
- <p className="text-lg font-medium mb-4">
- Get Started with supermemory
- </p>
- <div className="flex flex-col gap-3">
- <p className="text-sm text-blue-400 hover:text-blue-300 transition-colors">
- Click here to set up your AI connection
- </p>
- <p className="text-xs text-white/60">or</p>
- <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>
- </MemoryListView>
- </motion.div>
- )}
- </AnimatePresence>
-
- {/* Top Bar */}
- <div className="absolute top-2 left-0 right-0 z-10 p-4 flex items-center justify-between">
- <div className="flex items-center gap-3 justify-between w-full md:w-fit md:justify-start">
- <Link
- className="pointer-events-auto"
- href="https://supermemory.ai"
- rel="noopener noreferrer"
- target="_blank"
- >
- <LogoFull className="h-8 hidden md:block" />
- <Logo className="h-8 md:hidden" />
- </Link>
-
- <div className="hidden sm:block">
- <ProjectSelector />
- </div>
-
- <ConnectAIModal>
- <Button
- className="bg-white/5 hover:bg-white/10 border-white/20 text-white hover:text-white px-2 sm:px-3"
- size="sm"
- variant="outline"
- >
- <Unplug className="h-4 w-4" />
- <span className="hidden sm:inline ml-2">
- Connect to your AI
- </span>
- <span className="sm:hidden ml-1">Connect AI</span>
- </Button>
- </ConnectAIModal>
- </div>
-
- <div>
- <Menu />
- </div>
- </div>
-
- {/* Floating Open Chat Button */}
- {!isOpen && !isMobile && (
- <motion.div
- animate={{ opacity: 1, scale: 1 }}
- className="fixed bottom-6 right-6 z-50"
- initial={{ opacity: 0, scale: 0.8 }}
- transition={{
- type: "spring",
- stiffness: 300,
- damping: 25,
- }}
- >
- <Button
- className="px-4 bg-white hover:bg-white/80 text-[#001A39] shadow-lg hover:shadow-xl transition-all duration-200 rounded-full flex items-center gap-2 cursor-pointer"
- onClick={() => setIsOpen(true)}
- size="lg"
- >
- <MessageSquare className="h-5 w-5" />
- <span className="font-medium">Open Chat</span>
- </Button>
- </motion.div>
- )}
-
- <button
- className="fixed bottom-6 left-6 z-50 flex items-center overflow-hidden rounded-full shadow-2xl bg-transparent border-none cursor-pointer"
- onMouseEnter={() => setIsHelpHovered(true)}
- onMouseLeave={() => setIsHelpHovered(false)}
- type="button"
- >
- <div className="absolute inset-0 rounded-full">
- <GlassMenuEffect rounded="rounded-full" />
- </div>
-
- <div className="relative z-10 p-3 text-white">
- <HelpCircle className="h-5 w-5" />
- </div>
-
- <div
- className={`relative z-10 flex items-center text-white transition-all duration-300 overflow-hidden ${
- isHelpHovered
- ? "opacity-100 max-w-32 pr-4 pl-0 py-3"
- : "opacity-0 max-w-0 px-0 py-3"
- }`}
- >
- <a
- className="flex items-center gap-2 text-sm font-medium hover:text-white/80 transition-colors whitespace-nowrap"
- href="mailto:[email protected]"
- >
- <span>Need Help?</span>
- </a>
- </div>
- </button>
- </motion.div>
-
- {/* Chat panel - positioned absolutely */}
- <motion.div
- className="fixed top-0 right-0 h-full z-50 md:z-auto"
- style={{
- width: isOpen ? (isMobile ? "100vw" : "600px") : 0,
- pointerEvents: isOpen ? "auto" : "none",
- }}
- >
- <motion.div
- animate={{ x: isOpen ? 0 : isMobile ? "100%" : 600 }}
- className="absolute inset-0"
- exit={{ x: isMobile ? "100%" : 600 }}
- initial={{ x: isMobile ? "100%" : 600 }}
- key="chat"
- transition={{
- type: "spring",
- stiffness: 500,
- damping: 40,
- }}
- >
- <ChatRewrite />
- </motion.div>
- </motion.div>
-
- {showAddMemoryView && (
- <AddMemoryView
- initialTab="note"
- onClose={() => setShowAddMemoryView(false)}
- />
- )}
-
- {/* Referral/Upgrade Modal */}
- <ReferralUpgradeModal
- isOpen={showReferralModal}
- onClose={() => setShowReferralModal(false)}
- />
- </div>
- )
-}
-
-// Wrapper component to handle auth and waitlist checks
-export default function Page() {
- const { user, session } = useAuth()
- const { shouldShowOnboarding, isLoading: onboardingLoading } =
- useOnboardingStorage()
- const router = useRouter()
-
- useEffect(() => {
- const url = new URL(window.location.href)
- const authenticateChromeExtension = url.searchParams.get(
- "extension-auth-success",
- )
-
- if (authenticateChromeExtension) {
- const sessionToken = session?.token
- const userData = {
- email: user?.email,
- name: user?.name,
- userId: user?.id,
- }
-
- if (sessionToken && userData?.email) {
- const encodedToken = encodeURIComponent(sessionToken)
- window.postMessage({ token: encodedToken, userData }, "*")
- url.searchParams.delete("extension-auth-success")
- window.history.replaceState({}, "", url.toString())
- }
- }
- }, [user, session])
-
- useEffect(() => {
- if (user && !onboardingLoading && shouldShowOnboarding()) {
- router.push("/onboarding")
- }
- }, [user, shouldShowOnboarding, onboardingLoading, router])
-
- if (!user || onboardingLoading) {
- return (
- <div className="min-h-screen flex items-center justify-center bg-[#0f1419]">
- <div className="flex flex-col items-center gap-4">
- <LoaderIcon className="w-8 h-8 text-orange-500 animate-spin" />
- <p className="text-white/60">Loading...</p>
- </div>
- </div>
- )
- }
-
- if (shouldShowOnboarding()) {
- return null
- }
-
- return (
- <>
- <MemoryGraphPage />
- <InstallPrompt />
- </>
- )
-}
diff --git a/apps/web/button.tsx b/apps/web/button.tsx
deleted file mode 100644
index 3a671e03..00000000
--- a/apps/web/button.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { cn } from "@lib/utils";
-import { Slot } from "@radix-ui/react-slot";
-import { cva, type VariantProps } from "class-variance-authority";
-import type * as React from "react";
-
-const buttonVariants = cva(
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-2 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
- {
- variants: {
- variant: {
- default:
- "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
- destructive:
- "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
- outline:
- "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
- secondary:
- "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
- ghost:
- "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
- link: "text-primary underline-offset-4 hover:underline",
- },
- size: {
- default: "h-9 px-4 py-2 has-[>svg]:px-3",
- sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
- lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
- icon: "size-9",
- },
- },
- defaultVariants: {
- variant: "default",
- size: "default",
- },
- },
-);
-
-function Button({
- className,
- variant,
- size,
- asChild = false,
- ...props
-}: React.ComponentProps<"button"> &
- VariantProps<typeof buttonVariants> & {
- asChild?: boolean;
- }) {
- const Comp = asChild ? Slot : "button";
-
- return (
- <Comp
- className={cn(buttonVariants({ variant, size, className }))}
- data-slot="button"
- {...props}
- />
- );
-}
-
-export { Button, buttonVariants };
diff --git a/apps/web/components/chat-input.tsx b/apps/web/components/chat-input.tsx
new file mode 100644
index 00000000..cf7409fc
--- /dev/null
+++ b/apps/web/components/chat-input.tsx
@@ -0,0 +1,80 @@
+"use client"
+
+import { useState } from "react"
+import { useRouter } from "next/navigation"
+import { generateId } from "@lib/generate-id"
+import { usePersistentChat } from "@/stores/chat"
+import { ArrowUp } from "lucide-react"
+import { Button } from "@ui/components/button"
+import { ProjectSelector } from "./project-selector"
+
+export function ChatInput() {
+ const [message, setMessage] = useState("")
+ const router = useRouter()
+ const { setCurrentChatId } = usePersistentChat()
+
+ const handleSend = () => {
+ if (!message.trim()) return
+
+ const newChatId = generateId()
+
+ setCurrentChatId(newChatId)
+
+ // Store the initial message in sessionStorage for the chat page to pick up
+ sessionStorage.setItem(`chat-initial-${newChatId}`, message.trim())
+
+ router.push(`/chat/${newChatId}`)
+
+ setMessage("")
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault()
+ handleSend()
+ }
+ }
+
+ return (
+ <div className="flex-1 flex items-center justify-center px-4">
+ <div className="w-full max-w-4xl">
+ <div className="text-start mb-4">
+ <h2 className="text-3xl font-bold text-foreground">
+ Good evening, <span className="text-primary">Mahesh</span>
+ </h2>
+ </div>
+ <div className="relative">
+ <form
+ className="flex flex-col items-end bg-card border border-border rounded-[14px] shadow-lg"
+ onSubmit={(e) => {
+ e.preventDefault()
+ if (!message.trim()) return
+ handleSend()
+ }}
+ >
+ <textarea
+ value={message}
+ onChange={(e) => setMessage(e.target.value)}
+ onKeyDown={handleKeyDown}
+ placeholder="Ask your supermemory..."
+ className="w-full text-foreground placeholder-muted-foreground rounded-md outline-none resize-none text-base leading-relaxed px-6 py-4 bg-transparent"
+ rows={2}
+ />
+ <div className="flex items-center gap-2 w-full justify-between bg-accent py-2 px-3 rounded-b-[14px]">
+ <ProjectSelector />
+ <Button
+ onClick={handleSend}
+ disabled={!message.trim()}
+ className="text-primary-foreground border-0 rounded-xl transition-colors disabled:opacity-50 disabled:cursor-not-allowed !bg-primary h-8 w-8"
+ variant="outline"
+ size="icon"
+ >
+ <ArrowUp className="size-3.5" />
+ </Button>
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
+ )
+}
diff --git a/apps/web/components/connect-ai-modal.tsx b/apps/web/components/connect-ai-modal.tsx
index b9759d4e..b576934f 100644
--- a/apps/web/components/connect-ai-modal.tsx
+++ b/apps/web/components/connect-ai-modal.tsx
@@ -193,7 +193,7 @@ export function ConnectAIModal({
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="sm:max-w-4xl">
<DialogHeader>
- <DialogTitle>Connect Supermemory to Your AI</DialogTitle>
+ <DialogTitle>Connect supermemory to Your AI</DialogTitle>
<DialogDescription>
Connect supermemory to your favorite AI tools using the Model
Context Protocol (MCP). This allows your AI assistant to create,
@@ -205,9 +205,7 @@ export function ConnectAIModal({
{/* Step 1: Client Selection */}
<div className="space-y-4">
<div className="flex items-center gap-3">
- <div
- className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${"bg-white/10 text-white/60"}`}
- >
+ <div className="w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium bg-muted text-muted-foreground">
1
</div>
<h3 className="text-sm font-medium">Select Your AI Client</h3>
@@ -220,8 +218,8 @@ export function ConnectAIModal({
<button
className={`pr-3 pl-1 rounded-full border cursor-pointer transition-all ${
selectedClient === key
- ? "border-blue-500 bg-blue-500/10"
- : "border-white/10 hover:border-white/20 hover:bg-white/5"
+ ? "border-primary bg-primary/10"
+ : "border-border hover:border-border/60 hover:bg-muted/50"
}`}
key={key}
onClick={() =>
@@ -233,7 +231,7 @@ export function ConnectAIModal({
<div className="w-8 h-8 flex items-center justify-center">
<Image
alt={clientName}
- className="rounded object-contain text-white fill-white"
+ className="rounded object-contain"
height={20}
onError={(e) => {
const target = e.target as HTMLImageElement
@@ -245,7 +243,7 @@ export function ConnectAIModal({
) {
const fallback = document.createElement("span")
fallback.className =
- "fallback-text text-sm font-bold text-white/40"
+ "fallback-text text-sm font-bold text-muted-foreground"
fallback.textContent = clientName
.substring(0, 2)
.toUpperCase()
@@ -260,7 +258,7 @@ export function ConnectAIModal({
width={20}
/>
</div>
- <span className="text-sm font-medium text-white/80">
+ <span className="text-sm font-medium text-foreground/80">
{clientName}
</span>
</div>
@@ -274,7 +272,7 @@ export function ConnectAIModal({
<div className="space-y-4">
<div className="flex justify-between">
<div className="flex items-center gap-3">
- <div className="w-8 h-8 rounded-full bg-white/10 text-white/60 flex items-center justify-center text-sm font-medium">
+ <div className="w-8 h-8 rounded-full bg-muted text-muted-foreground flex items-center justify-center text-sm font-medium">
2
</div>
<h3 className="text-sm font-medium">
@@ -294,12 +292,12 @@ export function ConnectAIModal({
>
{/* Tabs */}
<div className="flex justify-end">
- <div className="flex bg-white/5 rounded-full p-1 border border-white/10">
+ <div className="flex bg-muted/50 rounded-full p-1 border border-border">
<button
className={`px-3 py-1.5 text-xs font-medium rounded-full transition-all ${
cursorInstallTab === "oneClick"
- ? "bg-white/10 text-white border border-white/20"
- : "text-white/60 hover:text-white/80"
+ ? "bg-background text-foreground border border-border shadow-sm"
+ : "text-muted-foreground hover:text-foreground"
}`}
onClick={() => setCursorInstallTab("oneClick")}
type="button"
@@ -309,8 +307,8 @@ export function ConnectAIModal({
<button
className={`px-3 py-1.5 text-xs font-medium rounded-full transition-all ${
cursorInstallTab === "manual"
- ? "bg-white/10 text-white border border-white/20"
- : "text-white/60 hover:text-white/80"
+ ? "bg-background text-foreground border border-border shadow-sm"
+ : "text-muted-foreground hover:text-foreground"
}`}
onClick={() => setCursorInstallTab("manual")}
type="button"
@@ -329,11 +327,11 @@ export function ConnectAIModal({
<div className="space-y-4">
<div className="flex flex-col items-center gap-4 p-6 border border-green-500/20 rounded-lg bg-green-500/5">
<div className="text-center">
- <p className="text-sm text-white/80 mb-2">
+ <p className="text-sm text-foreground/80 mb-2">
Click the button below to automatically install and
configure Supermemory in Cursor
</p>
- <p className="text-xs text-white/50">
+ <p className="text-xs text-muted-foreground">
This will install the MCP server without any
additional setup required
</p>
@@ -353,13 +351,13 @@ export function ConnectAIModal({
/>
</a>
</div>
- <p className="text-xs text-white/40 text-center">
+ <p className="text-xs text-muted-foreground/60 text-center">
Make sure you have Cursor installed on your system
</p>
</div>
) : (
<div className="space-y-4">
- <p className="text-sm text-white/70">
+ <p className="text-sm text-muted-foreground">
Choose a project and follow the installation steps below
</p>
<div className="max-w-md">
@@ -371,17 +369,11 @@ export function ConnectAIModal({
<SelectTrigger className="w-full">
<SelectValue placeholder="Select project" />
</SelectTrigger>
- <SelectContent className="bg-black/90 backdrop-blur-xl border-white/10">
- <SelectItem
- className="text-white hover:bg-white/10"
- value="none"
- >
+ <SelectContent>
+ <SelectItem value="none">
Auto-select project
</SelectItem>
- <SelectItem
- className="text-white hover:bg-white/10"
- value="sm_project_default"
- >
+ <SelectItem value="sm_project_default">
Default Project
</SelectItem>
{projects
@@ -391,7 +383,6 @@ export function ConnectAIModal({
)
.map((project: Project) => (
<SelectItem
- className="text-white hover:bg-white/10"
key={project.id}
value={project.containerTag}
>
@@ -426,7 +417,7 @@ export function ConnectAIModal({
<CopyIcon className="size-4" />
</Button>
</div>
- <p className="text-xs text-white/50">
+ <p className="text-xs text-muted-foreground">
Use this URL to configure supermemory in your AI assistant
</p>
</div>
@@ -440,17 +431,9 @@ export function ConnectAIModal({
<SelectTrigger className="w-full">
<SelectValue placeholder="Select project" />
</SelectTrigger>
- <SelectContent className="bg-black/90 backdrop-blur-xl border-white/10">
- <SelectItem
- className="text-white hover:bg-white/10"
- value="none"
- >
- Auto-select project
- </SelectItem>
- <SelectItem
- className="text-white hover:bg-white/10"
- value="sm_project_default"
- >
+ <SelectContent>
+ <SelectItem value="none">Auto-select project</SelectItem>
+ <SelectItem value="sm_project_default">
Default Project
</SelectItem>
{projects
@@ -460,7 +443,6 @@ export function ConnectAIModal({
)
.map((project: Project) => (
<SelectItem
- className="text-white hover:bg-white/10"
key={project.id}
value={project.containerTag}
>
@@ -480,7 +462,7 @@ export function ConnectAIModal({
(selectedClient !== "cursor" || cursorInstallTab === "manual") && (
<div className="space-y-4">
<div className="flex items-center gap-3">
- <div className="w-8 h-8 rounded-full bg-white/10 text-white/60 flex items-center justify-center text-sm font-medium">
+ <div className="w-8 h-8 rounded-full bg-muted text-muted-foreground flex items-center justify-center text-sm font-medium">
3
</div>
<h3 className="text-sm font-medium">
@@ -506,7 +488,7 @@ export function ConnectAIModal({
</Button>
</div>
- <p className="text-xs text-white/50">
+ <p className="text-xs text-muted-foreground">
{selectedClient === "cursor" && cursorInstallTab === "manual"
? "Copy and run this command in your terminal for manual installation (or switch to the one-click option above)"
: "Copy and run this command in your terminal to install the MCP server"}
@@ -518,19 +500,19 @@ export function ConnectAIModal({
{!selectedClient && (
<div className="space-y-4">
<div className="flex items-center gap-3">
- <div className="w-8 h-8 rounded-full bg-white/10 text-white/60 flex items-center justify-center text-sm font-medium">
+ <div className="w-8 h-8 rounded-full bg-muted text-muted-foreground flex items-center justify-center text-sm font-medium">
3
</div>
<h3 className="text-sm font-medium">Installation Command</h3>
</div>
<div className="relative">
- <div className="w-full h-10 bg-white/5 border border-white/10 rounded-md flex items-center px-3">
- <div className="w-full h-4 bg-white/20 rounded animate-pulse blur-sm" />
+ <div className="w-full h-10 bg-muted border border-border rounded-md flex items-center px-3">
+ <div className="w-full h-4 bg-muted-foreground/20 rounded animate-pulse blur-sm" />
</div>
</div>
- <p className="text-xs text-white/30">
+ <p className="text-xs text-muted-foreground/50">
Select a client above to see the installation command
</p>
</div>
@@ -539,18 +521,18 @@ export function ConnectAIModal({
<div className="gap-2 hidden">
<div>
<label
- className="text-sm font-medium text-white/80 block mb-2"
+ className="text-sm font-medium text-foreground/80 block mb-2"
htmlFor="mcp-server-url-desktop"
>
MCP Server URL
</label>
- <p className="text-xs text-white/50 mt-2">
+ <p className="text-xs text-muted-foreground mt-2">
Use this URL to configure supermemory in your AI assistant
</p>
</div>
- <div className="p-1 bg-white/5 rounded-lg border border-white/10 items-center flex px-2">
+ <div className="p-1 bg-muted rounded-lg border border-border items-center flex px-2">
<CopyableCell
- className="font-mono text-xs text-blue-400"
+ className="font-mono text-xs text-primary"
value="https://api.supermemory.ai/mcp"
/>
</div>
@@ -599,11 +581,11 @@ export function ConnectAIModal({
onOpenChange={setIsMigrateDialogOpen}
open={isMigrateDialogOpen}
>
- <DialogContent className="sm:max-w-2xl bg-black/90 backdrop-blur-xl border-white/10 text-white">
+ <DialogContent className="sm:max-w-2xl bg-popover border-border text-popover-foreground">
<div>
<DialogHeader>
<DialogTitle>Migrate from MCP v1</DialogTitle>
- <DialogDescription className="text-white/60">
+ <DialogDescription className="text-muted-foreground">
Migrate your MCP documents from the legacy system.
</DialogDescription>
</DialogHeader>
@@ -623,7 +605,7 @@ export function ConnectAIModal({
{({ state, handleChange, handleBlur }) => (
<>
<Input
- className="bg-white/5 border-white/10 text-white"
+ className="bg-input border-border text-foreground"
id="mcpUrl"
onBlur={handleBlur}
onChange={(e) => handleChange(e.target.value)}
@@ -631,14 +613,14 @@ export function ConnectAIModal({
value={state.value}
/>
{state.meta.errors.length > 0 && (
- <p className="text-sm text-red-400 mt-1">
+ <p className="text-sm text-destructive mt-1">
{state.meta.errors.join(", ")}
</p>
)}
</>
)}
</mcpMigrationForm.Field>
- <p className="text-xs text-white/50">
+ <p className="text-xs text-muted-foreground">
Enter your old MCP Link in the format: <br />
<span className="font-mono">
https://mcp.supermemory.ai/userId/sse
@@ -648,7 +630,6 @@ export function ConnectAIModal({
</div>
<div className="flex justify-end gap-3 mt-4">
<Button
- className="bg-white/5 hover:bg-white/10 border-white/10 text-white"
onClick={() => {
setIsMigrateDialogOpen(false)
mcpMigrationForm.reset()
@@ -659,7 +640,6 @@ export function ConnectAIModal({
Cancel
</Button>
<Button
- className="bg-white/10 hover:bg-white/20 text-white border-white/20"
disabled={
migrateMCPMutation.isPending ||
!mcpMigrationForm.state.canSubmit
diff --git a/apps/web/components/content-cards/google-docs.tsx b/apps/web/components/content-cards/google-docs.tsx
new file mode 100644
index 00000000..22f06f77
--- /dev/null
+++ b/apps/web/components/content-cards/google-docs.tsx
@@ -0,0 +1,164 @@
+"use client"
+
+import { Card, CardContent } from "@repo/ui/components/card"
+import { Badge } from "@repo/ui/components/badge"
+import { ExternalLink, FileText, Brain } from "lucide-react"
+import { useState } from "react"
+import { cn } from "@lib/utils"
+import { colors } from "@repo/ui/memory-graph/constants"
+import { getPastelBackgroundColor } from "../memories-utils"
+
+interface GoogleDocsCardProps {
+ title: string
+ url: string | null | undefined
+ description?: string | null
+ className?: string
+ onClick?: () => void
+ showExternalLink?: boolean
+ activeMemories?: Array<{ id: string; isForgotten?: boolean }>
+ lastModified?: string | Date
+}
+
+export const GoogleDocsCard = ({
+ title,
+ url,
+ description,
+ className,
+ onClick,
+ showExternalLink = true,
+ activeMemories,
+ lastModified,
+}: GoogleDocsCardProps) => {
+ const [imageError, setImageError] = useState(false)
+
+ const handleCardClick = () => {
+ if (onClick) {
+ onClick()
+ } else if (url) {
+ window.open(url, "_blank", "noopener,noreferrer")
+ }
+ }
+
+ const handleExternalLinkClick = (e: React.MouseEvent) => {
+ e.stopPropagation()
+ if (url) {
+ window.open(url, "_blank", "noopener,noreferrer")
+ }
+ }
+
+ return (
+ <Card
+ className={cn(
+ "cursor-pointer transition-all hover:shadow-md group overflow-hidden relative py-4",
+ className,
+ )}
+ onClick={handleCardClick}
+ style={{
+ backgroundColor: getPastelBackgroundColor(url || title || "googledocs"),
+ }}
+ >
+ <CardContent className="p-0">
+ <div className="px-4 border-b border-white/10">
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-2">
+ <svg
+ className="w-4 h-4"
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 87.3 78"
+ aria-label="Google Docs"
+ >
+ <title>Google Docs</title>
+ <path
+ fill="#0066da"
+ d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3L27.5 53H0c0 1.55.4 3.1 1.2 4.5z"
+ />
+ <path
+ fill="#00ac47"
+ d="M43.65 25 29.9 1.2c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44A9.06 9.06 0 0 0 0 53h27.5z"
+ />
+ <path
+ fill="#ea4335"
+ d="M73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75L86.1 57.5c.8-1.4 1.2-2.95 1.2-4.5H59.798l5.852 11.5z"
+ />
+ <path
+ fill="#00832d"
+ d="M43.65 25 57.4 1.2C56.05.4 54.5 0 52.9 0H34.4c-1.6 0-3.15.45-4.5 1.2z"
+ />
+ <path
+ fill="#2684fc"
+ d="M59.8 53H27.5L13.75 76.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z"
+ />
+ <path
+ fill="#ffba00"
+ d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3L43.65 25 59.8 53h27.45c0-1.55-.4-3.1-1.2-4.5z"
+ />
+ </svg>
+ <div className="flex flex-col">
+ <span className="text-xs text-muted-foreground">
+ Google Docs
+ </span>
+ </div>
+ </div>
+ {showExternalLink && (
+ <button
+ onClick={handleExternalLinkClick}
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded hover:bg-white/10 flex-shrink-0"
+ type="button"
+ aria-label="Open in Google Docs"
+ >
+ <ExternalLink className="w-4 h-4" />
+ </button>
+ )}
+ </div>
+ </div>
+
+ <div className="px-4 space-y-2">
+ <div className="flex items-start justify-between gap-2">
+ <h3 className="font-semibold text-sm line-clamp-2 leading-tight flex-1">
+ {title || "Untitled Document"}
+ </h3>
+ </div>
+
+ {description && (
+ <p className="text-xs text-muted-foreground line-clamp-3 leading-relaxed">
+ {description}
+ </p>
+ )}
+
+ <div className="flex items-center justify-between text-xs text-muted-foreground">
+ <div className="flex items-center gap-1">
+ <FileText className="w-3 h-3" />
+ <span>Google Workspace</span>
+ </div>
+ {lastModified && (
+ <span className="truncate">
+ Modified{" "}
+ {lastModified instanceof Date
+ ? lastModified.toLocaleDateString()
+ : new Date(lastModified).toLocaleDateString()}
+ </span>
+ )}
+ </div>
+
+ {activeMemories && activeMemories.length > 0 && (
+ <div>
+ <Badge
+ className="text-xs text-accent-foreground"
+ style={{
+ backgroundColor: colors.memory.secondary,
+ }}
+ variant="secondary"
+ >
+ <Brain className="w-3 h-3 mr-1" />
+ {activeMemories.length}{" "}
+ {activeMemories.length === 1 ? "memory" : "memories"}
+ </Badge>
+ </div>
+ )}
+ </div>
+ </CardContent>
+ </Card>
+ )
+}
+
+GoogleDocsCard.displayName = "GoogleDocsCard"
diff --git a/apps/web/components/content-cards/note.tsx b/apps/web/components/content-cards/note.tsx
new file mode 100644
index 00000000..e7703d9b
--- /dev/null
+++ b/apps/web/components/content-cards/note.tsx
@@ -0,0 +1,133 @@
+import { Badge } from "@repo/ui/components/badge"
+import { Card, CardContent, CardHeader } from "@repo/ui/components/card"
+
+import { colors } from "@repo/ui/memory-graph/constants"
+import { Brain, ExternalLink } from "lucide-react"
+import { cn } from "@lib/utils"
+import {
+ formatDate,
+ getPastelBackgroundColor,
+ getSourceUrl,
+} from "../memories-utils"
+import { MCPIcon } from "../menu"
+import { analytics } from "@/lib/analytics"
+import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
+import type { z } from "zod"
+
+type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
+type DocumentWithMemories = DocumentsResponse["documents"][0]
+
+interface NoteCardProps {
+ document: DocumentWithMemories
+ width: number
+ activeMemories: Array<{ id: string; isForgotten?: boolean }>
+ forgottenMemories: Array<{ id: string; isForgotten?: boolean }>
+ onOpenDetails: (document: DocumentWithMemories) => void
+ onDelete: (document: DocumentWithMemories) => void
+}
+
+export const NoteCard = ({
+ document,
+ width,
+ activeMemories,
+ forgottenMemories,
+ onOpenDetails,
+}: NoteCardProps) => {
+ return (
+ <Card
+ className="w-full p-4 transition-all cursor-pointer group relative overflow-hidden gap-2 shadow-xs"
+ onClick={() => {
+ analytics.documentCardClicked()
+ onOpenDetails(document)
+ }}
+ style={{
+ backgroundColor: getPastelBackgroundColor(
+ document.id || document.title || "note",
+ ),
+ width: width,
+ }}
+ >
+ <CardHeader className="relative z-10 px-0 pb-0">
+ <div className="flex items-center justify-between gap-2">
+ <div className="flex items-center gap-1">
+ <p
+ className={cn(
+ "text-sm font-medium line-clamp-1",
+ document.url ? "max-w-[190px]" : "max-w-[200px]",
+ )}
+ >
+ {document.title || "Untitled Document"}
+ </p>
+ </div>
+ {document.url && (
+ <button
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded"
+ onClick={(e) => {
+ e.stopPropagation()
+ const sourceUrl = getSourceUrl(document)
+ window.open(sourceUrl ?? undefined, "_blank")
+ }}
+ style={{
+ backgroundColor: "rgba(255, 255, 255, 0.05)",
+ color: colors.text.secondary,
+ }}
+ type="button"
+ >
+ <ExternalLink className="w-3 h-3" />
+ </button>
+ )}
+ <div className="flex items-center gap-2 text-[10px] text-muted-foreground">
+ <span>{formatDate(document.createdAt)}</span>
+ </div>
+ </div>
+ </CardHeader>
+ <CardContent className="relative z-10 px-0">
+ {document.content && (
+ <p
+ className="text-xs line-clamp-6"
+ style={{ color: colors.text.muted }}
+ >
+ {document.content}
+ </p>
+ )}
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-2 flex-wrap">
+ {activeMemories.length > 0 && (
+ <Badge
+ className="text-xs text-accent-foreground mt-2"
+ style={{
+ backgroundColor: colors.memory.secondary,
+ }}
+ variant="secondary"
+ >
+ <Brain className="w-3 h-3 mr-1" />
+ {activeMemories.length}{" "}
+ {activeMemories.length === 1 ? "memory" : "memories"}
+ </Badge>
+ )}
+ {forgottenMemories.length > 0 && (
+ <Badge
+ className="text-xs mt-2"
+ style={{
+ borderColor: "rgba(255, 255, 255, 0.2)",
+ color: colors.text.muted,
+ }}
+ variant="outline"
+ >
+ {forgottenMemories.length} forgotten
+ </Badge>
+ )}
+ {document.source === "mcp" && (
+ <Badge variant="outline" className="mt-2">
+ <MCPIcon className="w-3 h-3 mr-1" />
+ MCP
+ </Badge>
+ )}
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+ )
+}
+
+NoteCard.displayName = "NoteCard"
diff --git a/apps/web/components/content-cards/tweet.tsx b/apps/web/components/content-cards/tweet.tsx
new file mode 100644
index 00000000..3f46d6cc
--- /dev/null
+++ b/apps/web/components/content-cards/tweet.tsx
@@ -0,0 +1,105 @@
+import { Suspense } from "react"
+import type { Tweet } from "react-tweet/api"
+import {
+ type TwitterComponents,
+ TweetContainer,
+ TweetHeader,
+ TweetInReplyTo,
+ TweetBody,
+ TweetMedia,
+ TweetInfo,
+ QuotedTweet,
+ TweetNotFound,
+ TweetSkeleton,
+ enrichTweet,
+} from "react-tweet"
+import { Badge } from "@repo/ui/components/badge"
+import { Brain } from "lucide-react"
+import { colors } from "@repo/ui/memory-graph/constants"
+import { getPastelBackgroundColor } from "../memories-utils"
+
+type MyTweetProps = {
+ tweet: Tweet
+ components?: TwitterComponents
+}
+
+const MyTweet = ({ tweet: t, components }: MyTweetProps) => {
+ const parsedTweet = typeof t === "string" ? JSON.parse(t) : t
+ const tweet = enrichTweet(parsedTweet)
+ return (
+ <TweetContainer className="pb-5">
+ <TweetHeader tweet={tweet} components={components} />
+ {tweet.in_reply_to_status_id_str && <TweetInReplyTo tweet={tweet} />}
+ <TweetBody tweet={tweet} />
+ {tweet.mediaDetails?.length ? (
+ <TweetMedia tweet={tweet} components={components} />
+ ) : null}
+ {tweet.quoted_tweet && <QuotedTweet tweet={tweet.quoted_tweet} />}
+ <TweetInfo tweet={tweet} />
+ </TweetContainer>
+ )
+}
+
+const TweetContent = ({
+ components,
+ tweet,
+}: {
+ components: TwitterComponents
+ tweet: Tweet
+}) => {
+ if (!tweet) {
+ const NotFound = components?.TweetNotFound || TweetNotFound
+ return <NotFound />
+ }
+
+ return <MyTweet tweet={tweet} components={components} />
+}
+
+const CustomTweet = ({
+ fallback = <TweetSkeleton />,
+ ...props
+}: {
+ components: TwitterComponents
+ tweet: Tweet
+ fallback?: React.ReactNode
+}) => (
+ <Suspense fallback={fallback}>
+ <TweetContent {...props} />
+ </Suspense>
+)
+
+export const TweetCard = ({
+ data,
+ activeMemories,
+}: {
+ data: Tweet
+ activeMemories?: Array<{ id: string; isForgotten?: boolean }>
+}) => {
+ return (
+ <div
+ className="relative transition-all"
+ style={{
+ backgroundColor: getPastelBackgroundColor(data.id_str || "tweet"),
+ }}
+ >
+ <CustomTweet components={{}} tweet={data} />
+ {activeMemories && activeMemories.length > 0 && (
+ <div className="absolute bottom-2 left-4 z-10">
+ <Badge
+ className="text-xs text-accent-foreground"
+ style={{
+ backgroundColor: colors.memory.secondary,
+ }}
+ variant="secondary"
+ >
+ <Brain className="w-3 h-3 mr-1" />
+ {activeMemories.length}{" "}
+ {activeMemories.length === 1 ? "memory" : "memories"}
+ </Badge>
+ </div>
+ )}
+ </div>
+ )
+}
+
+TweetCard.displayName = "TweetCard"
diff --git a/apps/web/components/content-cards/website.tsx b/apps/web/components/content-cards/website.tsx
new file mode 100644
index 00000000..f36cd247
--- /dev/null
+++ b/apps/web/components/content-cards/website.tsx
@@ -0,0 +1,103 @@
+"use client"
+
+import { Card, CardContent } from "@repo/ui/components/card"
+import { ExternalLink } from "lucide-react"
+import { useState } from "react"
+import { cn } from "@lib/utils"
+import { getPastelBackgroundColor } from "../memories-utils"
+
+interface WebsiteCardProps {
+ title: string
+ url: string
+ image?: string
+ description?: string
+ className?: string
+ onClick?: () => void
+ showExternalLink?: boolean
+}
+
+export const WebsiteCard = ({
+ title,
+ url,
+ image,
+ description,
+ className,
+ onClick,
+ showExternalLink = true,
+}: WebsiteCardProps) => {
+ const [imageError, setImageError] = useState(false)
+
+ const handleCardClick = () => {
+ if (onClick) {
+ onClick()
+ } else {
+ window.open(url, "_blank", "noopener,noreferrer")
+ }
+ }
+
+ const handleExternalLinkClick = (e: React.MouseEvent) => {
+ e.stopPropagation()
+ window.open(url, "_blank", "noopener,noreferrer")
+ }
+
+ const hostname = (() => {
+ try {
+ return new URL(url).hostname
+ } catch {
+ return url
+ }
+ })()
+
+ return (
+ <Card
+ className={cn(
+ "cursor-pointer transition-all hover:shadow-md group overflow-hidden py-0",
+ className,
+ )}
+ onClick={handleCardClick}
+ style={{
+ backgroundColor: getPastelBackgroundColor(url || title || "website"),
+ }}
+ >
+ <CardContent className="p-0">
+ {image && !imageError && (
+ <div className="relative h-38 bg-gray-100 overflow-hidden">
+ <img
+ src={image}
+ alt={title || "Website preview"}
+ className="w-full h-full object-cover transition-transform group-hover:scale-105"
+ onError={() => setImageError(true)}
+ loading="lazy"
+ />
+ </div>
+ )}
+
+ <div className="px-4 py-2 space-y-2">
+ <div className="font-semibold text-sm line-clamp-2 leading-tight flex items-center justify-between">
+ {title}
+ {showExternalLink && (
+ <button
+ onClick={handleExternalLinkClick}
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded hover:bg-gray-100 flex-shrink-0"
+ type="button"
+ aria-label="Open in new tab"
+ >
+ <ExternalLink className="w-3 h-3" />
+ </button>
+ )}
+ </div>
+
+ {description && (
+ <p className="text-xs text-muted-foreground line-clamp-2 leading-relaxed">
+ {description}
+ </p>
+ )}
+
+ <p className="text-xs text-muted-foreground truncate">{hostname}</p>
+ </div>
+ </CardContent>
+ </Card>
+ )
+}
+
+WebsiteCard.displayName = "WebsiteCard"
diff --git a/apps/web/components/header.tsx b/apps/web/components/header.tsx
new file mode 100644
index 00000000..b51b4b84
--- /dev/null
+++ b/apps/web/components/header.tsx
@@ -0,0 +1,175 @@
+import { Button } from "@ui/components/button"
+import { Logo, LogoFull } from "@ui/assets/Logo"
+import Link from "next/link"
+import { MoonIcon, Plus, SunIcon, MonitorIcon, Network } from "lucide-react"
+import {
+ DropdownMenuContent,
+ DropdownMenuTrigger,
+ DropdownMenuSeparator,
+ DropdownMenuLabel,
+} from "@ui/components/dropdown-menu"
+import { DropdownMenuItem } from "@ui/components/dropdown-menu"
+import { DropdownMenu } from "@ui/components/dropdown-menu"
+import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar"
+import { useAuth } from "@lib/auth-context"
+import { ConnectAIModal } from "./connect-ai-modal"
+import { useTheme } from "next-themes"
+import { cn } from "@lib/utils"
+import { useRouter } from "next/navigation"
+import { MCPIcon } from "./menu"
+import { authClient } from "@lib/auth"
+import { analytics } from "@/lib/analytics"
+import { useGraphModal } from "@/stores"
+
+export function Header({ onAddMemory }: { onAddMemory?: () => void }) {
+ const { user } = useAuth()
+ const { theme, setTheme } = useTheme()
+ const router = useRouter()
+ const { setIsOpen: setGraphModalOpen } = useGraphModal()
+
+ const handleSignOut = () => {
+ analytics.userSignedOut()
+ authClient.signOut()
+ router.push("/login")
+ }
+
+ return (
+ <div className="flex items-center justify-between w-full p-3 md:p-4">
+ <div className="flex items-center gap-2 md:gap-3 justify-between w-full">
+ <Link
+ className="pointer-events-auto"
+ href={
+ process.env.NODE_ENV === "development"
+ ? "http://localhost:3000"
+ : "https://app.supermemory.ai"
+ }
+ rel="noopener noreferrer"
+ >
+ <LogoFull className="h-8 hidden md:block" />
+ <Logo className="h-8 md:hidden" />
+ </Link>
+
+ <div className="flex items-center gap-1.5 md:gap-3">
+ <Button
+ variant="secondary"
+ size="sm"
+ onClick={onAddMemory}
+ className="gap-1.5"
+ >
+ <Plus className="h-4 w-4" />
+ <span className="hidden sm:inline">Add Memory</span>
+ <span className="hidden md:inline bg-secondary-foreground/10 rounded-md px-2 py-[2px] text-xs">
+ c
+ </span>
+ </Button>
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => setGraphModalOpen(true)}
+ className="gap-1.5"
+ >
+ <Network className="h-4 w-4" />
+ <span className="hidden sm:inline">Graph View</span>
+ </Button>
+ <ConnectAIModal>
+ <Button variant="ghost" size="sm" className="gap-1.5">
+ <MCPIcon className="h-4 w-4" />
+ <span className="hidden lg:inline">Connect to AI (MCP)</span>
+ </Button>
+ </ConnectAIModal>
+ <DropdownMenu>
+ <DropdownMenuTrigger>
+ <Avatar className="border border-border h-8 w-8 md:h-10 md:w-10">
+ <AvatarImage src={user?.image ?? ""} />
+ <AvatarFallback>{user?.name?.charAt(0)}</AvatarFallback>
+ </Avatar>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent className="mr-2 md:mr-4 px-2 w-56">
+ <DropdownMenuLabel>
+ <div>
+ <p className="text-sm font-medium">{user?.name}</p>
+ <p className="text-xs text-muted-foreground">{user?.email}</p>
+ </div>
+ </DropdownMenuLabel>
+ <DropdownMenuSeparator />
+ <DropdownMenuItem onClick={() => router.push("/settings")}>
+ Profile
+ </DropdownMenuItem>
+ <DropdownMenuItem
+ onClick={() => router.push("/settings/billing")}
+ >
+ Billing
+ </DropdownMenuItem>
+ <DropdownMenuItem
+ className="flex items-center justify-between p-2 cursor-default hover:bg-transparent focus:bg-transparent data-[highlighted]:bg-transparent"
+ onSelect={(e) => e.preventDefault()}
+ >
+ <span className="text-sm font-medium">Theme</span>
+ <div className="flex items-center gap-1 bg-accent rounded-full">
+ <Button
+ variant={theme === "system" ? "default" : "ghost"}
+ size="sm"
+ className={cn(
+ "h-6 w-6 rounded-full group hover:cursor-pointer",
+ )}
+ onClick={() => setTheme("system")}
+ title="System"
+ >
+ <MonitorIcon
+ className={cn(
+ theme === "system"
+ ? "text-primay-foreground"
+ : "text-muted-foreground",
+ "h-3 w-3 group-hover:text-foreground",
+ )}
+ />
+ </Button>
+ <Button
+ variant={theme === "light" ? "default" : "ghost"}
+ size="sm"
+ className={cn(
+ "h-6 w-6 rounded-full group hover:cursor-pointer",
+ )}
+ onClick={() => setTheme("light")}
+ title="Light"
+ >
+ <SunIcon
+ className={cn(
+ theme === "light"
+ ? "text-primay-foreground"
+ : "text-muted-foreground",
+ "h-3 w-3 group-hover:text-foreground",
+ )}
+ />
+ </Button>
+ <Button
+ variant={theme === "dark" ? "default" : "ghost"}
+ size="sm"
+ className={cn(
+ "h-6 w-6 rounded-full group hover:cursor-pointer",
+ )}
+ onClick={() => setTheme("dark")}
+ title="Dark"
+ >
+ <MoonIcon
+ className={cn(
+ theme === "dark"
+ ? "text-primay-foreground"
+ : "text-muted-foreground",
+ "h-3 w-3 group-hover:text-foreground",
+ )}
+ />
+ </Button>
+ </div>
+ </DropdownMenuItem>
+ <DropdownMenuSeparator />
+ <DropdownMenuItem onClick={() => handleSignOut()}>
+ Logout
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
+ </div>
+ </div>
+ </div>
+ )
+}
diff --git a/apps/web/components/masonry-memory-list.tsx b/apps/web/components/masonry-memory-list.tsx
new file mode 100644
index 00000000..2f634f74
--- /dev/null
+++ b/apps/web/components/masonry-memory-list.tsx
@@ -0,0 +1,269 @@
+"use client"
+
+import { useIsMobile } from "@hooks/use-mobile"
+import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
+import { colors } from "@repo/ui/memory-graph/constants"
+import { Sparkles } from "lucide-react"
+import { Masonry, useInfiniteLoader } from "masonic"
+import { memo, useCallback, useMemo, useState } from "react"
+import type { z } from "zod"
+import { analytics } from "@/lib/analytics"
+import { useDeleteDocument } from "@lib/queries"
+import { useProject } from "@/stores"
+
+import { MemoryDetail } from "./memories-utils/memory-detail"
+import { TweetCard } from "./content-cards/tweet"
+import { WebsiteCard } from "./content-cards/website"
+import { NoteCard } from "./content-cards/note"
+import { GoogleDocsCard } from "./content-cards/google-docs"
+import type { Tweet } from "react-tweet/api"
+
+type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
+type DocumentWithMemories = DocumentsResponse["documents"][0]
+
+interface MasonryMemoryListProps {
+ children?: React.ReactNode
+ documents: DocumentWithMemories[]
+ isLoading: boolean
+ isLoadingMore: boolean
+ error: Error | null
+ totalLoaded: number
+ hasMore: boolean
+ loadMoreDocuments: () => Promise<void>
+}
+
+const DocumentCard = memo(
+ ({
+ index: _index,
+ data: document,
+ width,
+ onOpenDetails,
+ onDelete,
+ }: {
+ index: number
+ data: DocumentWithMemories & { ogImage?: string }
+ width: number
+ onOpenDetails: (document: DocumentWithMemories) => void
+ onDelete: (document: DocumentWithMemories) => void
+ }) => {
+ const activeMemories = document.memoryEntries.filter((m) => !m.isForgotten)
+ const forgottenMemories = document.memoryEntries.filter(
+ (m) => m.isForgotten,
+ )
+
+ if (
+ document.url?.includes("https://docs.googleapis.com/v1/documents") ||
+ document.url?.includes("docs.google.com/document") ||
+ document.type === "google_doc"
+ ) {
+ return (
+ <GoogleDocsCard
+ url={document.url}
+ title={document.title || "Untitled Document"}
+ description={document.content}
+ activeMemories={activeMemories}
+ lastModified={document.updatedAt || document.createdAt}
+ />
+ )
+ }
+
+ if (
+ document.url?.includes("x.com/") &&
+ document.metadata?.sm_internal_twitter_metadata
+ ) {
+ return (
+ <TweetCard
+ data={
+ document.metadata?.sm_internal_twitter_metadata as unknown as Tweet
+ }
+ activeMemories={activeMemories}
+ />
+ )
+ }
+
+ if (document.url?.includes("https://")) {
+ return (
+ <WebsiteCard
+ url={document.url}
+ title={document.title || "Untitled Document"}
+ image={document.ogImage}
+ />
+ )
+ }
+
+ return (
+ <NoteCard
+ document={document}
+ width={width}
+ activeMemories={activeMemories}
+ forgottenMemories={forgottenMemories}
+ onOpenDetails={onOpenDetails}
+ onDelete={onDelete}
+ />
+ )
+ },
+)
+
+DocumentCard.displayName = "DocumentCard"
+
+export const MasonryMemoryList = ({
+ children,
+ documents,
+ isLoading,
+ isLoadingMore,
+ error,
+ hasMore,
+ loadMoreDocuments,
+}: MasonryMemoryListProps) => {
+ const [selectedSpace, _] = useState<string>("all")
+ const [selectedDocument, setSelectedDocument] =
+ useState<DocumentWithMemories | null>(null)
+ const [isDetailOpen, setIsDetailOpen] = useState(false)
+ const isMobile = useIsMobile()
+ const { selectedProject } = useProject()
+ const deleteDocumentMutation = useDeleteDocument(selectedProject)
+
+ const handleDeleteDocument = useCallback(
+ (document: DocumentWithMemories) => {
+ deleteDocumentMutation.mutate(document.id)
+ },
+ [deleteDocumentMutation],
+ )
+
+ // Filter documents based on selected space
+ const filteredDocuments = useMemo(() => {
+ if (!documents) return []
+
+ if (selectedSpace === "all") {
+ return documents
+ }
+
+ return documents
+ .map((doc) => ({
+ ...doc,
+ memoryEntries: doc.memoryEntries.filter(
+ (memory) =>
+ (memory.spaceContainerTag ?? memory.spaceId) === selectedSpace,
+ ),
+ }))
+ .filter((doc) => doc.memoryEntries.length > 0)
+ }, [documents, selectedSpace])
+
+ const handleOpenDetails = useCallback((document: DocumentWithMemories) => {
+ analytics.memoryDetailOpened()
+ setSelectedDocument(document)
+ setIsDetailOpen(true)
+ }, [])
+
+ const handleCloseDetails = useCallback(() => {
+ setIsDetailOpen(false)
+ setTimeout(() => setSelectedDocument(null), 300)
+ }, [])
+
+ // Infinite loading with Masonic
+ const maybeLoadMore = useInfiniteLoader(
+ async (_startIndex, _stopIndex, _currentItems) => {
+ if (hasMore && !isLoadingMore) {
+ await loadMoreDocuments()
+ }
+ },
+ {
+ isItemLoaded: (index, items) => !!items[index],
+ minimumBatchSize: 10,
+ threshold: 5,
+ },
+ )
+
+ const renderDocumentCard = useCallback(
+ ({
+ index,
+ data,
+ width,
+ }: {
+ index: number
+ data: DocumentWithMemories
+ width: number
+ }) => (
+ <DocumentCard
+ index={index}
+ data={data}
+ width={width}
+ onOpenDetails={handleOpenDetails}
+ onDelete={handleDeleteDocument}
+ />
+ ),
+ [handleOpenDetails, handleDeleteDocument],
+ )
+
+ return (
+ <>
+ <div className="h-full relative pt-10">
+ {error ? (
+ <div className="h-full flex items-center justify-center p-4">
+ <div className="rounded-xl overflow-hidden">
+ <div
+ className="relative z-10 px-6 py-4"
+ style={{ color: colors.text.primary }}
+ >
+ Error loading documents: {error.message}
+ </div>
+ </div>
+ </div>
+ ) : isLoading ? (
+ <div className="h-full flex items-center justify-center p-4">
+ <div className="rounded-xl overflow-hidden">
+ <div
+ className="relative z-10 px-6 py-4"
+ >
+ <div className="flex items-center gap-2">
+ <Sparkles className="w-4 h-4 animate-spin text-blue-400" />
+ <span>Loading memory list...</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ ) : filteredDocuments.length === 0 && !isLoading ? (
+ <div className="h-full flex items-center justify-center p-4">
+ {children}
+ </div>
+ ) : (
+ <div
+ className="h-full overflow-auto custom-scrollbar sm-tweet-theme"
+ data-theme="light"
+ >
+ <Masonry
+ items={filteredDocuments}
+ render={renderDocumentCard}
+ columnGutter={16}
+ rowGutter={16}
+ columnWidth={280}
+ maxColumnCount={isMobile ? 1 : undefined}
+ itemHeightEstimate={200}
+ overscanBy={3}
+ onRender={maybeLoadMore}
+ className="px-4"
+ />
+
+ {isLoadingMore && (
+ <div className="py-8 flex items-center justify-center">
+ <div className="flex items-center gap-2">
+ <Sparkles className="w-4 h-4 animate-spin text-blue-400" />
+ <span style={{ color: colors.text.primary }}>
+ Loading more memories...
+ </span>
+ </div>
+ </div>
+ )}
+ </div>
+ )}
+ </div>
+
+ <MemoryDetail
+ document={selectedDocument}
+ isOpen={isDetailOpen}
+ onClose={handleCloseDetails}
+ isMobile={isMobile}
+ />
+ </>
+ )
+}
diff --git a/apps/web/components/memories-utils/html-content-renderer.tsx b/apps/web/components/memories-utils/html-content-renderer.tsx
new file mode 100644
index 00000000..6231f5eb
--- /dev/null
+++ b/apps/web/components/memories-utils/html-content-renderer.tsx
@@ -0,0 +1,62 @@
+import { memo, useMemo } from "react"
+import DOMPurify from "dompurify"
+
+interface HTMLContentRendererProps {
+ content: string
+ className?: string
+}
+
+/**
+ * Detects if content is likely HTML based on common HTML patterns
+ */
+const isHTMLContent = (content: string): boolean => {
+ // Check for HTML tags, entities, and DOCTYPE
+ const htmlPatterns = [
+ /<[a-z][\s\S]*>/i, // HTML tags
+ /&[a-z]+;/i, // HTML entities
+ /<!doctype\s+html/i, // DOCTYPE declaration
+ /<\/[a-z]+>/i, // Closing tags
+ ]
+
+ return htmlPatterns.some((pattern) => pattern.test(content))
+}
+
+export const HTMLContentRenderer = memo(
+ ({ content, className = "" }: HTMLContentRendererProps) => {
+ const { isHTML, processedContent } = useMemo(() => {
+ const contentIsHTML = isHTMLContent(content)
+
+ if (contentIsHTML) {
+ return {
+ isHTML: true,
+ processedContent: DOMPurify.sanitize(content),
+ }
+ }
+
+ return {
+ isHTML: false,
+ processedContent: content,
+ }
+ }, [content])
+
+ if (isHTML) {
+ return (
+ <div
+ className={`${className} bg-background`}
+ // biome-ignore lint/security/noDangerouslySetInnerHtml: Content is sanitized with DOMPurify
+ dangerouslySetInnerHTML={{ __html: processedContent }}
+ />
+ )
+ }
+
+ return (
+ <p
+ className={`text-sm leading-relaxed whitespace-pre-wrap text-foreground ${className}`}
+ >
+ {processedContent}
+ </p>
+ )
+ },
+)
+
+HTMLContentRenderer.displayName = "HTMLContentRenderer"
diff --git a/apps/web/components/memories-utils/index.tsx b/apps/web/components/memories-utils/index.tsx
new file mode 100644
index 00000000..3052f3c7
--- /dev/null
+++ b/apps/web/components/memories-utils/index.tsx
@@ -0,0 +1,109 @@
+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] || "th")
+ }
+
+ 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
+}
+
+// Simple hash function for consistent color generation
+const hashString = (str: string): number => {
+ let hash = 0
+ for (let i = 0; i < str.length; i++) {
+ const char = str.charCodeAt(i)
+ hash = (hash << 5) - hash + char
+ hash = hash & hash // Convert to 32-bit integer
+ }
+ return Math.abs(hash)
+}
+
+// Generate consistent pastel background color based on document ID
+export const getPastelBackgroundColor = (
+ documentId: string | undefined | null,
+): string => {
+ // Handle null/undefined cases
+ if (!documentId) {
+ return "rgba(255, 255, 255, 0.06)" // Default fallback color
+ }
+
+ const hash = hashString(documentId)
+
+ // Define pastel color palette with good contrast against dark backgrounds
+ const pastelColors = [
+ // Soft pinks and roses
+ "rgba(255, 182, 193, 0.08)", // Light pink
+ "rgba(255, 218, 221, 0.08)", // Misty rose
+ "rgba(255, 192, 203, 0.08)", // Pink
+
+ // Soft blues and purples
+ "rgba(173, 216, 230, 0.08)", // Light blue
+ "rgba(221, 160, 221, 0.08)", // Plum
+ "rgba(218, 112, 214, 0.08)", // Orchid
+ "rgba(147, 197, 253, 0.08)", // Sky blue
+
+ // Soft greens
+ "rgba(152, 251, 152, 0.08)", // Pale green
+ "rgba(175, 238, 238, 0.08)", // Pale turquoise
+ "rgba(144, 238, 144, 0.08)", // Light green
+
+ // Soft oranges and yellows
+ "rgba(255, 218, 185, 0.08)", // Peach puff
+ "rgba(255, 239, 213, 0.08)", // Papaya whip
+ "rgba(255, 228, 196, 0.08)", // Bisque
+
+ // Soft corals and salmons
+ "rgba(250, 128, 114, 0.08)", // Salmon
+ "rgba(255, 127, 80, 0.08)", // Coral
+ "rgba(255, 160, 122, 0.08)", // Light salmon
+ ]
+
+ // Use hash to consistently pick a color
+ const colorIndex = hash % pastelColors.length
+ return pastelColors[colorIndex] || "rgba(255, 255, 255, 0.06)"
+}
diff --git a/apps/web/components/memories-utils/memory-detail.tsx b/apps/web/components/memories-utils/memory-detail.tsx
new file mode 100644
index 00000000..8f238731
--- /dev/null
+++ b/apps/web/components/memories-utils/memory-detail.tsx
@@ -0,0 +1,385 @@
+import { getDocumentIcon } from "@/lib/document-icon"
+import {
+ Drawer,
+ DrawerContent,
+ DrawerHeader,
+ DrawerTitle,
+} from "@repo/ui/components/drawer"
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from "@repo/ui/components/dialog"
+import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
+import { Badge } from "@ui/components/badge"
+import {
+ Brain,
+ Calendar,
+ ChevronDown,
+ ChevronUp,
+ CircleUserRound,
+ ExternalLink,
+ List,
+ 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"
+import { HTMLContentRenderer } from "./html-content-renderer"
+
+type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
+type DocumentWithMemories = DocumentsResponse["documents"][0]
+type MemoryEntry = DocumentWithMemories["memoryEntries"][0]
+
+const formatDocumentType = (type: string) => {
+ if (type.toLowerCase() === "pdf") return "PDF"
+
+ return type
+ .split("_")
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
+ .join(" ")
+}
+
+const MemoryDetailItem = memo(({ memory }: { memory: MemoryEntry }) => {
+ return (
+ <div
+ className={`p-2.5 md:p-4 rounded-lg md:rounded-xl border transition-all relative overflow-hidden group ${
+ memory.isLatest
+ ? "bg-card shadow-sm hover:shadow-md border-primary/30"
+ : "bg-card/50 shadow-xs hover:shadow-sm border-border/60 hover:border-border"
+ }`}
+ >
+ <div className="flex items-start gap-2 md:gap-3 relative z-10">
+ <div className="flex-1 space-y-1.5 md:space-y-3">
+ <Label1Regular className="text-xs md:text-sm leading-relaxed text-left text-card-foreground">
+ {memory.memory}
+ </Label1Regular>
+ <div className="flex gap-1 md:gap-2 justify-between items-center flex-wrap">
+ <div className="flex items-center gap-2 md:gap-3 text-[10px] md:text-xs text-muted-foreground">
+ <span className="flex items-center gap-1">
+ <Calendar className="w-3 h-3 md:w-3.5 md:h-3.5" />
+ {formatDate(memory.createdAt)}
+ </span>
+ <span className="font-mono bg-muted/30 px-1 md:px-1.5 py-0.5 rounded text-[9px] md:text-[10px]">
+ v{memory.version}
+ </span>
+ {memory.sourceRelevanceScore && (
+ <span
+ className={`flex items-center gap-1 font-medium ${
+ memory.sourceRelevanceScore > 70
+ ? "text-emerald-600 dark:text-emerald-400"
+ : "text-muted-foreground"
+ }`}
+ >
+ <Sparkles className="w-3.5 h-3.5" />
+ {memory.sourceRelevanceScore}%
+ </span>
+ )}
+ </div>
+ <div className="flex items-center gap-1 md:gap-1.5 flex-wrap">
+ {memory.isForgotten && (
+ <Badge
+ className="text-[9px] md:text-[10px] h-4 md:h-5"
+ variant="destructive"
+ >
+ Forgotten
+ </Badge>
+ )}
+ {memory.isLatest && (
+ <Badge
+ className="text-[9px] md:text-[10px] h-4 md:h-5 bg-primary/15 text-primary border-primary/30"
+ variant="outline"
+ >
+ Latest
+ </Badge>
+ )}
+ {memory.forgetAfter && (
+ <Badge
+ className="text-[9px] md:text-[10px] h-4 md:h-5 text-amber-600 dark:text-amber-500 bg-amber-500/10 border-amber-500/30"
+ variant="outline"
+ >
+ <span className="hidden sm:inline">
+ Expires {formatDate(memory.forgetAfter)}
+ </span>
+ <span className="sm:hidden">Expires</span>
+ </Badge>
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+})
+
+export const MemoryDetail = memo(
+ ({
+ document,
+ isOpen,
+ onClose,
+ isMobile,
+ }: {
+ document: DocumentWithMemories | null
+ isOpen: boolean
+ onClose: () => void
+ isMobile: boolean
+ }) => {
+ if (!document) return null
+
+ const [isSummaryOpen, setIsSummaryOpen] = useState(false)
+
+ const activeMemories = document.memoryEntries.filter((m) => !m.isForgotten)
+ const forgottenMemories = document.memoryEntries.filter(
+ (m) => m.isForgotten,
+ )
+
+ const HeaderContent = ({
+ TitleComponent,
+ }: {
+ TitleComponent: typeof DialogTitle | typeof DrawerTitle
+ }) => (
+ <div className="flex items-start justify-between gap-2">
+ <div className="flex items-start gap-2 md:gap-3 flex-1 min-w-0">
+ <div className="p-1.5 md:p-2 rounded-lg bg-muted/10 flex-shrink-0">
+ {getDocumentIcon(
+ document.type,
+ "w-4 h-4 md:w-5 md:h-5 text-foreground",
+ )}
+ </div>
+ <div className="flex-1 min-w-0">
+ <TitleComponent className="text-foreground text-sm md:text-base truncate text-left">
+ {document.title || "Untitled Document"}
+ </TitleComponent>
+ <div className="flex items-center gap-1.5 md:gap-2 mt-1 text-[10px] md:text-xs text-muted-foreground flex-wrap">
+ <span>{formatDocumentType(document.type)}</span>
+ <span>•</span>
+ <span>{formatDate(document.createdAt)}</span>
+ {document.url && (
+ <>
+ <span>•</span>
+ <button
+ className="flex items-center gap-0.5 md:gap-1 transition-all hover:gap-1 md:hover:gap-2 text-primary hover:text-primary/80 whitespace-nowrap"
+ onClick={() => {
+ const sourceUrl = getSourceUrl(document)
+ window.open(sourceUrl ?? undefined, "_blank")
+ }}
+ type="button"
+ >
+ <span className="hidden sm:inline">View source</span>
+ <span className="sm:hidden">Source</span>
+ <ExternalLink className="w-2.5 h-2.5 md:w-3 md:h-3" />
+ </button>
+ </>
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+
+ const ContentDisplaySection = () => {
+ const hasContent = document.content && document.content.trim().length > 0
+
+ if (!hasContent) {
+ return (
+ <div className="text-center py-12 rounded-lg bg-muted/5">
+ <CircleUserRound className="w-12 h-12 mx-auto mb-4 opacity-30 text-muted-foreground" />
+ <p className="text-muted-foreground">
+ No content available for this document
+ </p>
+ </div>
+ )
+ }
+
+ return (
+ <div className="p-3 md:p-4 rounded-lg bg-muted/5 border border-border h-full overflow-y-auto max-w-3xl">
+ <HTMLContentRenderer content={document.content || ""} />
+ </div>
+ )
+ }
+
+ const SummaryDisplaySection = () => {
+ const hasSummary = document.summary && document.summary.trim().length > 0
+
+ if (!hasSummary) {
+ return (
+ <div className="text-center py-6 rounded-lg bg-muted/5">
+ <List className="w-6 h-6 mx-auto mb-2 opacity-30 text-muted-foreground" />
+ <p className="text-muted-foreground text-xs">
+ No summary available
+ </p>
+ </div>
+ )
+ }
+
+ return (
+ <div className="p-2.5 md:p-3 px-3 md:px-4 rounded-lg bg-primary/5 border border-primary/15">
+ <p className="text-xs md:text-sm leading-relaxed whitespace-pre-wrap text-muted-foreground">
+ {document.summary}
+ </p>
+ </div>
+ )
+ }
+
+ const MemoryContent = () => (
+ <div className="space-y-6">
+ {activeMemories.length > 0 && (
+ <div>
+ <div className="text-sm font-medium mb-2 flex items-start gap-2 py-2 text-muted-foreground">
+ Active Memories ({activeMemories.length})
+ </div>
+ <div className="space-y-3">
+ {activeMemories.map((memory) => (
+ <div key={memory.id}>
+ <MemoryDetailItem memory={memory} />
+ </div>
+ ))}
+ </div>
+ </div>
+ )}
+
+ {forgottenMemories.length > 0 && (
+ <div>
+ <div className="text-sm font-medium mb-4 px-3 py-2 rounded-lg opacity-60 text-muted-foreground bg-muted/5">
+ Forgotten Memories ({forgottenMemories.length})
+ </div>
+ <div className="space-y-3 opacity-40">
+ {forgottenMemories.map((memory) => (
+ <MemoryDetailItem key={memory.id} memory={memory} />
+ ))}
+ </div>
+ </div>
+ )}
+
+ {activeMemories.length === 0 && forgottenMemories.length === 0 && (
+ <div className="text-center py-12 rounded-lg bg-muted/5">
+ <Brain className="w-12 h-12 mx-auto mb-4 opacity-30 text-muted-foreground" />
+ <p className="text-muted-foreground">
+ No memories found for this document
+ </p>
+ </div>
+ )}
+ </div>
+ )
+
+ if (isMobile) {
+ return (
+ <Drawer onOpenChange={onClose} open={isOpen}>
+ <DrawerContent className="border-0 p-0 overflow-hidden max-h-[95vh] bg-background border-t border-border backdrop-blur-xl flex flex-col">
+ <div className="p-3 md:p-4 relative bg-muted/5 flex-shrink-0">
+ <DrawerHeader className="p-0 text-left">
+ <HeaderContent TitleComponent={DrawerTitle} />
+ </DrawerHeader>
+ </div>
+
+ <div className="flex-1 overflow-y-auto">
+ <div className="border-b border-border">
+ <div className="p-2.5 md:p-3 bg-muted/5">
+ <h4 className="text-sm md:text-base font-medium text-foreground">
+ Content
+ </h4>
+ </div>
+ <div className="p-3 md:p-4">
+ <ContentDisplaySection />
+ </div>
+ </div>
+
+ <div className="border-b border-border">
+ <div className="p-2.5 md:p-3 bg-muted/5">
+ <h4 className="text-sm md:text-base font-medium text-foreground">
+ Summary
+ </h4>
+ </div>
+ <div className="p-3 md:p-4">
+ <SummaryDisplaySection />
+ </div>
+ </div>
+
+ <div>
+ <div className="px-2.5 pt-2.5 md:p-3 bg-muted/5">
+ <h4 className="text-sm md:text-base font-medium text-foreground">
+ Memories
+ </h4>
+ </div>
+ <div className="p-3 md:p-4">
+ <MemoryContent />
+ </div>
+ </div>
+ </div>
+ </DrawerContent>
+ </Drawer>
+ )
+ }
+
+ return (
+ <Dialog onOpenChange={onClose} open={isOpen}>
+ <DialogContent className="w-[95vw] md:w-[90vw] lg:w-[85vw] h-[90vh] border-0 p-0 overflow-hidden flex flex-col bg-background !max-w-7xl gap-0">
+ <div className="p-4 md:p-6 relative flex-shrink-0 bg-muted/5">
+ <DialogHeader className="pb-0">
+ <HeaderContent TitleComponent={DialogTitle} />
+ </DialogHeader>
+ </div>
+
+ <div className="flex-1 flex flex-col lg:flex-row overflow-hidden">
+ <div className="flex-1 flex flex-col h-full justify-between min-w-0">
+ <div className="p-2 px-3 md:px-4 overflow-y-auto custom-scrollbar transition-all duration-300">
+ <h3 className="font-medium text-[10px] md:text-xs text-muted-foreground uppercase pb-1 px-1">
+ Content
+ </h3>
+ <ContentDisplaySection />
+ </div>
+
+ <div className="transition-all duration-300 mx-2 mb-3 md:mb-4 flex-shrink-0">
+ <div className="bg-card border border-border rounded-xl shadow-lg backdrop-blur-sm h-full flex flex-col">
+ <button
+ onClick={() => setIsSummaryOpen(!isSummaryOpen)}
+ className="flex-shrink-0 w-full flex items-center justify-between p-3 md:p-4 hover:bg-muted/5 transition-colors rounded-t-xl"
+ type="button"
+ >
+ <div className="flex items-center gap-1.5 md:gap-2">
+ <h3 className="font-semibold text-xs md:text-sm text-foreground">
+ Summary
+ </h3>
+ {document.summary &&
+ document.summary.trim().length > 0 && (
+ <Badge
+ className="text-[10px] h-5"
+ variant="secondary"
+ >
+ Available
+ </Badge>
+ )}
+ </div>
+ {isSummaryOpen ? (
+ <ChevronDown className="w-4 h-4 text-muted-foreground" />
+ ) : (
+ <ChevronUp className="w-4 h-4 text-muted-foreground" />
+ )}
+ </button>
+
+ {isSummaryOpen && (
+ <div className="flex-1 px-3 md:px-4 pb-3 md:pb-4 overflow-hidden min-h-0">
+ <div className="h-full overflow-y-auto custom-scrollbar">
+ <SummaryDisplaySection />
+ </div>
+ </div>
+ )}
+ </div>
+ </div>
+ </div>
+
+ <div className="w-full lg:w-96 flex flex-col border-t lg:border-t-0 lg:border-l border-border">
+ <div className="flex-1 flex flex-col">
+ <div className="flex-1 memory-dialog-scroll overflow-y-auto p-3 md:p-4">
+ <MemoryContent />
+ </div>
+ </div>
+ </div>
+ </div>
+ </DialogContent>
+ </Dialog>
+ )
+ },
+)
diff --git a/apps/web/components/memories.tsx b/apps/web/components/memories.tsx
new file mode 100644
index 00000000..568e9eb5
--- /dev/null
+++ b/apps/web/components/memories.tsx
@@ -0,0 +1,276 @@
+"use client"
+
+import { useAuth } from "@lib/auth-context"
+import { $fetch } from "@repo/lib/api"
+import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
+import { useInfiniteQuery } from "@tanstack/react-query"
+import { useCallback, useEffect, useMemo, useState } from "react"
+import type { z } from "zod"
+import { MemoryGraph } from "@repo/ui/memory-graph"
+import { Dialog, DialogContent } from "@repo/ui/components/dialog"
+import { ConnectAIModal } from "@/components/connect-ai-modal"
+import { MasonryMemoryList } from "@/components/masonry-memory-list"
+import { AddMemoryView } from "@/components/views/add-memory"
+import { useChatOpen, useProject, useGraphModal } from "@/stores"
+import { useGraphHighlights } from "@/stores/highlights"
+
+type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
+type DocumentWithMemories = DocumentsResponse["documents"][0]
+
+export function Memories() {
+ const { user } = useAuth()
+ const { documentIds: allHighlightDocumentIds } = useGraphHighlights()
+ const { selectedProject } = useProject()
+ const { isOpen } = useChatOpen()
+ const { isOpen: showGraphModal, setIsOpen: setShowGraphModal } =
+ useGraphModal()
+ const [injectedDocs, setInjectedDocs] = useState<DocumentWithMemories[]>([])
+ const [showAddMemoryView, setShowAddMemoryView] = useState(false)
+ const [showConnectAIModal, setShowConnectAIModal] = useState(false)
+
+ const IS_DEV = process.env.NODE_ENV === "development"
+ const PAGE_SIZE = IS_DEV ? 100 : 100
+ const MAX_TOTAL = 1000
+
+ const {
+ data,
+ error,
+ isPending,
+ isFetchingNextPage,
+ hasNextPage,
+ fetchNextPage,
+ } = useInfiniteQuery<DocumentsResponse, Error>({
+ queryKey: ["documents-with-memories", selectedProject],
+ initialPageParam: 1,
+ queryFn: async ({ pageParam }) => {
+ const response = await $fetch("@post/documents/documents", {
+ body: {
+ page: pageParam as number,
+ limit: (pageParam as number) === 1 ? (IS_DEV ? 500 : 500) : PAGE_SIZE,
+ sort: "createdAt",
+ order: "desc",
+ containerTags: selectedProject ? [selectedProject] : undefined,
+ },
+ disableValidation: true,
+ })
+
+ if (response.error) {
+ throw new Error(response.error?.message || "Failed to fetch documents")
+ }
+
+ return response.data
+ },
+ getNextPageParam: (lastPage, allPages) => {
+ const loaded = allPages.reduce(
+ (acc, p) => acc + (p.documents?.length ?? 0),
+ 0,
+ )
+ if (loaded >= MAX_TOTAL) return undefined
+
+ const { currentPage, totalPages } = lastPage.pagination
+ if (currentPage < totalPages) {
+ return currentPage + 1
+ }
+ return undefined
+ },
+ staleTime: 5 * 60 * 1000,
+ enabled: !!user, // Only run query if user is authenticated
+ })
+
+ const baseDocuments = useMemo(() => {
+ return (
+ data?.pages.flatMap((p: DocumentsResponse) => p.documents ?? []) ?? []
+ )
+ }, [data])
+
+ const allDocuments = useMemo(() => {
+ if (injectedDocs.length === 0) return baseDocuments
+ const byId = new Map<string, DocumentWithMemories>()
+ for (const d of injectedDocs) byId.set(d.id, d)
+ for (const d of baseDocuments) if (!byId.has(d.id)) byId.set(d.id, d)
+ return Array.from(byId.values())
+ }, [baseDocuments, injectedDocs])
+
+ const totalLoaded = allDocuments.length
+ const hasMore = hasNextPage
+ const isLoadingMore = isFetchingNextPage
+
+ const loadMoreDocuments = useCallback(async (): Promise<void> => {
+ if (hasNextPage && !isFetchingNextPage) {
+ await fetchNextPage()
+ return
+ }
+ return
+ }, [hasNextPage, isFetchingNextPage, fetchNextPage])
+
+ // Handle highlighted documents injection for chat
+ useEffect(() => {
+ if (!isOpen) return
+ if (!allHighlightDocumentIds || allHighlightDocumentIds.length === 0) return
+ const present = new Set<string>()
+ for (const d of [...baseDocuments, ...injectedDocs]) {
+ if (d.id) present.add(d.id)
+ if (d.customId) present.add(d.customId as string)
+ }
+ const missing = allHighlightDocumentIds.filter(
+ (id: string) => !present.has(id),
+ )
+ if (missing.length === 0) return
+ let cancelled = false
+ const run = async () => {
+ try {
+ const resp = await $fetch("@post/documents/documents/by-ids", {
+ body: {
+ ids: missing,
+ by: "customId",
+ containerTags: selectedProject ? [selectedProject] : undefined,
+ },
+ disableValidation: true,
+ })
+ if (cancelled || resp?.error) return
+ const extraDocs = resp?.data?.documents as
+ | DocumentWithMemories[]
+ | undefined
+ if (!extraDocs || extraDocs.length === 0) return
+ setInjectedDocs((prev) => {
+ const seen = new Set<string>([
+ ...prev.map((d) => d.id),
+ ...baseDocuments.map((d) => d.id),
+ ])
+ const merged = [...prev]
+ for (const doc of extraDocs) {
+ if (!seen.has(doc.id)) {
+ merged.push(doc)
+ seen.add(doc.id)
+ }
+ }
+ return merged
+ })
+ } catch {}
+ }
+ void run()
+ return () => {
+ cancelled = true
+ }
+ }, [
+ isOpen,
+ allHighlightDocumentIds,
+ baseDocuments,
+ injectedDocs,
+ selectedProject,
+ ])
+
+ // Show connect AI modal if no documents
+ useEffect(() => {
+ if (allDocuments.length === 0) {
+ setShowConnectAIModal(true)
+ }
+ }, [allDocuments.length])
+
+ if (!user) {
+ return (
+ <div className="flex items-center justify-center h-full">
+ <div className="text-center text-muted-foreground">
+ <p>Please log in to view your memories</p>
+ </div>
+ </div>
+ )
+ }
+
+ 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">
+ <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>
+ </MasonryMemoryList>
+
+ {showAddMemoryView && (
+ <AddMemoryView
+ initialTab="note"
+ onClose={() => setShowAddMemoryView(false)}
+ />
+ )}
+ </div>
+
+ {/* Memory Graph Modal */}
+ <Dialog open={showGraphModal} onOpenChange={setShowGraphModal}>
+ <DialogContent
+ className="w-[95vw] h-[95vh] p-0 max-w-6xl sm:max-w-6xl"
+ showCloseButton={true}
+ >
+ <div className="w-full h-full">
+ <MemoryGraph
+ documents={allDocuments}
+ error={error}
+ hasMore={hasMore}
+ isLoading={isPending}
+ isLoadingMore={isLoadingMore}
+ loadMoreDocuments={loadMoreDocuments}
+ totalLoaded={totalLoaded}
+ variant="console"
+ showSpacesSelector={true}
+ highlightDocumentIds={allHighlightDocumentIds}
+ highlightsVisible={isOpen}
+ >
+ <div className="absolute inset-0 flex items-center justify-center">
+ <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>
+ </MemoryGraph>
+ </div>
+ </DialogContent>
+ </Dialog>
+ </>
+ )
+}
diff --git a/apps/web/components/memories/index.tsx b/apps/web/components/memories/index.tsx
deleted file mode 100644
index 97ef57bd..00000000
--- a/apps/web/components/memories/index.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-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
deleted file mode 100644
index dad2a8a3..00000000
--- a/apps/web/components/memories/memory-detail.tsx
+++ /dev/null
@@ -1,415 +0,0 @@
-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 {
- 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, 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';
-
-type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>;
-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 (
- <button
- className="p-4 rounded-lg transition-all relative overflow-hidden cursor-pointer"
- style={{
- backgroundColor: memory.isLatest
- ? colors.memory.primary
- : 'rgba(255, 255, 255, 0.02)',
- }}
- tabIndex={0}
- type="button"
- >
- <div className="flex items-start gap-2 relative z-10">
- <div
- className="p-1 rounded"
- style={{
- backgroundColor: memory.isLatest
- ? colors.memory.secondary
- : 'transparent',
- }}
- >
- <Brain
- className={`w-4 h-4 flex-shrink-0 transition-all ${
- memory.isLatest ? 'text-blue-400' : 'text-blue-400/50'
- }`}
- />
- </div>
- <div className="flex-1 space-y-2">
- <Label1Regular
- className="text-sm leading-relaxed text-left"
- style={{ color: colors.text.primary }}
- >
- {memory.memory}
- </Label1Regular>
- <div className="flex gap-2 justify-between">
- <div
- className="flex items-center gap-4 text-xs"
- style={{ color: colors.text.muted }}
- >
- <span className="flex items-center gap-1">
- <Calendar className="w-3 h-3" />
- {formatDate(memory.createdAt)}
- </span>
- <span className="font-mono">v{memory.version}</span>
- {memory.sourceRelevanceScore && (
- <span
- className="flex items-center gap-1"
- style={{
- color:
- memory.sourceRelevanceScore > 70
- ? colors.accent.emerald
- : colors.text.muted,
- }}
- >
- <Sparkles className="w-3 h-3" />
- {memory.sourceRelevanceScore}%
- </span>
- )}
- </div>
- <div className="flex items-center gap-2 flex-wrap">
- {memory.isForgotten && (
- <Badge
- className="text-xs border-red-500/30 backdrop-blur-sm"
- style={{
- backgroundColor: colors.status.forgotten,
- color: '#dc2626',
- backdropFilter: 'blur(4px)',
- WebkitBackdropFilter: 'blur(4px)',
- }}
- variant="destructive"
- >
- Forgotten
- </Badge>
- )}
- {memory.isLatest && (
- <Badge
- className="text-xs"
- style={{
- backgroundColor: colors.memory.secondary,
- color: colors.text.primary,
- backdropFilter: 'blur(4px)',
- WebkitBackdropFilter: 'blur(4px)',
- }}
- variant="default"
- >
- Latest
- </Badge>
- )}
- {memory.forgetAfter && (
- <Badge
- className="text-xs backdrop-blur-sm"
- style={{
- color: colors.status.expiring,
- backgroundColor: 'rgba(251, 165, 36, 0.1)',
- backdropFilter: 'blur(4px)',
- WebkitBackdropFilter: 'blur(4px)',
- }}
- variant="outline"
- >
- Expires: {formatDate(memory.forgetAfter)}
- </Badge>
- )}
- </div>
- </div>
- </div>
- </div>
- </button>
- );
-});
-
-export const MemoryDetail = memo(
- ({
- document,
- isOpen,
- onClose,
- isMobile,
- }: {
- document: DocumentWithMemories | null;
- isOpen: boolean;
- onClose: () => void;
- isMobile: boolean;
- }) => {
- if (!document) return null;
-
- const activeMemories = document.memoryEntries.filter((m) => !m.isForgotten);
- const forgottenMemories = document.memoryEntries.filter(
- (m) => m.isForgotten
- );
-
- const HeaderContent = ({
- TitleComponent,
- }: {
- TitleComponent: typeof SheetTitle | typeof DrawerTitle;
- }) => (
- <div className="flex items-start justify-between gap-2">
- <div className="flex items-start gap-3 flex-1">
- <div
- className="p-2 rounded-lg"
- style={{
- backgroundColor: colors.background.secondary,
- }}
- >
- {getDocumentIcon(document.type, 'w-5 h-5')}
- </div>
- <div className="flex-1">
- <TitleComponent style={{ color: colors.text.primary }}>
- {document.title || 'Untitled Document'}
- </TitleComponent>
- <div
- className="flex items-center gap-2 mt-1 text-xs"
- style={{ color: colors.text.muted }}
- >
- <span>{formatDocumentType(document.type)}</span>
- <span>•</span>
- <span>{formatDate(document.createdAt)}</span>
- {document.url && (
- <>
- <span>•</span>
- <button
- className="flex items-center gap-1 transition-all hover:gap-2"
- onClick={() => {
- const sourceUrl = getSourceUrl(document);
- window.open(sourceUrl ?? undefined, '_blank');
- }}
- style={{ color: colors.accent.primary }}
- type="button"
- >
- View source
- <ExternalLink className="w-3 h-3" />
- </button>
- </>
- )}
- </div>
- </div>
- </div>
- </div>
- );
-
- 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 defaultTab = hasContent ? 'content' : 'summary';
-
- return (
- <div className="mt-4">
- <Tabs defaultValue={defaultTab} className="w-full">
- <TabsList
- className={`grid w-full bg-white/5 border border-white/10 h-11 ${
- hasContent && hasSummary ? 'grid-cols-2' : 'grid-cols-1'
- }`}
- >
- {hasContent && (
- <TabsTrigger
- value="content"
- className="text-xs bg-transparent h-8"
- style={{ color: colors.text.secondary }}
- >
- <CircleUserRound className="w-3 h-3" />
- Original Content
- </TabsTrigger>
- )}
- {hasSummary && (
- <TabsTrigger
- value="summary"
- className="text-xs flex items-center gap-1 bg-transparent h-8"
- style={{ color: colors.text.secondary }}
- >
- <List className="w-3 h-3" />
- Summary
- </TabsTrigger>
- )}
- </TabsList>
-
- {hasContent && (
- <TabsContent value="content" className="mt-3">
- <div className="p-3 rounded-lg max-h-48 overflow-y-auto custom-scrollbar bg-white/[0.03] border border-white/[0.08]">
- <p
- className="text-sm leading-relaxed whitespace-pre-wrap"
- style={{ color: colors.text.primary }}
- >
- {document.content}
- </p>
- </div>
- </TabsContent>
- )}
-
- {hasSummary && (
- <TabsContent value="summary" className="mt-3">
- <div className="p-3 rounded-lg max-h-48 overflow-y-auto custom-scrollbar bg-indigo-500/5 border border-indigo-500/15">
- <p
- className="text-sm leading-relaxed whitespace-pre-wrap"
- style={{ color: colors.text.muted }}
- >
- {document.summary}
- </p>
- </div>
- </TabsContent>
- )}
- </Tabs>
- </div>
- );
- };
-
- const MemoryContent = () => (
- <div className="space-y-6 px-6">
- {activeMemories.length > 0 && (
- <div>
- <div
- className="text-sm font-medium mb-2 flex items-start gap-2 py-2"
- style={{
- color: colors.text.secondary,
- }}
- >
- Active Memories ({activeMemories.length})
- </div>
- <div className="space-y-3">
- {activeMemories.map((memory) => (
- <div
- key={memory.id}
- >
- <MemoryDetailItem memory={memory} />
- </div>
- ))}
- </div>
- </div>
- )}
-
- {forgottenMemories.length > 0 && (
- <div>
- <div
- className="text-sm font-medium mb-4 px-3 py-2 rounded-lg opacity-60"
- style={{
- color: colors.text.muted,
- backgroundColor: 'rgba(255, 255, 255, 0.02)',
- }}
- >
- Forgotten Memories ({forgottenMemories.length})
- </div>
- <div className="space-y-3 opacity-40">
- {forgottenMemories.map((memory) => (
- <MemoryDetailItem key={memory.id} memory={memory} />
- ))}
- </div>
- </div>
- )}
-
- {activeMemories.length === 0 && forgottenMemories.length === 0 && (
- <div
- className="text-center py-12 rounded-lg"
- style={{
- backgroundColor: 'rgba(255, 255, 255, 0.02)',
- }}
- >
- <Brain
- className="w-12 h-12 mx-auto mb-4 opacity-30"
- style={{ color: colors.text.muted }}
- />
- <p style={{ color: colors.text.muted }}>
- No memories found for this document
- </p>
- </div>
- )}
- </div>
- );
-
- if (isMobile) {
- return (
- <Drawer onOpenChange={onClose} open={isOpen}>
- <DrawerContent
- className="border-0 p-0 overflow-hidden max-h-[90vh]"
- style={{
- backgroundColor: colors.background.secondary,
- borderTop: `1px solid ${colors.document.border}`,
- backdropFilter: 'blur(20px)',
- WebkitBackdropFilter: 'blur(20px)',
- }}
- >
- {/* Header section with glass effect */}
- <div
- className="p-4 relative border-b"
- style={{
- backgroundColor: 'rgba(255, 255, 255, 0.02)',
- borderBottom: `1px solid ${colors.document.border}`,
- }}
- >
- <DrawerHeader className="pb-0 px-0 text-left">
- <HeaderContent TitleComponent={DrawerTitle} />
- </DrawerHeader>
-
- <ContentAndSummarySection />
- </div>
-
- <div className="flex-1 memory-drawer-scroll overflow-y-auto">
- <MemoryContent />
- </div>
- </DrawerContent>
- </Drawer>
- );
- }
-
- return (
- <Sheet onOpenChange={onClose} open={isOpen}>
- <SheetContent
- className="w-full sm:max-w-2xl border-0 p-0 overflow-hidden"
- style={{
- backgroundColor: colors.background.secondary,
- }}
- >
- <div
- className="p-6 relative"
- style={{
- backgroundColor: 'rgba(255, 255, 255, 0.02)',
- }}
- >
- <SheetHeader className="pb-0">
- <HeaderContent TitleComponent={SheetTitle} />
- </SheetHeader>
-
- <ContentAndSummarySection />
- </div>
-
- <div className="h-[calc(100vh-200px)] memory-sheet-scroll overflow-y-auto">
- <MemoryContent />
- </div>
- </SheetContent>
- </Sheet>
- );
- }
-);
diff --git a/apps/web/components/memory-list-view.tsx b/apps/web/components/memory-list-view.tsx
index b91e2562..c2b4b0c8 100644
--- a/apps/web/components/memory-list-view.tsx
+++ b/apps/web/components/memory-list-view.tsx
@@ -26,9 +26,9 @@ import { analytics } from "@/lib/analytics"
import { useDeleteDocument } from "@lib/queries"
import { useProject } from "@/stores"
-import { MemoryDetail } from "./memories/memory-detail"
+import { MemoryDetail } from "./memories-utils/memory-detail"
import { getDocumentIcon } from "@/lib/document-icon"
-import { formatDate, getSourceUrl } from "./memories"
+import { formatDate, getSourceUrl } from "./memories-utils"
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
type DocumentWithMemories = DocumentsResponse["documents"][0]
@@ -44,31 +44,6 @@ interface MemoryListViewProps {
loadMoreDocuments: () => Promise<void>
}
-const GreetingMessage = memo(() => {
- const getGreeting = () => {
- const hour = new Date().getHours()
- if (hour < 12) return "Good morning"
- if (hour < 17) return "Good afternoon"
- return "Good evening"
- }
-
- return (
- <div className="flex items-center gap-3 mb-3 px-4 md:mb-6 md:mt-3">
- <div>
- <h1
- className="text-lg md:text-xl font-semibold"
- style={{ color: colors.text.primary }}
- >
- {getGreeting()}!
- </h1>
- <p className="text-xs md:text-sm" style={{ color: colors.text.muted }}>
- Welcome back to your memory collection
- </p>
- </div>
- </div>
- )
-})
-
const DocumentCard = memo(
({
document,
@@ -86,7 +61,7 @@ const DocumentCard = memo(
return (
<Card
- className="h-full mx-4 p-4 transition-all cursor-pointer group relative overflow-hidden border-0 gap-2 md:w-full"
+ className="h-full mx-4 p-4 transition-all cursor-pointer group relative overflow-hidden gap-2 md:w-full shadow-xs"
onClick={() => {
analytics.documentCardClicked()
onOpenDetails(document)
@@ -312,7 +287,7 @@ export const MemoryListView = ({
hasMore,
isLoadingMore,
loadMoreDocuments,
- virtualizer.getVirtualItems(),
+ virtualizer.getVirtualItems,
virtualItems.length,
])
@@ -322,7 +297,6 @@ export const MemoryListView = ({
<div
className="h-full overflow-hidden relative pb-20"
ref={containerRef}
- style={{ backgroundColor: colors.background.primary }}
>
{error ? (
<div className="h-full flex items-center justify-center p-4">
@@ -358,8 +332,6 @@ export const MemoryListView = ({
ref={parentRef}
className="h-full overflow-auto mt-20 custom-scrollbar"
>
- <GreetingMessage />
-
<div
className="w-full relative"
style={{
@@ -375,13 +347,13 @@ export const MemoryListView = ({
key={virtualRow.key}
data-index={virtualRow.index}
ref={virtualizer.measureElement}
- className="absolute top-0 left-0 w-full"
+ className="absolute top-0 left-0 w-full sm-tweet-theme"
style={{
transform: `translateY(${virtualRow.start + virtualRow.index * gap}px)`,
}}
>
<div
- className="grid justify-start"
+ className="grid justify-center"
style={{
gridTemplateColumns: `repeat(${columns}, ${columnWidth}px)`,
gap: `${gap}px`,
diff --git a/apps/web/components/menu.tsx b/apps/web/components/menu.tsx
index f32b56d1..e59f3839 100644
--- a/apps/web/components/menu.tsx
+++ b/apps/web/components/menu.tsx
@@ -10,7 +10,7 @@ import { ConnectAIModal } from "./connect-ai-modal"
import { HeadingH2Bold } from "@repo/ui/text/heading/heading-h2-bold"
import { GlassMenuEffect } from "@ui/other/glass-effect"
import { useCustomer } from "autumn-js/react"
-import { MessageSquareMore, Plus, Puzzle, User, X } from "lucide-react"
+import { Plus, Puzzle, User, X } from "lucide-react"
import { AnimatePresence, LayoutGroup, motion } from "motion/react"
import { useRouter, useSearchParams } from "next/navigation"
import { useCallback, useEffect, useState } from "react"
@@ -22,7 +22,7 @@ import { AddMemoryExpandedView, AddMemoryView } from "./views/add-memory"
import { IntegrationsView } from "./views/integrations"
import { ProfileView } from "./views/profile"
-const MCPIcon = ({ className }: { className?: string }) => {
+export const MCPIcon = ({ className }: { className?: string }) => {
return (
<svg
className={className}
@@ -66,7 +66,7 @@ function Menu({ id }: { id?: string }) {
const autumn = useCustomer()
const { setIsOpen } = useChatOpen()
- const { data: memoriesCheck } = fetchMemoriesFeature(autumn)
+ const { data: memoriesCheck } = fetchMemoriesFeature(autumn, !autumn.isLoading)
const memoriesUsed = memoriesCheck?.usage ?? 0
const memoriesLimit = memoriesCheck?.included_usage ?? 0
@@ -104,12 +104,6 @@ function Menu({ id }: { id?: string }) {
disabled: false,
},
{
- icon: MessageSquareMore,
- text: "Chat",
- key: "chat" as const,
- disabled: false,
- },
- {
icon: Puzzle,
text: "Integrations",
key: "integrations" as const,
@@ -248,14 +242,9 @@ function Menu({ id }: { id?: string }) {
},
}}
>
- {/* Glass effect background */}
- <motion.div className="absolute inset-0" layout>
- <GlassMenuEffect />
- </motion.div>
-
{/* Menu content */}
<motion.div
- className="relative z-20 flex flex-col gap-6 w-full"
+ className="relative z-20 flex flex-col gap-6 w-full bg-white"
layout
>
<AnimatePresence
@@ -304,7 +293,7 @@ function Menu({ id }: { id?: string }) {
duration: 0.1,
},
}}
- className={`flex w-full items-center text-white/80 transition-colors duration-100 hover:text-white cursor-pointer relative ${isHovered || expandedView ? "px-1" : ""}`}
+ className={`flex w-full items-center transition-colors duration-100 cursor-pointer relative ${isHovered || expandedView ? "px-1" : ""}`}
initial={{ opacity: 0, y: 20, scale: 0.95 }}
layout
onClick={() => handleMenuItemClick(item.key)}
@@ -326,14 +315,14 @@ function Menu({ id }: { id?: string }) {
initial={{ scale: 0.8 }}
layout="position"
>
- <item.icon className="duration-200 h-6 w-6 drop-shadow-lg flex-shrink-0" />
+ <item.icon className="duration-200 h-6 w-6 flex-shrink-0" />
</motion.div>
<motion.p
animate={{
opacity: isHovered ? 1 : 0,
x: isHovered ? 0 : -10,
}}
- className="drop-shadow-lg pl-3 whitespace-nowrap"
+ className="pl-3 whitespace-nowrap"
initial={{ opacity: 0, x: -10 }}
style={{
transform: "translateZ(0)",
@@ -353,7 +342,7 @@ function Menu({ id }: { id?: string }) {
opacity: 1,
scaleX: 1,
}}
- className="w-full h-px bg-white/20 mt-3 origin-left"
+ className="w-full h-px bg-black/20 mt-3 origin-left"
initial={{ opacity: 0, scaleX: 0 }}
transition={{
duration: 0.3,
diff --git a/apps/web/components/project-selector.tsx b/apps/web/components/project-selector.tsx
index a592c474..0682a641 100644
--- a/apps/web/components/project-selector.tsx
+++ b/apps/web/components/project-selector.tsx
@@ -1,8 +1,8 @@
-"use client";
+"use client"
-import { $fetch } from "@repo/lib/api";
-import { DEFAULT_PROJECT_ID } from "@repo/lib/constants";
-import { Button } from "@repo/ui/components/button";
+import { $fetch } from "@repo/lib/api"
+import { DEFAULT_PROJECT_ID } from "@repo/lib/constants"
+import { Button } from "@repo/ui/components/button"
import {
Dialog,
DialogContent,
@@ -10,141 +10,139 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
-} from "@repo/ui/components/dialog";
+} from "@repo/ui/components/dialog"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
-} from "@repo/ui/components/dropdown-menu";
-import { Label } from "@repo/ui/components/label";
+} from "@repo/ui/components/dropdown-menu"
+import { Label } from "@repo/ui/components/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
-} from "@repo/ui/components/select";
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+} from "@repo/ui/components/select"
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import {
ChevronDown,
FolderIcon,
Loader2,
MoreHorizontal,
- MoreVertical,
Plus,
Trash2,
-} from "lucide-react";
-import { AnimatePresence, motion } from "motion/react";
-import { useState } from "react";
-import { toast } from "sonner";
-import { useProjectMutations } from "@/hooks/use-project-mutations";
-import { useProjectName } from "@/hooks/use-project-name";
-import { useProject } from "@/stores";
-import { CreateProjectDialog } from "./create-project-dialog";
+} from "lucide-react"
+import { AnimatePresence, motion } from "motion/react"
+import { useState } from "react"
+import { toast } from "sonner"
+import { useProjectMutations } from "@/hooks/use-project-mutations"
+import { useProjectName } from "@/hooks/use-project-name"
+import { useProject } from "@/stores"
+import { CreateProjectDialog } from "./create-project-dialog"
interface Project {
- id: string;
- name: string;
- containerTag: string;
- createdAt: string;
- updatedAt: string;
- isExperimental?: boolean;
+ id: string
+ name: string
+ containerTag: string
+ createdAt: string
+ updatedAt: string
+ isExperimental?: boolean
}
export function ProjectSelector() {
- const queryClient = useQueryClient();
- const [isOpen, setIsOpen] = useState(false);
- const [showCreateDialog, setShowCreateDialog] = useState(false);
- const { selectedProject } = useProject();
- const projectName = useProjectName();
- const { switchProject, deleteProjectMutation } = useProjectMutations();
+ const queryClient = useQueryClient()
+ const [isOpen, setIsOpen] = useState(false)
+ const [showCreateDialog, setShowCreateDialog] = useState(false)
+ const { selectedProject } = useProject()
+ const projectName = useProjectName()
+ const { switchProject, deleteProjectMutation } = useProjectMutations()
const [deleteDialog, setDeleteDialog] = useState<{
- open: boolean;
- project: null | { id: string; name: string; containerTag: string };
- action: "move" | "delete";
- targetProjectId: string;
+ open: boolean
+ project: null | { id: string; name: string; containerTag: string }
+ action: "move" | "delete"
+ targetProjectId: string
}>({
open: false,
project: null,
action: "move",
targetProjectId: DEFAULT_PROJECT_ID,
- });
+ })
const [expDialog, setExpDialog] = useState<{
- open: boolean;
- projectId: string;
+ open: boolean
+ projectId: string
}>({
open: false,
projectId: "",
- });
+ })
const { data: projects = [], isLoading } = useQuery({
queryKey: ["projects"],
queryFn: async () => {
- const response = await $fetch("@get/projects");
+ const response = await $fetch("@get/projects")
if (response.error) {
- throw new Error(response.error?.message || "Failed to load projects");
+ throw new Error(response.error?.message || "Failed to load projects")
}
- return response.data?.projects || [];
+ return response.data?.projects || []
},
staleTime: 30 * 1000,
- });
+ })
const enableExperimentalMutation = useMutation({
mutationFn: async (projectId: string) => {
const response = await $fetch(
`@post/projects/${projectId}/enable-experimental`,
- );
+ )
if (response.error) {
throw new Error(
response.error?.message || "Failed to enable experimental mode",
- );
+ )
}
- return response.data;
+ return response.data
},
onSuccess: () => {
- toast.success("Experimental mode enabled for project");
- queryClient.invalidateQueries({ queryKey: ["projects"] });
- setExpDialog({ open: false, projectId: "" });
+ toast.success("Experimental mode enabled for project")
+ queryClient.invalidateQueries({ queryKey: ["projects"] })
+ setExpDialog({ open: false, projectId: "" })
},
onError: (error) => {
toast.error("Failed to enable experimental mode", {
description: error instanceof Error ? error.message : "Unknown error",
- });
+ })
},
- });
+ })
const handleProjectSelect = (containerTag: string) => {
- switchProject(containerTag);
- setIsOpen(false);
- };
+ switchProject(containerTag)
+ setIsOpen(false)
+ }
const handleCreateNewProject = () => {
- setIsOpen(false);
- setShowCreateDialog(true);
- };
+ setIsOpen(false)
+ setShowCreateDialog(true)
+ }
return (
<div className="relative">
- <motion.button
- className="flex items-center gap-1.5 px-2 py-1.5 rounded-md bg-white/5 hover:bg-white/10 transition-colors"
+ <Button
+ variant="ghost"
+ className="flex items-center gap-1.5 px-2 py-1.5 rounded-md transition-colors"
onClick={() => setIsOpen(!isOpen)}
- whileHover={{ scale: 1.01 }}
- whileTap={{ scale: 0.99 }}
>
- <FolderIcon className="h-3.5 w-3.5 text-white/70" />
- <span className="text-xs font-medium text-white/90 max-w-32 truncate">
+ <FolderIcon className="h-3.5 w-3.5" />
+ <span className="text-xs font-medium max-w-32 truncate">
{isLoading ? "..." : projectName}
</span>
<motion.div
animate={{ rotate: isOpen ? 180 : 0 }}
transition={{ duration: 0.15 }}
>
- <ChevronDown className="h-3 w-3 text-white/50" />
+ <ChevronDown className="h-3 w-3" />
</motion.div>
- </motion.button>
+ </Button>
<AnimatePresence>
{isOpen && (
@@ -158,29 +156,27 @@ export function ProjectSelector() {
/>
<motion.div
- className="absolute top-full left-0 mt-1 w-56 bg-[#0f1419] backdrop-blur-xl border border-white/10 rounded-md shadow-xl z-50 overflow-hidden"
+ className="absolute top-full left-0 mt-1 w-56 bg-background/95 backdrop-blur-xl border border-border rounded-md shadow-xl z-50 overflow-hidden"
initial={{ opacity: 0, y: -5, scale: 0.98 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -5, scale: 0.98 }}
transition={{ duration: 0.15 }}
>
<div className="p-1.5 max-h-64 overflow-y-auto">
- {/* Default Project */}
- <motion.div
+ <Button
+ variant="ghost"
className={`flex items-center justify-between p-2 rounded-md transition-colors cursor-pointer ${
selectedProject === DEFAULT_PROJECT_ID
- ? "bg-white/15"
- : "hover:bg-white/8"
+ ? "bg-accent"
+ : "hover:bg-accent/50"
}`}
onClick={() => handleProjectSelect(DEFAULT_PROJECT_ID)}
>
<div className="flex items-center gap-2">
- <FolderIcon className="h-3.5 w-3.5 text-white/70" />
- <span className="text-xs font-medium text-white">
- Default
- </span>
+ <FolderIcon className="h-3.5 w-3.5" />
+ <span className="text-xs font-medium">Default</span>
</div>
- </motion.div>
+ </Button>
{/* User Projects */}
{projects
@@ -190,71 +186,69 @@ export function ProjectSelector() {
key={project.id}
className={`flex items-center justify-between p-2 rounded-md transition-colors group ${
selectedProject === project.containerTag
- ? "bg-white/15"
- : "hover:bg-white/8"
+ ? "bg-accent"
+ : "hover:bg-accent/50"
}`}
initial={{ opacity: 0, x: -5 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.03 }}
>
- <div
+ <button
className="flex items-center gap-2 flex-1 cursor-pointer"
+ type="button"
onClick={() =>
handleProjectSelect(project.containerTag)
}
>
- <FolderIcon className="h-3.5 w-3.5 text-white/70" />
- <span className="text-xs font-medium text-white truncate max-w-32">
+ <FolderIcon className="h-3.5 w-3.5 opacity-70" />
+ <span className="text-xs font-medium truncate max-w-32">
{project.name}
</span>
- </div>
+ </button>
<div className="flex items-center gap-1">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<motion.button
- className="p-1 hover:bg-white/10 rounded transition-all"
+ className="p-1 hover:bg-accent rounded transition-all"
onClick={(e) => e.stopPropagation()}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
- <MoreHorizontal className="h-3 w-3 text-white/50" />
+ <MoreHorizontal className="h-3 w-3" />
</motion.button>
</DropdownMenuTrigger>
- <DropdownMenuContent
- align="end"
- className="bg-black/90 border-white/10"
- >
+ <DropdownMenuContent align="end">
{/* Show experimental toggle only if NOT experimental and NOT default project */}
{!project.isExperimental &&
project.containerTag !== DEFAULT_PROJECT_ID && (
<DropdownMenuItem
- className="text-blue-400 hover:text-blue-300 cursor-pointer text-xs"
+ className="text-blue-600 dark:text-blue-400 hover:text-blue-500 dark:hover:text-blue-300 cursor-pointer text-xs"
onClick={(e) => {
- e.stopPropagation();
+ e.stopPropagation()
setExpDialog({
open: true,
projectId: project.id,
- });
- setIsOpen(false);
+ })
+ setIsOpen(false)
}}
>
- <div className="h-3 w-3 mr-2 rounded border border-blue-400" />
+ <div className="h-3 w-3 mr-2 rounded border border-blue-600 dark:border-blue-400" />
Enable Experimental Mode
</DropdownMenuItem>
)}
{project.isExperimental && (
<DropdownMenuItem
- className="text-blue-300/50 text-xs"
+ className="text-blue-600/50 dark:text-blue-300/50 text-xs"
disabled
>
- <div className="h-3 w-3 mr-2 rounded bg-blue-400" />
+ <div className="h-3 w-3 mr-2 rounded bg-blue-600 dark:bg-blue-400" />
Experimental Mode Active
</DropdownMenuItem>
)}
<DropdownMenuItem
- className="text-red-400 hover:text-red-300 cursor-pointer text-xs"
+ className="text-red-600 dark:text-red-400 hover:text-red-500 dark:hover:text-red-300 cursor-pointer text-xs"
onClick={(e) => {
- e.stopPropagation();
+ e.stopPropagation()
setDeleteDialog({
open: true,
project: {
@@ -264,8 +258,8 @@ export function ProjectSelector() {
},
action: "move",
targetProjectId: "",
- });
- setIsOpen(false);
+ })
+ setIsOpen(false)
}}
>
<Trash2 className="h-3 w-3 mr-2" />
@@ -278,15 +272,15 @@ export function ProjectSelector() {
))}
<motion.div
- className="flex items-center gap-2 p-2 rounded-md hover:bg-white/8 transition-colors cursor-pointer border-t border-white/10 mt-1"
+ className="flex items-center gap-2 p-2 rounded-md hover:bg-accent/50 transition-colors cursor-pointer border-t border-border mt-1"
onClick={handleCreateNewProject}
whileHover={{ x: 1 }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: (projects.length + 1) * 0.03 }}
>
- <Plus className="h-3.5 w-3.5 text-white/70" />
- <span className="text-xs font-medium text-white/80">
+ <Plus className="h-3.5 w-3.5 text-foreground/70" />
+ <span className="text-xs font-medium text-foreground/80">
New Project
</span>
</motion.div>
@@ -310,7 +304,7 @@ export function ProjectSelector() {
}
open={deleteDialog.open}
>
- <DialogContent className="sm:max-w-2xl bg-black/90 backdrop-blur-xl border-white/10 text-white">
+ <DialogContent className="sm:max-w-2xl">
<motion.div
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
@@ -318,7 +312,7 @@ export function ProjectSelector() {
>
<DialogHeader>
<DialogTitle>Delete Project</DialogTitle>
- <DialogDescription className="text-white/60">
+ <DialogDescription>
Are you sure you want to delete "{deleteDialog.project.name}
"? Choose what to do with the documents in this project.
</DialogDescription>
@@ -339,10 +333,7 @@ export function ProjectSelector() {
}
type="radio"
/>
- <Label
- className="text-white cursor-pointer text-sm"
- htmlFor="move"
- >
+ <Label className="cursor-pointer text-sm" htmlFor="move">
Move documents to another project
</Label>
</div>
@@ -362,14 +353,11 @@ export function ProjectSelector() {
}
value={deleteDialog.targetProjectId}
>
- <SelectTrigger className="w-full bg-white/5 border-white/10 text-white">
+ <SelectTrigger className="w-full">
<SelectValue placeholder="Select target project..." />
</SelectTrigger>
- <SelectContent className="bg-black/90 backdrop-blur-xl border-white/10">
- <SelectItem
- className="text-white hover:bg-white/10"
- value={DEFAULT_PROJECT_ID}
- >
+ <SelectContent>
+ <SelectItem value={DEFAULT_PROJECT_ID}>
Default Project
</SelectItem>
{projects
@@ -379,11 +367,7 @@ export function ProjectSelector() {
p.containerTag !== DEFAULT_PROJECT_ID,
)
.map((project: Project) => (
- <SelectItem
- className="text-white hover:bg-white/10"
- key={project.id}
- value={project.id}
- >
+ <SelectItem key={project.id} value={project.id}>
{project.name}
</SelectItem>
))}
@@ -406,7 +390,7 @@ export function ProjectSelector() {
type="radio"
/>
<Label
- className="text-white cursor-pointer text-sm"
+ className="cursor-pointer text-sm"
htmlFor="delete"
>
Delete all documents in this project
@@ -415,7 +399,7 @@ export function ProjectSelector() {
{deleteDialog.action === "delete" && (
<motion.p
animate={{ opacity: 1 }}
- className="text-sm text-red-400 ml-6"
+ className="text-sm text-red-600 dark:text-red-400 ml-6"
initial={{ opacity: 0 }}
>
⚠️ This action cannot be undone. All documents will be
@@ -430,7 +414,6 @@ export function ProjectSelector() {
whileTap={{ scale: 0.95 }}
>
<Button
- className="bg-white/5 hover:bg-white/10 border-white/10 text-white"
onClick={() =>
setDeleteDialog({
open: false,
@@ -450,11 +433,11 @@ export function ProjectSelector() {
whileTap={{ scale: 0.95 }}
>
<Button
- className={`${
+ className={
deleteDialog.action === "delete"
- ? "bg-red-600 hover:bg-red-700"
- : "bg-white/10 hover:bg-white/20"
- } text-white border-white/20`}
+ ? "bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700 text-white"
+ : ""
+ }
disabled={
deleteProjectMutation.isPending ||
(deleteDialog.action === "move" &&
@@ -478,10 +461,10 @@ export function ProjectSelector() {
project: null,
action: "move",
targetProjectId: "",
- });
+ })
},
},
- );
+ )
}
}}
type="button"
@@ -514,7 +497,7 @@ export function ProjectSelector() {
onOpenChange={(open) => setExpDialog({ ...expDialog, open })}
open={expDialog.open}
>
- <DialogContent className="sm:max-w-lg bg-black/90 backdrop-blur-xl border-white/10 text-white">
+ <DialogContent className="sm:max-w-lg">
<motion.div
animate={{ opacity: 1, scale: 1 }}
className="flex flex-col gap-4"
@@ -522,19 +505,19 @@ export function ProjectSelector() {
initial={{ opacity: 0, scale: 0.95 }}
>
<DialogHeader>
- <DialogTitle className="text-white">
- Enable Experimental Mode?
- </DialogTitle>
- <DialogDescription className="text-white/60">
+ <DialogTitle>Enable Experimental Mode?</DialogTitle>
+ <DialogDescription>
Experimental mode enables beta features and advanced memory
relationships for this project.
<br />
<br />
- <span className="text-yellow-400 font-medium">
+ <span className="text-yellow-600 dark:text-yellow-400 font-medium">
Warning:
</span>{" "}
This action is{" "}
- <span className="text-red-400 font-bold">irreversible</span>
+ <span className="text-red-600 dark:text-red-400 font-bold">
+ irreversible
+ </span>
. Once enabled, you cannot return to regular mode for this
project.
</DialogDescription>
@@ -545,7 +528,6 @@ export function ProjectSelector() {
whileTap={{ scale: 0.95 }}
>
<Button
- className="bg-white/5 hover:bg-white/10 border-white/10 text-white"
onClick={() =>
setExpDialog({ open: false, projectId: "" })
}
@@ -560,7 +542,7 @@ export function ProjectSelector() {
whileTap={{ scale: 0.95 }}
>
<Button
- className="bg-blue-600 hover:bg-blue-700 text-white"
+ className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white"
disabled={enableExperimentalMutation.isPending}
onClick={() =>
enableExperimentalMutation.mutate(expDialog.projectId)
@@ -584,5 +566,5 @@ export function ProjectSelector() {
)}
</AnimatePresence>
</div>
- );
+ )
}
diff --git a/apps/web/components/referral-upgrade-modal.tsx b/apps/web/components/referral-upgrade-modal.tsx
deleted file mode 100644
index 029bd2ae..00000000
--- a/apps/web/components/referral-upgrade-modal.tsx
+++ /dev/null
@@ -1,304 +0,0 @@
-"use client";
-
-import { useAuth } from "@lib/auth-context";
-import { fetchMemoriesFeature, fetchSubscriptionStatus } from "@lib/queries";
-import { Button } from "@repo/ui/components/button";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogTitle,
-} from "@repo/ui/components/dialog";
-import { Input } from "@repo/ui/components/input";
-import { Label } from "@repo/ui/components/label";
-import {
- Tabs,
- TabsContent,
- TabsList,
- TabsTrigger,
-} from "@repo/ui/components/tabs";
-import { HeadingH3Bold } from "@repo/ui/text/heading/heading-h3-bold";
-import { useCustomer } from "autumn-js/react";
-import {
- CheckCircle,
- Copy,
- CreditCard,
- Gift,
- LoaderIcon,
- Share2,
- Users,
-} from "lucide-react";
-import { motion } from "motion/react";
-import Link from "next/link";
-import { useState } from "react";
-
-interface ReferralUpgradeModalProps {
- isOpen: boolean;
- onClose: () => void;
-}
-
-export function ReferralUpgradeModal({
- isOpen,
- onClose,
-}: ReferralUpgradeModalProps) {
- const { user } = useAuth();
- const autumn = useCustomer();
- const [isLoading, setIsLoading] = useState(false);
- const [copied, setCopied] = useState(false);
-
- const { data: memoriesCheck } = fetchMemoriesFeature(autumn as any);
- const memoriesUsed = memoriesCheck?.usage ?? 0;
- const memoriesLimit = memoriesCheck?.included_usage ?? 0;
-
- // Fetch subscription status
- const {
- data: status = {
- consumer_pro: null,
- },
- isLoading: isCheckingStatus,
- } = fetchSubscriptionStatus(autumn as any);
-
- const isPro = status.consumer_pro;
-
- // Handle upgrade
- const handleUpgrade = async () => {
- setIsLoading(true);
- try {
- await autumn.attach({
- productId: "consumer_pro",
- successUrl: "https://app.supermemory.ai/",
- });
- window.location.reload();
- } catch (error) {
- console.error(error);
- setIsLoading(false);
- }
- };
-
- // Generate referral link (you'll need to implement this based on your referral system)
- const referralLink = `https://app.supermemory.ai/ref/${user?.id || "user"}`;
-
- const handleCopyReferralLink = async () => {
- try {
- await navigator.clipboard.writeText(referralLink);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
- } catch (error) {
- console.error("Failed to copy:", error);
- }
- };
-
- const handleShare = async () => {
- if (navigator.share) {
- try {
- await navigator.share({
- title: "Join Supermemory",
- text: "Check out Supermemory - the best way to organize and search your digital memories!",
- url: referralLink,
- });
- } catch (error) {
- console.error("Error sharing:", error);
- }
- } else {
- handleCopyReferralLink();
- }
- };
-
- if (user?.isAnonymous) {
- return (
- <Dialog open={isOpen} onOpenChange={onClose}>
- <DialogContent className="sm:max-w-md bg-[#0f1419] backdrop-blur-xl border-white/10 text-white">
- <motion.div
- animate={{ opacity: 1, scale: 1 }}
- exit={{ opacity: 0, scale: 0.95 }}
- initial={{ opacity: 0, scale: 0.95 }}
- >
- <DialogHeader>
- <DialogTitle>Get More Memories</DialogTitle>
- <DialogDescription className="text-white/60">
- Sign in to access referrals and upgrade options
- </DialogDescription>
- </DialogHeader>
-
- <div className="text-center py-6">
- <Button
- asChild
- className="bg-white/10 hover:bg-white/20 text-white border-white/20"
- >
- <Link href="/login">Sign in</Link>
- </Button>
- </div>
- </motion.div>
- </DialogContent>
- </Dialog>
- );
- }
-
- return (
- <Dialog open={isOpen} onOpenChange={onClose}>
- <DialogContent className="sm:max-w-lg bg-[#0f1419] backdrop-blur-xl border-white/10 text-white">
- <motion.div
- animate={{ opacity: 1, scale: 1 }}
- exit={{ opacity: 0, scale: 0.95 }}
- initial={{ opacity: 0, scale: 0.95 }}
- >
- <DialogHeader className="mb-4">
- <DialogTitle>Get More Memories</DialogTitle>
- <DialogDescription className="text-white/60">
- Expand your memory capacity through referrals or upgrades
- </DialogDescription>
- </DialogHeader>
-
- {/* Current Usage */}
- <div className="bg-white/5 rounded-lg p-4 mb-6">
- <div className="flex justify-between items-center mb-2">
- <span className="text-sm text-white/70">Current Usage</span>
- <span
- className={`text-sm ${memoriesUsed >= memoriesLimit ? "text-red-400" : "text-white/90"}`}
- >
- {memoriesUsed} / {memoriesLimit} memories
- </span>
- </div>
- <div className="w-full bg-white/10 rounded-full h-2">
- <div
- className={`h-2 rounded-full transition-all ${
- memoriesUsed >= memoriesLimit
- ? "bg-red-500"
- : isPro
- ? "bg-green-500"
- : "bg-blue-500"
- }`}
- style={{
- width: `${Math.min((memoriesUsed / memoriesLimit) * 100, 100)}%`,
- }}
- />
- </div>
- </div>
-
- {/* Tabs */}
- <Tabs defaultValue="refer" className="w-full">
- <TabsList className="grid w-full grid-cols-2 bg-white/5">
- <TabsTrigger value="refer" className="flex items-center gap-2">
- <Users className="w-4 h-4" />
- Refer Friends
- </TabsTrigger>
- {!isPro && (
- <TabsTrigger
- value="upgrade"
- className="flex items-center gap-2"
- >
- <CreditCard className="w-4 h-4" />
- Upgrade Plan
- </TabsTrigger>
- )}
- </TabsList>
-
- <TabsContent value="refer" className="space-y-4 mt-6">
- <div className="text-center">
- <Gift className="w-12 h-12 text-blue-400 mx-auto mb-3" />
- <HeadingH3Bold className="text-white mb-2">
- Invite Friends, Get More Memories
- </HeadingH3Bold>
- <p className="text-white/70 text-sm">
- For every friend who joins, you both get +5 extra memories!
- </p>
- </div>
-
- <div className="bg-white/5 rounded-lg p-4">
- <Label className="text-sm text-white/70 mb-2 block">
- Your Referral Link
- </Label>
- <div className="flex gap-2">
- <Input
- className="flex-1 bg-white/10 border-white/20 text-white"
- readOnly
- value={referralLink}
- />
- <Button
- className="bg-white/5 hover:bg-white/10 text-white border-white/20"
- onClick={handleCopyReferralLink}
- size="sm"
- variant="outline"
- >
- {copied ? (
- <CheckCircle className="w-4 h-4" />
- ) : (
- <Copy className="w-4 h-4" />
- )}
- </Button>
- </div>
- </div>
-
- <Button
- className="w-full bg-white/5 hover:bg-white/10 text-white border-white/20"
- onClick={handleShare}
- variant="outline"
- >
- <Share2 className="w-4 h-4 mr-2" />
- Share Link
- </Button>
- </TabsContent>
-
- {!isPro && (
- <TabsContent value="upgrade" className="space-y-4 mt-6">
- <div className="text-center">
- <CreditCard className="w-12 h-12 text-purple-400 mx-auto mb-3" />
- <HeadingH3Bold className="text-white mb-2">
- Upgrade to Pro
- </HeadingH3Bold>
- <p className="text-white/70 text-sm">
- Get unlimited memories and advanced features
- </p>
- </div>
-
- <div className="bg-white/5 rounded-lg border border-white/10 p-4">
- <h4 className="font-medium text-white mb-3">
- Pro Plan Benefits
- </h4>
- <ul className="space-y-2">
- <li className="flex items-center gap-2 text-sm text-white/90">
- <CheckCircle className="h-4 w-4 text-green-400" />
- 500 memories (vs {memoriesLimit} free)
- </li>
- <li className="flex items-center gap-2 text-sm text-white/90">
- <CheckCircle className="h-4 w-4 text-green-400" />
- 10 connections
- </li>
- <li className="flex items-center gap-2 text-sm text-white/90">
- <CheckCircle className="h-4 w-4 text-green-400" />
- Advanced search
- </li>
- <li className="flex items-center gap-2 text-sm text-white/90">
- <CheckCircle className="h-4 w-4 text-green-400" />
- Priority support
- </li>
- </ul>
- </div>
-
- <Button
- className="w-full bg-blue-600 hover:bg-blue-700 text-white"
- disabled={isLoading || isCheckingStatus}
- onClick={handleUpgrade}
- >
- {isLoading || isCheckingStatus ? (
- <>
- <LoaderIcon className="h-4 w-4 animate-spin mr-2" />
- Upgrading...
- </>
- ) : (
- "Upgrade to Pro - $15/month"
- )}
- </Button>
-
- <p className="text-xs text-white/50 text-center">
- Cancel anytime. No questions asked.
- </p>
- </TabsContent>
- )}
- </Tabs>
- </motion.div>
- </DialogContent>
- </Dialog>
- );
-}
diff --git a/apps/web/components/text-shimmer.tsx b/apps/web/components/text-shimmer.tsx
index 1825d08c..815200fe 100644
--- a/apps/web/components/text-shimmer.tsx
+++ b/apps/web/components/text-shimmer.tsx
@@ -38,7 +38,7 @@ function TextShimmerComponent({
initial={{ backgroundPosition: "100% center" }}
animate={{ backgroundPosition: "0% center" }}
transition={{
- repeat: Infinity,
+ repeat: Number.POSITIVE_INFINITY,
duration,
ease: "linear",
}}
diff --git a/apps/web/components/views/add-memory/action-buttons.tsx b/apps/web/components/views/add-memory/action-buttons.tsx
index 6dc49304..fc901ba9 100644
--- a/apps/web/components/views/add-memory/action-buttons.tsx
+++ b/apps/web/components/views/add-memory/action-buttons.tsx
@@ -26,7 +26,7 @@ export function ActionButtons({
return (
<div className={`flex gap-3 order-1 sm:order-2 justify-end ${className}`}>
<Button
- className="hover:bg-white/10 text-white border-none flex-1 sm:flex-initial"
+ className="hover:bg-foreground/10 border-none flex-1 sm:flex-initial"
onClick={onCancel}
type="button"
variant="ghost"
@@ -40,7 +40,7 @@ export function ActionButtons({
className="flex-1 sm:flex-initial"
>
<Button
- className="bg-white/10 hover:bg-white/20 text-white border-white/20 w-full"
+ className="bg-foreground hover:bg-foreground/20 border-foreground/20 w-full"
disabled={isSubmitting || isSubmitDisabled}
onClick={submitType === 'button' ? onSubmit : undefined}
type={submitType}
diff --git a/apps/web/components/views/add-memory/index.tsx b/apps/web/components/views/add-memory/index.tsx
index 8bd6a0f0..a78e7629 100644
--- a/apps/web/components/views/add-memory/index.tsx
+++ b/apps/web/components/views/add-memory/index.tsx
@@ -1,8 +1,6 @@
+"use client"
import { $fetch } from "@lib/api"
-import {
- fetchConsumerProProduct,
- fetchMemoriesFeature,
-} from "@repo/lib/queries"
+import { fetchMemoriesFeature } from "@repo/lib/queries"
import { Button } from "@repo/ui/components/button"
import {
Dialog,
@@ -12,6 +10,12 @@ import {
DialogHeader,
DialogTitle,
} from "@repo/ui/components/dialog"
+import {
+ Tabs,
+ TabsList,
+ TabsTrigger,
+ TabsContent,
+} from "@repo/ui/components/tabs"
import { Input } from "@repo/ui/components/input"
import { Label } from "@repo/ui/components/label"
import { Textarea } from "@repo/ui/components/textarea"
@@ -43,13 +47,12 @@ import { ConnectionsTabContent } from "../connections-tab-content"
import { ActionButtons } from "./action-buttons"
import { MemoryUsageRing } from "./memory-usage-ring"
import { ProjectSelection } from "./project-selection"
-import { TabButton } from "./tab-button"
const TextEditor = dynamic(
() => import("./text-editor").then((mod) => ({ default: mod.TextEditor })),
{
loading: () => (
- <div className="bg-white/5 border border-white/10 rounded-md">
+ <div className="bg-foreground/5 border border-foreground/10 rounded-md">
<div className="flex-1 min-h-48 max-h-64 overflow-y-auto flex items-center justify-center text-white/70">
Loading editor...
</div>
@@ -66,28 +69,6 @@ const TextEditor = dynamic(
},
)
-// // Processing status component
-// function ProcessingStatus({ status }: { status: string }) {
-// const statusConfig = {
-// queued: { color: "text-yellow-400", label: "Queued", icon: "⏳" },
-// extracting: { color: "text-blue-400", label: "Extracting", icon: "📤" },
-// chunking: { color: "text-indigo-400", label: "Chunking", icon: "✂️" },
-// embedding: { color: "text-purple-400", label: "Embedding", icon: "🧠" },
-// indexing: { color: "text-pink-400", label: "Indexing", icon: "📝" },
-// unknown: { color: "text-gray-400", label: "Processing", icon: "⚙️" },
-// }
-
-// const config =
-// statusConfig[status as keyof typeof statusConfig] || statusConfig.unknown
-
-// return (
-// <div className={`flex items-center gap-1 text-xs ${config.color}`}>
-// <span>{config.icon}</span>
-// <span>{config.label}</span>
-// </div>
-// )
-// }
-
export function AddMemoryView({
onClose,
initialTab = "note",
@@ -107,7 +88,7 @@ export function AddMemoryView({
const [newProjectName, setNewProjectName] = useState("")
// Check memory limits
- const { data: memoriesCheck } = fetchMemoriesFeature(autumn)
+ const { data: memoriesCheck } = fetchMemoriesFeature(autumn, !autumn.isLoading)
const memoriesUsed = memoriesCheck?.usage ?? 0
const memoriesLimit = memoriesCheck?.included_usage ?? 0
@@ -560,460 +541,452 @@ export function AddMemoryView({
open={showAddDialog}
>
<DialogContent
- className="w-[95vw] max-w-3xl sm:max-w-3xl bg-[#0f1419] backdrop-blur-xl border-white/10 text-white z-[80] max-h-[90vh] overflow-y-auto"
+ className="w-[100vw] max-w-4xl sm:max-w-4xl backdrop-blur-xl border-white/10 z-[80] h-[52vh] overflow-y-auto p-4"
showCloseButton={false}
>
- <motion.div
- animate={{ opacity: 1, scale: 1 }}
- exit={{ opacity: 0, scale: 0.95 }}
- initial={{ opacity: 0, scale: 0.95 }}
+ <Tabs
+ value={activeTab}
+ onValueChange={(value) => setActiveTab(value as typeof activeTab)}
+ className="flex flex-row gap-4"
+ orientation="vertical"
>
- <DialogHeader>
- <div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
- <div className="flex-1">
- <DialogTitle className="text-base">
- Add to Memory
- </DialogTitle>
- <DialogDescription className="text-white/50">
- Save any webpage, article, or file to your memory
- </DialogDescription>
+ <TabsList className="flex flex-col gap-2 max-w-48 h-fit bg-transparent p-0">
+ <TabsTrigger
+ value="note"
+ className="flex flex-col gap-1 justify-start items-start h-auto w-full"
+ >
+ <div className="flex gap-1 items-center">
+ <Brain className="h-4 w-4" />
+ Note
</div>
- <div className="sm:ml-4 order-first sm:order-last">
- <div className="bg-white/5 p-1 h-10 sm:h-8 rounded-md flex overflow-x-auto">
- <TabButton
- icon={Brain}
- isActive={activeTab === "note"}
- label="Note"
- onClick={() => setActiveTab("note")}
- />
- <TabButton
- icon={LinkIcon}
- isActive={activeTab === "link"}
- label="Link"
- onClick={() => setActiveTab("link")}
- />
- <TabButton
- icon={FileIcon}
- isActive={activeTab === "file"}
- label="File"
- onClick={() => setActiveTab("file")}
- />
- <TabButton
- icon={PlugIcon}
- isActive={activeTab === "connect"}
- label="Connect"
- onClick={() => setActiveTab("connect")}
+ <span className="text-xs text-muted-foreground text-wrap text-left">
+ Write down your thoughts
+ </span>
+ </TabsTrigger>
+ <TabsTrigger
+ value="link"
+ className="flex flex-col gap-1 justify-start items-start h-auto w-full"
+ >
+ <div className="flex gap-1 items-center">
+ <LinkIcon className="h-4 w-4" />
+ Link
+ </div>
+ <span className="text-xs text-muted-foreground text-wrap text-left">
+ Save any webpage
+ </span>
+ </TabsTrigger>
+ <TabsTrigger
+ value="file"
+ className="flex flex-col gap-1 justify-start items-start h-auto w-full"
+ >
+ <div className="flex gap-1 items-center">
+ <FileIcon className="h-4 w-4" />
+ File
+ </div>
+ <span className="text-xs text-muted-foreground text-wrap text-left">
+ Upload any file
+ </span>
+ </TabsTrigger>
+ <TabsTrigger
+ value="connect"
+ className="flex flex-col gap-1 justify-start items-start h-auto w-full"
+ >
+ <div className="flex gap-1 items-center">
+ <PlugIcon className="h-4 w-4" />
+ Connect
+ </div>
+ <span className="text-xs text-muted-foreground text-wrap text-left">
+ Connect to your favorite apps
+ </span>
+ </TabsTrigger>
+ </TabsList>
+
+ <TabsContent value="note" className="space-y-4">
+ <form
+ onSubmit={(e) => {
+ e.preventDefault()
+ e.stopPropagation()
+ addContentForm.handleSubmit()
+ }}
+ className="h-full flex flex-col"
+ >
+ <div className="grid gap-4">
+ {/* Note Input */}
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className="flex flex-col gap-2"
+ initial={{ opacity: 0, y: 10 }}
+ transition={{ delay: 0.1 }}
+ >
+ <addContentForm.Field
+ name="content"
+ validators={{
+ onChange: ({ value }) => {
+ if (!value || value.trim() === "") {
+ return "Note is required"
+ }
+ return undefined
+ },
+ }}
+ >
+ {({ state, handleChange, handleBlur }) => (
+ <>
+ <div
+ className={`bg-black/5 border border-black/10 rounded-md ${
+ addContentMutation.isPending ? "opacity-50" : ""
+ }`}
+ >
+ <TextEditor
+ disabled={addContentMutation.isPending}
+ onBlur={handleBlur}
+ onChange={handleChange}
+ placeholder="Write your note here..."
+ value={state.value}
+ />
+ </div>
+ {state.meta.errors.length > 0 && (
+ <motion.p
+ animate={{ opacity: 1, height: "auto" }}
+ className="text-sm text-red-400 mt-1"
+ exit={{ opacity: 0, height: 0 }}
+ initial={{ opacity: 0, height: 0 }}
+ >
+ {state.meta.errors
+ .map((error) =>
+ typeof error === "string"
+ ? error
+ : (error?.message ??
+ `Error: ${JSON.stringify(error)}`),
+ )
+ .join(", ")}
+ </motion.p>
+ )}
+ </>
+ )}
+ </addContentForm.Field>
+ </motion.div>
+ </div>
+ <div className="flex flex-col sm:flex-row sm:justify-between sm:items-end w-full gap-4 mt-auto">
+ <div className="flex flex-col sm:flex-row sm:items-end gap-4 order-2 sm:order-1">
+ {/* Project Selection */}
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className={`flex flex-col gap-2 flex-1 sm:flex-initial ${
+ addContentMutation.isPending ? "opacity-50" : ""
+ }`}
+ initial={{ opacity: 0, y: 10 }}
+ transition={{ delay: 0.15 }}
+ >
+ <addContentForm.Field name="project">
+ {({ state, handleChange }) => (
+ <ProjectSelection
+ disabled={addContentMutation.isPending}
+ id="note-project"
+ isLoading={isLoadingProjects}
+ onCreateProject={() =>
+ setShowCreateProjectDialog(true)
+ }
+ onProjectChange={handleChange}
+ projects={projects}
+ selectedProject={state.value}
+ />
+ )}
+ </addContentForm.Field>
+ </motion.div>
+
+ <MemoryUsageRing
+ memoriesLimit={memoriesLimit}
+ memoriesUsed={memoriesUsed}
/>
</div>
- </div>
- </div>
- </DialogHeader>
- <div className="mt-4">
- {activeTab === "note" && (
- <div className="space-y-4">
- <form
- onSubmit={(e) => {
- e.preventDefault()
- e.stopPropagation()
- addContentForm.handleSubmit()
+ <ActionButtons
+ isSubmitDisabled={!addContentForm.state.canSubmit}
+ isSubmitting={addContentMutation.isPending}
+ onCancel={() => {
+ setShowAddDialog(false)
+ onClose?.()
+ addContentForm.reset()
}}
+ submitIcon={Plus}
+ submitText="Add Note"
+ />
+ </div>
+ </form>
+ </TabsContent>
+
+ <TabsContent value="link" className="space-y-4">
+ <form
+ onSubmit={(e) => {
+ e.preventDefault()
+ e.stopPropagation()
+ addContentForm.handleSubmit()
+ }}
+ className="h-full flex flex-col"
+ >
+ <div className="grid gap-4">
+ {/* Link Input */}
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className="flex flex-col gap-2"
+ initial={{ opacity: 0, y: 10 }}
+ transition={{ delay: 0.1 }}
>
- <div className="grid gap-4">
- {/* Note Input */}
- <motion.div
- animate={{ opacity: 1, y: 0 }}
- className="flex flex-col gap-2"
- initial={{ opacity: 0, y: 10 }}
- transition={{ delay: 0.1 }}
- >
- <addContentForm.Field
- name="content"
- validators={{
- onChange: ({ value }) => {
- if (!value || value.trim() === "") {
- return "Note is required"
- }
- return undefined
- },
- }}
- >
- {({ state, handleChange, handleBlur }) => (
- <>
- <div
- className={`bg-white/5 border border-white/10 rounded-md ${
- addContentMutation.isPending
- ? "opacity-50"
- : ""
- }`}
- >
- <TextEditor
- className="text-white"
- disabled={addContentMutation.isPending}
- onBlur={handleBlur}
- onChange={handleChange}
- placeholder="Write your note here..."
- value={state.value}
- />
- </div>
- {state.meta.errors.length > 0 && (
- <motion.p
- animate={{ opacity: 1, height: "auto" }}
- className="text-sm text-red-400 mt-1"
- exit={{ opacity: 0, height: 0 }}
- initial={{ opacity: 0, height: 0 }}
- >
- {state.meta.errors
- .map((error) =>
- typeof error === "string"
- ? error
- : (error?.message ??
- `Error: ${JSON.stringify(error)}`),
- )
- .join(", ")}
- </motion.p>
- )}
- </>
+ <label
+ className="text-sm font-medium"
+ htmlFor="link-content"
+ >
+ Link
+ </label>
+ <addContentForm.Field
+ name="content"
+ validators={{
+ onChange: ({ value }) => {
+ if (!value || value.trim() === "") {
+ return "Link is required"
+ }
+ try {
+ new URL(value)
+ return undefined
+ } catch {
+ return "Please enter a valid link"
+ }
+ },
+ }}
+ >
+ {({ state, handleChange, handleBlur }) => (
+ <>
+ <Input
+ className={`bg-black/5 border-black/10 text-black ${
+ addContentMutation.isPending ? "opacity-50" : ""
+ }`}
+ disabled={addContentMutation.isPending}
+ id="link-content"
+ onBlur={handleBlur}
+ onChange={(e) => handleChange(e.target.value)}
+ placeholder="https://example.com/article"
+ value={state.value}
+ />
+ {state.meta.errors.length > 0 && (
+ <motion.p
+ animate={{ opacity: 1, height: "auto" }}
+ className="text-sm text-red-400 mt-1"
+ exit={{ opacity: 0, height: 0 }}
+ initial={{ opacity: 0, height: 0 }}
+ >
+ {state.meta.errors
+ .map((error) =>
+ typeof error === "string"
+ ? error
+ : (error?.message ??
+ `Error: ${JSON.stringify(error)}`),
+ )
+ .join(", ")}
+ </motion.p>
)}
- </addContentForm.Field>
- </motion.div>
- </div>
- <div className="mt-6 flex flex-col sm:flex-row sm:justify-between sm:items-end w-full gap-4">
- <div className="flex flex-col sm:flex-row sm:items-end gap-4 order-2 sm:order-1">
- {/* Project Selection */}
- <motion.div
- animate={{ opacity: 1, y: 0 }}
- className={`flex flex-col gap-2 flex-1 sm:flex-initial ${
- addContentMutation.isPending ? "opacity-50" : ""
- }`}
- initial={{ opacity: 0, y: 10 }}
- transition={{ delay: 0.15 }}
- >
- <addContentForm.Field name="project">
- {({ state, handleChange }) => (
- <ProjectSelection
- disabled={addContentMutation.isPending}
- id="note-project"
- isLoading={isLoadingProjects}
- onCreateProject={() =>
- setShowCreateProjectDialog(true)
- }
- onProjectChange={handleChange}
- projects={projects}
- selectedProject={state.value}
- />
- )}
- </addContentForm.Field>
- </motion.div>
-
- <MemoryUsageRing
- memoriesLimit={memoriesLimit}
- memoriesUsed={memoriesUsed}
- />
- </div>
-
- <ActionButtons
- isSubmitDisabled={!addContentForm.state.canSubmit}
- isSubmitting={addContentMutation.isPending}
- onCancel={() => {
- setShowAddDialog(false)
- onClose?.()
- addContentForm.reset()
- }}
- submitIcon={Plus}
- submitText="Add Note"
- />
- </div>
- </form>
+ </>
+ )}
+ </addContentForm.Field>
+ </motion.div>
</div>
- )}
-
- {activeTab === "link" && (
- <div className="space-y-4">
- <form
- onSubmit={(e) => {
- e.preventDefault()
- e.stopPropagation()
- addContentForm.handleSubmit()
+ <div className="mt-auto flex justify-between items-end w-full">
+ <div className="flex items-end gap-4">
+ {/* Left side - Project Selection */}
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className={`flex flex-col gap-2 ${
+ addContentMutation.isPending ? "opacity-50" : ""
+ }`}
+ initial={{ opacity: 0, y: 10 }}
+ transition={{ delay: 0.15 }}
+ >
+ <addContentForm.Field name="project">
+ {({ state, handleChange }) => (
+ <ProjectSelection
+ disabled={addContentMutation.isPending}
+ id="link-project-2"
+ isLoading={isLoadingProjects}
+ onCreateProject={() =>
+ setShowCreateProjectDialog(true)
+ }
+ onProjectChange={handleChange}
+ projects={projects}
+ selectedProject={state.value}
+ />
+ )}
+ </addContentForm.Field>
+ </motion.div>
+
+ <MemoryUsageRing
+ memoriesLimit={memoriesLimit}
+ memoriesUsed={memoriesUsed}
+ />
+ </div>
+
+ <ActionButtons
+ isSubmitDisabled={!addContentForm.state.canSubmit}
+ isSubmitting={addContentMutation.isPending}
+ onCancel={() => {
+ setShowAddDialog(false)
+ onClose?.()
+ addContentForm.reset()
}}
+ submitIcon={Plus}
+ submitText="Add Link"
+ />
+ </div>
+ </form>
+ </TabsContent>
+
+ <TabsContent value="file" className="space-y-4">
+ <form
+ onSubmit={(e) => {
+ e.preventDefault()
+ e.stopPropagation()
+ fileUploadForm.handleSubmit()
+ }}
+ >
+ <div className="grid gap-4">
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className="flex flex-col gap-2"
+ initial={{ opacity: 0, y: 10 }}
+ transition={{ delay: 0.1 }}
>
- <div className="grid gap-4">
- {/* Link Input */}
- <motion.div
- animate={{ opacity: 1, y: 0 }}
- className="flex flex-col gap-2"
- initial={{ opacity: 0, y: 10 }}
- transition={{ delay: 0.1 }}
- >
- <label
- className="text-sm font-medium"
- htmlFor="link-content"
- >
- Link
- </label>
- <addContentForm.Field
- name="content"
- validators={{
- onChange: ({ value }) => {
- if (!value || value.trim() === "") {
- return "Link is required"
- }
- try {
- new URL(value)
- return undefined
- } catch {
- return "Please enter a valid link"
- }
- },
- }}
- >
- {({ state, handleChange, handleBlur }) => (
- <>
- <Input
- className={`bg-white/5 border-white/10 text-white ${
- addContentMutation.isPending
- ? "opacity-50"
- : ""
- }`}
- disabled={addContentMutation.isPending}
- id="link-content"
- onBlur={handleBlur}
- onChange={(e) => handleChange(e.target.value)}
- placeholder="https://example.com/article"
- value={state.value}
- />
- {state.meta.errors.length > 0 && (
- <motion.p
- animate={{ opacity: 1, height: "auto" }}
- className="text-sm text-red-400 mt-1"
- exit={{ opacity: 0, height: 0 }}
- initial={{ opacity: 0, height: 0 }}
- >
- {state.meta.errors
- .map((error) =>
- typeof error === "string"
- ? error
- : (error?.message ??
- `Error: ${JSON.stringify(error)}`),
- )
- .join(", ")}
- </motion.p>
- )}
- </>
- )}
- </addContentForm.Field>
- </motion.div>
- </div>
- <div className="mt-6 flex justify-between items-end w-full">
- <div className="flex items-end gap-4">
- {/* Left side - Project Selection */}
- <motion.div
- animate={{ opacity: 1, y: 0 }}
- className={`flex flex-col gap-2 ${
- addContentMutation.isPending ? "opacity-50" : ""
- }`}
- initial={{ opacity: 0, y: 10 }}
- transition={{ delay: 0.15 }}
- >
- <addContentForm.Field name="project">
- {({ state, handleChange }) => (
- <ProjectSelection
- disabled={addContentMutation.isPending}
- id="link-project-2"
- isLoading={isLoadingProjects}
- onCreateProject={() =>
- setShowCreateProjectDialog(true)
- }
- onProjectChange={handleChange}
- projects={projects}
- selectedProject={state.value}
- />
- )}
- </addContentForm.Field>
- </motion.div>
-
- <MemoryUsageRing
- memoriesLimit={memoriesLimit}
- memoriesUsed={memoriesUsed}
+ <label className="text-sm font-medium" htmlFor="file">
+ File
+ </label>
+ <Dropzone
+ accept={{
+ "application/pdf": [".pdf"],
+ "application/msword": [".doc"],
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
+ [".docx"],
+ "text/plain": [".txt"],
+ "text/markdown": [".md"],
+ "text/csv": [".csv"],
+ "application/json": [".json"],
+ "image/*": [".png", ".jpg", ".jpeg", ".gif", ".webp"],
+ }}
+ className="bg-black/5 border-black/10 hover:bg-black/10 min-h-40"
+ maxFiles={10}
+ maxSize={10 * 1024 * 1024} // 10MB
+ onDrop={(acceptedFiles) =>
+ setSelectedFiles(acceptedFiles)
+ }
+ src={selectedFiles}
+ >
+ <DropzoneEmptyState />
+ <DropzoneContent className="overflow-auto" />
+ </Dropzone>
+ </motion.div>
+
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className="flex flex-col gap-2"
+ initial={{ opacity: 0, y: 10 }}
+ transition={{ delay: 0.15 }}
+ >
+ <label
+ className="text-sm font-medium"
+ htmlFor="file-title"
+ >
+ Title (optional)
+ </label>
+ <fileUploadForm.Field name="title">
+ {({ state, handleChange, handleBlur }) => (
+ <Input
+ className="bg-black/5 border-black/10"
+ id="file-title"
+ onBlur={handleBlur}
+ onChange={(e) => handleChange(e.target.value)}
+ placeholder="Give this file a title"
+ value={state.value}
/>
- </div>
-
- <ActionButtons
- isSubmitDisabled={!addContentForm.state.canSubmit}
- isSubmitting={addContentMutation.isPending}
- onCancel={() => {
- setShowAddDialog(false)
- onClose?.()
- addContentForm.reset()
- }}
- submitIcon={Plus}
- submitText="Add Link"
- />
- </div>
- </form>
- </div>
- )}
-
- {activeTab === "file" && (
- <div className="space-y-4">
- <form
- onSubmit={(e) => {
- e.preventDefault()
- e.stopPropagation()
- fileUploadForm.handleSubmit()
- }}
+ )}
+ </fileUploadForm.Field>
+ </motion.div>
+
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className="flex flex-col gap-2"
+ initial={{ opacity: 0, y: 10 }}
+ transition={{ delay: 0.2 }}
>
- <div className="grid gap-4">
- <motion.div
- animate={{ opacity: 1, y: 0 }}
- className="flex flex-col gap-2"
- initial={{ opacity: 0, y: 10 }}
- transition={{ delay: 0.1 }}
- >
- <label className="text-sm font-medium" htmlFor="file">
- File
- </label>
- <Dropzone
- accept={{
- "application/pdf": [".pdf"],
- "application/msword": [".doc"],
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
- [".docx"],
- "text/plain": [".txt"],
- "text/markdown": [".md"],
- "text/csv": [".csv"],
- "application/json": [".json"],
- "image/*": [
- ".png",
- ".jpg",
- ".jpeg",
- ".gif",
- ".webp",
- ],
- }}
- className="bg-white/5 border-white/10 hover:bg-white/10 min-h-40"
- maxFiles={10}
- maxSize={10 * 1024 * 1024} // 10MB
- onDrop={(acceptedFiles) =>
- setSelectedFiles(acceptedFiles)
- }
- src={selectedFiles}
- >
- <DropzoneEmptyState />
- <DropzoneContent className="overflow-auto" />
- </Dropzone>
- </motion.div>
-
- <motion.div
- animate={{ opacity: 1, y: 0 }}
- className="flex flex-col gap-2"
- initial={{ opacity: 0, y: 10 }}
- transition={{ delay: 0.15 }}
- >
- <label
- className="text-sm font-medium"
- htmlFor="file-title"
- >
- Title (optional)
- </label>
- <fileUploadForm.Field name="title">
- {({ state, handleChange, handleBlur }) => (
- <Input
- className="bg-white/5 border-white/10 text-white"
- id="file-title"
- onBlur={handleBlur}
- onChange={(e) => handleChange(e.target.value)}
- placeholder="Give this file a title"
- value={state.value}
- />
- )}
- </fileUploadForm.Field>
- </motion.div>
-
- <motion.div
- animate={{ opacity: 1, y: 0 }}
- className="flex flex-col gap-2"
- initial={{ opacity: 0, y: 10 }}
- transition={{ delay: 0.2 }}
- >
- <label
- className="text-sm font-medium"
- htmlFor="file-description"
- >
- Description (optional)
- </label>
- <fileUploadForm.Field name="description">
- {({ state, handleChange, handleBlur }) => (
- <Textarea
- className="bg-white/5 border-white/10 text-white min-h-20 max-h-40 overflow-y-auto resize-none"
- id="file-description"
- onBlur={handleBlur}
- onChange={(e) => handleChange(e.target.value)}
- placeholder="Add notes or context about this file"
- value={state.value}
- />
- )}
- </fileUploadForm.Field>
- </motion.div>
- </div>
- <div className="mt-6 flex flex-col sm:flex-row sm:justify-between sm:items-end w-full gap-4">
- <div className="flex items-end gap-4">
- {/* Left side - Project Selection */}
- <motion.div
- animate={{ opacity: 1, y: 0 }}
- className={`flex flex-col gap-2 flex-1 sm:flex-initial ${
- fileUploadMutation.isPending ? "opacity-50" : ""
- }`}
- initial={{ opacity: 0, y: 10 }}
- transition={{ delay: 0.25 }}
- >
- <fileUploadForm.Field name="project">
- {({ state, handleChange }) => (
- <ProjectSelection
- disabled={fileUploadMutation.isPending}
- id="file-project"
- isLoading={isLoadingProjects}
- onCreateProject={() =>
- setShowCreateProjectDialog(true)
- }
- onProjectChange={handleChange}
- projects={projects}
- selectedProject={state.value}
- />
- )}
- </fileUploadForm.Field>
- </motion.div>
-
- <MemoryUsageRing
- memoriesLimit={memoriesLimit}
- memoriesUsed={memoriesUsed}
+ <label
+ className="text-sm font-medium"
+ htmlFor="file-description"
+ >
+ Description (optional)
+ </label>
+ <fileUploadForm.Field name="description">
+ {({ state, handleChange, handleBlur }) => (
+ <Textarea
+ className="bg-black/5 border-black/10 min-h-20 max-h-40 overflow-y-auto resize-none"
+ id="file-description"
+ onBlur={handleBlur}
+ onChange={(e) => handleChange(e.target.value)}
+ placeholder="Add notes or context about this file"
+ value={state.value}
/>
- </div>
-
- <ActionButtons
- isSubmitDisabled={selectedFiles.length === 0}
- isSubmitting={fileUploadMutation.isPending}
- onCancel={() => {
- setShowAddDialog(false)
- onClose?.()
- fileUploadForm.reset()
- setSelectedFiles([])
- }}
- submitIcon={UploadIcon}
- submitText="Upload File"
- />
- </div>
- </form>
+ )}
+ </fileUploadForm.Field>
+ </motion.div>
</div>
- )}
+ <div className="mt-6 flex flex-col sm:flex-row sm:justify-between sm:items-end w-full gap-4">
+ <div className="flex items-end gap-4">
+ {/* Left side - Project Selection */}
+ <motion.div
+ animate={{ opacity: 1, y: 0 }}
+ className={`flex flex-col gap-2 flex-1 sm:flex-initial ${
+ fileUploadMutation.isPending ? "opacity-50" : ""
+ }`}
+ initial={{ opacity: 0, y: 10 }}
+ transition={{ delay: 0.25 }}
+ >
+ <fileUploadForm.Field name="project">
+ {({ state, handleChange }) => (
+ <ProjectSelection
+ disabled={fileUploadMutation.isPending}
+ id="file-project"
+ isLoading={isLoadingProjects}
+ onCreateProject={() =>
+ setShowCreateProjectDialog(true)
+ }
+ onProjectChange={handleChange}
+ projects={projects}
+ selectedProject={state.value}
+ />
+ )}
+ </fileUploadForm.Field>
+ </motion.div>
+
+ <MemoryUsageRing
+ memoriesLimit={memoriesLimit}
+ memoriesUsed={memoriesUsed}
+ />
+ </div>
- {activeTab === "connect" && (
- <div className="space-y-4">
- <ConnectionsTabContent />
+ <ActionButtons
+ isSubmitDisabled={selectedFiles.length === 0}
+ isSubmitting={fileUploadMutation.isPending}
+ onCancel={() => {
+ setShowAddDialog(false)
+ onClose?.()
+ fileUploadForm.reset()
+ setSelectedFiles([])
+ }}
+ submitIcon={UploadIcon}
+ submitText="Upload File"
+ />
</div>
- )}
- </div>
- </motion.div>
+ </form>
+ </TabsContent>
+
+ <TabsContent value="connect" className="space-y-4">
+ <ConnectionsTabContent />
+ </TabsContent>
+ </Tabs>
</DialogContent>
</Dialog>
)}
@@ -1025,7 +998,7 @@ export function AddMemoryView({
onOpenChange={setShowCreateProjectDialog}
open={showCreateProjectDialog}
>
- <DialogContent className="w-[95vw] max-w-2xl sm:max-w-2xl bg-black/90 backdrop-blur-xl border-white/10 text-white z-[80] max-h-[90vh] overflow-y-auto">
+ <DialogContent className="w-[95vw] max-w-2xl sm:max-w-2xl bg-black/90 backdrop-blur-xl border-white/10 z-[80] max-h-[90vh] overflow-y-auto">
<motion.div
animate={{ opacity: 1, scale: 1 }}
initial={{ opacity: 0, scale: 0.95 }}
@@ -1045,7 +1018,7 @@ export function AddMemoryView({
>
<Label htmlFor="projectName">Project Name</Label>
<Input
- className="bg-white/5 border-white/10 text-white"
+ className="bg-white/5 border-white/10"
id="projectName"
onChange={(e) => setNewProjectName(e.target.value)}
placeholder="My Awesome Project"
@@ -1063,7 +1036,7 @@ export function AddMemoryView({
whileTap={{ scale: 0.95 }}
>
<Button
- className="bg-white/5 hover:bg-white/10 border-white/10 text-white w-full sm:w-auto"
+ className="bg-white/5 hover:bg-white/10 border-white/10 w-full sm:w-auto"
onClick={() => {
setShowCreateProjectDialog(false)
setNewProjectName("")
@@ -1080,7 +1053,7 @@ export function AddMemoryView({
whileTap={{ scale: 0.95 }}
>
<Button
- className="bg-white/10 hover:bg-white/20 text-white border-white/20 w-full sm:w-auto"
+ className="bg-white/10 hover:bg-white/20 border-white/20 w-full sm:w-auto"
disabled={
createProjectMutation.isPending || !newProjectName.trim()
}
@@ -1131,7 +1104,7 @@ export function AddMemoryExpandedView() {
<div className="flex flex-wrap gap-2">
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<Button
- className="bg-white/10 hover:bg-white/20 text-white border-white/20"
+ className="bg-white/10 hover:bg-white/20 border-white/20"
onClick={() => handleOpenDialog("note")}
size="sm"
variant="outline"
@@ -1143,7 +1116,7 @@ export function AddMemoryExpandedView() {
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<Button
- className="bg-white/10 hover:bg-white/20 text-white border-white/20"
+ className="bg-white/10 hover:bg-white/20 border-white/20"
onClick={() => handleOpenDialog("link")}
size="sm"
variant="outline"
@@ -1155,7 +1128,7 @@ export function AddMemoryExpandedView() {
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<Button
- className="bg-white/10 hover:bg-white/20 text-white border-white/20"
+ className="bg-white/10 hover:bg-white/20 border-white/20"
onClick={() => handleOpenDialog("file")}
size="sm"
variant="outline"
@@ -1167,7 +1140,7 @@ export function AddMemoryExpandedView() {
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<Button
- className="bg-white/10 hover:bg-white/20 text-white border-white/20"
+ className="bg-white/10 hover:bg-white/20 border-white/20"
onClick={() => handleOpenDialog("connect")}
size="sm"
variant="outline"
diff --git a/apps/web/components/views/add-memory/project-selection.tsx b/apps/web/components/views/add-memory/project-selection.tsx
index f23768a3..3c166303 100644
--- a/apps/web/components/views/add-memory/project-selection.tsx
+++ b/apps/web/components/views/add-memory/project-selection.tsx
@@ -50,18 +50,18 @@ export function ProjectSelection({
value={selectedProject}
>
<SelectTrigger
- className={`bg-white/5 border-white/10 text-white ${className}`}
+ className={`bg-foreground/5 border-foreground/10 ${className}`}
id={id}
>
<SelectValue placeholder="Select a project" />
</SelectTrigger>
<SelectContent
- className="bg-black/90 backdrop-blur-xl border-white/10 z-[90]"
+ className="bg-black/90 backdrop-blur-xl border-foreground/10 z-[90]"
position="popper"
sideOffset={5}
>
<SelectItem
- className="text-white hover:bg-white/10"
+ className="hover:bg-foreground/10"
key="default"
value="sm_project_default"
>
@@ -71,7 +71,7 @@ export function ProjectSelection({
.filter((p) => p.containerTag !== 'sm_project_default' && p.id)
.map((project) => (
<SelectItem
- className="text-white hover:bg-white/10"
+ className="hover:bg-foreground/10"
key={project.id || project.containerTag}
value={project.containerTag}
>
@@ -79,7 +79,7 @@ export function ProjectSelection({
</SelectItem>
))}
<SelectItem
- className="text-white hover:bg-white/10 border-t border-white/10 mt-1"
+ className="hover:bg-foreground/10 border-t border-foreground/10 mt-1"
key="create-new"
value="create-new-project"
>
diff --git a/apps/web/components/views/add-memory/text-editor.tsx b/apps/web/components/views/add-memory/text-editor.tsx
index 5a07b8f7..f6cf9425 100644
--- a/apps/web/components/views/add-memory/text-editor.tsx
+++ b/apps/web/components/views/add-memory/text-editor.tsx
@@ -285,7 +285,7 @@ export function TextEditor({
return (
<blockquote
{...props.attributes}
- className="border-l-4 border-white/20 pl-4 italic text-white/80"
+ className="border-l-4 border-foreground/20 pl-4 italic text-foreground/80"
>
{props.children}
</blockquote>
@@ -312,7 +312,7 @@ export function TextEditor({
if (leaf.code) {
children = (
- <code className="bg-white/10 px-1 rounded text-sm">{children}</code>
+ <code className="bg-foreground/10 px-1 rounded text-sm">{children}</code>
);
}
@@ -402,10 +402,10 @@ export function TextEditor({
variant="ghost"
size="sm"
className={cn(
- "h-8 w-8 !p-0 text-white/70 transition-all duration-200 rounded-sm",
- "hover:bg-white/15 hover:text-white hover:scale-105",
+ "h-8 w-8 !p-0 text-foreground/70 transition-all duration-200 rounded-sm",
+ "hover:bg-foreground/15 hover:text-foreground hover:scale-105",
"active:scale-95",
- isActive && "bg-white/20 text-white",
+ isActive && "bg-foreground/20 text-foreground",
)}
onMouseDown={onMouseDown}
title={title}
@@ -444,7 +444,7 @@ export function TextEditor({
onBlur={onBlur}
readOnly={disabled}
className={cn(
- "outline-none w-full h-full text-white placeholder:text-white/50",
+ "outline-none w-full h-full placeholder:text-foreground/50",
disabled && "opacity-50 cursor-not-allowed",
)}
style={{
@@ -457,7 +457,7 @@ export function TextEditor({
</div>
{/* Toolbar */}
- <div className="p-1 flex items-center gap-2 bg-white/5 backdrop-blur-sm rounded-b-md">
+ <div className="p-1 flex items-center gap-2 bg-foreground/5 backdrop-blur-sm rounded-b-md">
<div className="flex items-center gap-1">
{/* Text formatting */}
<ToolbarButton
@@ -489,7 +489,7 @@ export function TextEditor({
/>
</div>
- <div className="w-px h-6 bg-white/30 mx-2" />
+ <div className="w-px h-6 bg-foreground/30 mx-2" />
<div className="flex items-center gap-1">
{/* Block formatting */}
diff --git a/apps/web/components/views/billing.tsx b/apps/web/components/views/billing.tsx
index 8b79eb23..679b648e 100644
--- a/apps/web/components/views/billing.tsx
+++ b/apps/web/components/views/billing.tsx
@@ -1,70 +1,72 @@
-import { useAuth } from "@lib/auth-context";
+import { useAuth } from "@lib/auth-context"
import {
fetchConnectionsFeature,
fetchMemoriesFeature,
fetchSubscriptionStatus,
-} from "@lib/queries";
-import { Button } from "@ui/components/button";
-import { HeadingH3Bold } from "@ui/text/heading/heading-h3-bold";
-import { useCustomer } from "autumn-js/react";
-import { CheckCircle, LoaderIcon, X } from "lucide-react";
-import { motion } from "motion/react";
-import Link from "next/link";
-import { useEffect, useState } from "react";
-import { analytics } from "@/lib/analytics";
+} from "@lib/queries"
+import { Button } from "@ui/components/button"
+import { HeadingH3Bold } from "@ui/text/heading/heading-h3-bold"
+import { useCustomer } from "autumn-js/react"
+import { CheckCircle, LoaderIcon, X } from "lucide-react"
+import { motion } from "motion/react"
+import Link from "next/link"
+import { useEffect, useState } from "react"
+import { analytics } from "@/lib/analytics"
export function BillingView() {
- const autumn = useCustomer();
- const { user } = useAuth();
- const [isLoading, setIsLoading] = useState(false);
+ const autumn = useCustomer()
+ const { user } = useAuth()
+ const [isLoading, setIsLoading] = useState(false)
useEffect(() => {
- analytics.billingViewed();
- }, []);
+ analytics.billingViewed()
+ }, [])
- const { data: memoriesCheck } = fetchMemoriesFeature(autumn as any);
-
- const memoriesUsed = memoriesCheck?.usage ?? 0;
- const memoriesLimit = memoriesCheck?.included_usage ?? 0;
-
- const { data: connectionsCheck } = fetchConnectionsFeature(autumn as any);
-
- const connectionsUsed = connectionsCheck?.usage ?? 0;
-
- // Fetch subscription status with React Query
const {
data: status = {
consumer_pro: null,
},
isLoading: isCheckingStatus,
- } = fetchSubscriptionStatus(autumn as any);
+ } = fetchSubscriptionStatus(autumn, !autumn.isLoading)
+
+ const { data: memoriesCheck } = fetchMemoriesFeature(
+ autumn,
+ !autumn.isLoading && !isCheckingStatus,
+ )
+
+ const memoriesUsed = memoriesCheck?.usage ?? 0
+ const memoriesLimit = memoriesCheck?.included_usage ?? 0
+
+ const { data: connectionsCheck } = fetchConnectionsFeature(autumn, !autumn.isLoading && !isCheckingStatus)
+
+ const connectionsUsed = connectionsCheck?.usage ?? 0
// Handle upgrade
const handleUpgrade = async () => {
- analytics.upgradeInitiated();
- setIsLoading(true);
+ analytics.upgradeInitiated()
+ setIsLoading(true)
try {
await autumn.attach({
productId: "consumer_pro",
successUrl: "https://app.supermemory.ai/",
- });
- analytics.upgradeCompleted();
- window.location.reload();
+ })
+ analytics.upgradeCompleted()
+ window.location.reload()
} catch (error) {
- console.error(error);
- setIsLoading(false);
+ console.error(error)
+ setIsLoading(false)
}
- };
+ }
// Handle manage billing
const handleManageBilling = async () => {
- analytics.billingPortalOpened();
+ analytics.billingPortalOpened()
await autumn.openBillingPortal({
returnUrl: "https://app.supermemory.ai",
- });
- };
+ })
+ }
- const isPro = status.consumer_pro;
+ const isPro = status.consumer_pro
if (user?.isAnonymous) {
return (
@@ -74,18 +76,20 @@ export function BillingView() {
initial={{ opacity: 0, scale: 0.9 }}
transition={{ type: "spring", damping: 20 }}
>
- <p className="text-white/70 mb-4">Sign in to unlock premium features</p>
+ <p className="text-muted-foreground mb-4">
+ Sign in to unlock premium features
+ </p>
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<Button
asChild
- className="bg-white/10 hover:bg-white/20 text-white border-white/20"
+ className="bg-muted hover:bg-muted/80 text-foreground border-border"
size="sm"
>
<Link href="/login">Sign in</Link>
</Button>
</motion.div>
</motion.div>
- );
+ )
}
if (isPro) {
@@ -96,28 +100,28 @@ export function BillingView() {
initial={{ opacity: 0, y: 10 }}
>
<div className="space-y-3">
- <HeadingH3Bold className="text-white flex items-center gap-2">
+ <HeadingH3Bold className="text-foreground flex items-center gap-2">
Pro Plan
- <span className="text-xs bg-green-500/20 text-green-400 px-2 py-0.5 rounded-full">
+ <span className="text-xs bg-green-500/20 text-green-600 dark:text-green-400 px-2 py-0.5 rounded-full">
Active
</span>
</HeadingH3Bold>
- <p className="text-sm text-white/70">
+ <p className="text-sm text-muted-foreground">
You're enjoying expanded memory capacity with supermemory Pro!
</p>
</div>
{/* Current Usage */}
<div className="space-y-3">
- <h4 className="text-sm font-medium text-white/90">Current Usage</h4>
+ <h4 className="text-sm font-medium text-foreground">Current Usage</h4>
<div className="space-y-2">
<div className="flex justify-between items-center">
- <span className="text-sm text-white/70">Memories</span>
- <span className="text-sm text-white/90">
+ <span className="text-sm text-muted-foreground">Memories</span>
+ <span className="text-sm text-foreground">
{memoriesUsed} / {memoriesLimit}
</span>
</div>
- <div className="w-full bg-white/10 rounded-full h-2">
+ <div className="w-full bg-muted-foreground/50 rounded-full h-2">
<div
className="bg-green-500 h-2 rounded-full transition-all"
style={{
@@ -128,26 +132,19 @@ export function BillingView() {
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
- <span className="text-sm text-white/70">Connections</span>
- <span className="text-sm text-white/90">
+ <span className="text-sm text-muted-foreground">Connections</span>
+ <span className="text-sm text-foreground">
{connectionsUsed} / 10
</span>
</div>
</div>
</div>
- <motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
- <Button
- className="bg-white/10 hover:bg-white/20 text-white border-white/20"
- onClick={handleManageBilling}
- size="sm"
- variant="outline"
- >
- Manage Billing
- </Button>
- </motion.div>
+ <Button onClick={handleManageBilling} size="sm" variant="default">
+ Manage Billing
+ </Button>
</motion.div>
- );
+ )
}
return (
@@ -158,17 +155,19 @@ export function BillingView() {
>
{/* Current Usage - Free Plan */}
<div className="space-y-3">
- <HeadingH3Bold className="text-white">Current Plan: Free</HeadingH3Bold>
+ <HeadingH3Bold className="text-foreground">
+ Current Plan: Free
+ </HeadingH3Bold>
<div className="space-y-2">
<div className="flex justify-between items-center">
- <span className="text-sm text-white/70">Memories</span>
+ <span className="text-sm text-muted-foreground">Memories</span>
<span
- className={`text-sm ${memoriesUsed >= memoriesLimit ? "text-red-400" : "text-white/90"}`}
+ className={`text-sm ${memoriesUsed >= memoriesLimit ? "text-red-500" : "text-foreground"}`}
>
{memoriesUsed} / {memoriesLimit}
</span>
</div>
- <div className="w-full bg-white/10 rounded-full h-2">
+ <div className="w-full bg-muted-foreground/50 rounded-full h-2">
<div
className={`h-2 rounded-full transition-all ${
memoriesUsed >= memoriesLimit ? "bg-red-500" : "bg-blue-500"
@@ -183,23 +182,25 @@ export function BillingView() {
{/* Comparison */}
<div className="space-y-4">
- <HeadingH3Bold className="text-white">Upgrade to Pro</HeadingH3Bold>
+ <HeadingH3Bold className="text-foreground">
+ Upgrade to Pro
+ </HeadingH3Bold>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Free Plan */}
- <div className="p-4 bg-white/5 rounded-lg border border-white/10">
- <h4 className="font-medium text-white/90 mb-3">Free Plan</h4>
+ <div className="p-4 bg-muted/50 rounded-lg border border-border">
+ <h4 className="font-medium text-foreground mb-3">Free Plan</h4>
<ul className="space-y-2">
- <li className="flex items-center gap-2 text-sm text-white/70">
- <CheckCircle className="h-4 w-4 text-green-400" />
+ <li className="flex items-center gap-2 text-sm text-muted-foreground">
+ <CheckCircle className="h-4 w-4 text-green-500" />
200 memories
</li>
- <li className="flex items-center gap-2 text-sm text-white/70">
- <X className="h-4 w-4 text-red-400" />
+ <li className="flex items-center gap-2 text-sm text-muted-foreground">
+ <X className="h-4 w-4 text-red-500" />
No connections
</li>
- <li className="flex items-center gap-2 text-sm text-white/70">
- <CheckCircle className="h-4 w-4 text-green-400" />
+ <li className="flex items-center gap-2 text-sm text-muted-foreground">
+ <CheckCircle className="h-4 w-4 text-green-500" />
Basic search
</li>
</ul>
@@ -207,9 +208,9 @@ export function BillingView() {
{/* Pro Plan */}
<div className="p-4 bg-gradient-to-br from-blue-500/10 to-purple-500/10 rounded-lg border border-blue-500/20">
- <h4 className="font-medium text-white mb-3 flex items-center gap-2">
+ <h4 className="font-medium text-foreground mb-3 flex items-center gap-2">
Pro Plan
- <span className="text-xs bg-blue-500/20 text-blue-400 px-2 py-0.5 rounded-full">
+ <span className="text-xs bg-blue-500/20 text-blue-600 dark:text-blue-400 px-2 py-0.5 rounded-full">
Recommended
</span>
</h4>
@@ -218,44 +219,41 @@ export function BillingView() {
<CheckCircle className="h-4 w-4 text-green-400" />
Unlimited memories
</li>
- <li className="flex items-center gap-2 text-sm text-white/90">
- <CheckCircle className="h-4 w-4 text-green-400" />
+ <li className="flex items-center gap-2 text-sm text-foreground">
+ <CheckCircle className="h-4 w-4 text-green-500" />
10 connections
</li>
- <li className="flex items-center gap-2 text-sm text-white/90">
- <CheckCircle className="h-4 w-4 text-green-400" />
+ <li className="flex items-center gap-2 text-sm text-foreground">
+ <CheckCircle className="h-4 w-4 text-green-500" />
Advanced search
</li>
- <li className="flex items-center gap-2 text-sm text-white/90">
- <CheckCircle className="h-4 w-4 text-green-400" />
+ <li className="flex items-center gap-2 text-sm text-foreground">
+ <CheckCircle className="h-4 w-4 text-green-500" />
Priority support
</li>
</ul>
</div>
</div>
- <motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
- <Button
- className="bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white border-0 w-full"
- disabled={isLoading || isCheckingStatus}
- onClick={handleUpgrade}
- size="sm"
- >
- {isLoading || isCheckingStatus ? (
- <>
- <LoaderIcon className="h-4 w-4 animate-spin mr-2" />
- Upgrading...
- </>
- ) : (
- <div>Upgrade to Pro - $15/month (only for first 100 users)</div>
- )}
- </Button>
- </motion.div>
+ <Button
+ className="bg-blue-600 hover:bg-blue-700 text-white border-0 w-full"
+ disabled={isLoading || isCheckingStatus}
+ onClick={handleUpgrade}
+ >
+ {isLoading || isCheckingStatus ? (
+ <>
+ <LoaderIcon className="h-4 w-4 animate-spin mr-2" />
+ Upgrading...
+ </>
+ ) : (
+ <div>Upgrade to Pro - $15/month (only for first 100 users)</div>
+ )}
+ </Button>
- <p className="text-xs text-white/50 text-center">
+ <p className="text-xs text-muted-foreground text-center">
Cancel anytime. No questions asked.
</p>
</div>
</motion.div>
- );
+ )
}
diff --git a/apps/web/components/views/chat/chat-messages.tsx b/apps/web/components/views/chat/chat-messages.tsx
index 85f91565..db0ae209 100644
--- a/apps/web/components/views/chat/chat-messages.tsx
+++ b/apps/web/components/views/chat/chat-messages.tsx
@@ -1,49 +1,49 @@
-"use client";
+"use client"
-import { useChat, useCompletion } from "@ai-sdk/react";
-import { cn } from "@lib/utils";
-import { Button } from "@ui/components/button";
-import { Input } from "@ui/components/input";
-import { DefaultChatTransport } from "ai";
+import { useChat, useCompletion } from "@ai-sdk/react"
+import { cn } from "@lib/utils"
+import { Button } from "@ui/components/button"
+import { DefaultChatTransport } from "ai"
import {
ArrowUp,
Check,
ChevronDown,
ChevronRight,
Copy,
+ Plus,
RotateCcw,
X,
-} from "lucide-react";
-import { useEffect, useRef, useState } from "react";
-import { toast } from "sonner";
-import { Streamdown } from "streamdown";
-import { TextShimmer } from "@/components/text-shimmer";
-import { usePersistentChat, useProject } from "@/stores";
-import { useGraphHighlights } from "@/stores/highlights";
-import { Spinner } from "../../spinner";
+} from "lucide-react"
+import { useCallback, useEffect, useRef, useState } from "react"
+import { toast } from "sonner"
+import { Streamdown } from "streamdown"
+import { TextShimmer } from "@/components/text-shimmer"
+import { usePersistentChat, useProject } from "@/stores"
+import { useGraphHighlights } from "@/stores/highlights"
+import { Spinner } from "../../spinner"
interface MemoryResult {
- documentId?: string;
- title?: string;
- content?: string;
- url?: string;
- score?: number;
+ documentId?: string
+ title?: string
+ content?: string
+ url?: string
+ score?: number
}
interface ExpandableMemoriesProps {
- foundCount: number;
- results: MemoryResult[];
+ foundCount: number
+ results: MemoryResult[]
}
function ExpandableMemories({ foundCount, results }: ExpandableMemoriesProps) {
- const [isExpanded, setIsExpanded] = useState(false);
+ const [isExpanded, setIsExpanded] = useState(false)
if (foundCount === 0) {
return (
<div className="text-sm flex items-center gap-2 text-muted-foreground">
<Check className="size-4" /> No memories found
</div>
- );
+ )
}
return (
@@ -58,17 +58,16 @@ function ExpandableMemories({ foundCount, results }: ExpandableMemoriesProps) {
) : (
<ChevronRight className="size-4" />
)}
- <Check className="size-4" />
- Found {foundCount} {foundCount === 1 ? "memory" : "memories"}
+ Related memories
</button>
{isExpanded && results.length > 0 && (
- <div className="mt-2 ml-6 space-y-2 max-h-48 overflow-y-auto">
+ <div className="mt-2 ml-6 space-y-2 max-h-48 overflow-y-auto grid grid-cols-3 gap-2">
{results.map((result, index) => {
const isClickable =
result.url &&
(result.url.startsWith("http://") ||
- result.url.startsWith("https://"));
+ result.url.startsWith("https://"))
const content = (
<>
@@ -83,7 +82,7 @@ function ExpandableMemories({ foundCount, results }: ExpandableMemoriesProps) {
</div>
)}
{result.url && (
- <div className="text-xs text-blue-400 mt-1 truncate">
+ <div className="text-xs text-blue-600 dark:text-blue-400 mt-1 truncate">
{result.url}
</div>
)}
@@ -93,12 +92,12 @@ function ExpandableMemories({ foundCount, results }: ExpandableMemoriesProps) {
</div>
)}
</>
- );
+ )
if (isClickable) {
return (
<a
- className="block p-2 bg-white/5 rounded-md border border-white/10 hover:bg-white/10 hover:border-white/20 transition-colors cursor-pointer"
+ className="block p-2 bg-accent/50 rounded-md border border-border hover:bg-accent transition-colors cursor-pointer"
href={result.url}
key={result.documentId || index}
rel="noopener noreferrer"
@@ -106,93 +105,93 @@ function ExpandableMemories({ foundCount, results }: ExpandableMemoriesProps) {
>
{content}
</a>
- );
+ )
}
return (
<div
- className="p-2 bg-white/5 rounded-md border border-white/10"
+ className="p-2 bg-accent/50 rounded-md border border-border"
key={result.documentId || index}
>
{content}
</div>
- );
+ )
})}
</div>
)}
</div>
- );
+ )
}
function useStickyAutoScroll(triggerKeys: ReadonlyArray<unknown>) {
- const scrollContainerRef = useRef<HTMLDivElement>(null);
- const bottomRef = useRef<HTMLDivElement>(null);
- const [isAutoScroll, setIsAutoScroll] = useState(true);
- const [isFarFromBottom, setIsFarFromBottom] = useState(false);
+ const scrollContainerRef = useRef<HTMLDivElement>(null)
+ const bottomRef = useRef<HTMLDivElement>(null)
+ const [isAutoScroll, setIsAutoScroll] = useState(true)
+ const [isFarFromBottom, setIsFarFromBottom] = useState(false)
- const scrollToBottom = (behavior: ScrollBehavior = "auto") => {
- const node = bottomRef.current;
- if (node) node.scrollIntoView({ behavior, block: "end" });
- };
+ const scrollToBottom = useCallback((behavior: ScrollBehavior = "auto") => {
+ const node = bottomRef.current
+ if (node) node.scrollIntoView({ behavior, block: "end" })
+ }, [])
useEffect(function observeBottomVisibility() {
- const container = scrollContainerRef.current;
- const sentinel = bottomRef.current;
- if (!container || !sentinel) return;
+ const container = scrollContainerRef.current
+ const sentinel = bottomRef.current
+ if (!container || !sentinel) return
const observer = new IntersectionObserver(
(entries) => {
- if (!entries || entries.length === 0) return;
- const isIntersecting = entries.some((e) => e.isIntersecting);
- setIsAutoScroll(isIntersecting);
+ if (!entries || entries.length === 0) return
+ const isIntersecting = entries.some((e) => e.isIntersecting)
+ setIsAutoScroll(isIntersecting)
},
{ root: container, rootMargin: "0px 0px 80px 0px", threshold: 0 },
- );
- observer.observe(sentinel);
- return () => observer.disconnect();
- }, []);
+ )
+ observer.observe(sentinel)
+ return () => observer.disconnect()
+ }, [])
useEffect(
function observeContentResize() {
- const container = scrollContainerRef.current;
- if (!container) return;
+ const container = scrollContainerRef.current
+ if (!container) return
const resizeObserver = new ResizeObserver(() => {
- if (isAutoScroll) scrollToBottom("auto");
+ if (isAutoScroll) scrollToBottom("auto")
const distanceFromBottom =
- container.scrollHeight - container.scrollTop - container.clientHeight;
- setIsFarFromBottom(distanceFromBottom > 100);
- });
- resizeObserver.observe(container);
- return () => resizeObserver.disconnect();
+ container.scrollHeight - container.scrollTop - container.clientHeight
+ setIsFarFromBottom(distanceFromBottom > 100)
+ })
+ resizeObserver.observe(container)
+ return () => resizeObserver.disconnect()
},
- [isAutoScroll],
- );
+ [isAutoScroll, scrollToBottom],
+ )
function enableAutoScroll() {
- setIsAutoScroll(true);
+ setIsAutoScroll(true)
}
useEffect(
function autoScrollOnNewContent() {
- if (isAutoScroll) scrollToBottom("auto");
+ if (isAutoScroll) scrollToBottom("auto")
},
[isAutoScroll, scrollToBottom, ...triggerKeys],
- );
+ )
- const recomputeDistanceFromBottom = () => {
- const container = scrollContainerRef.current;
- if (!container) return;
+ const recomputeDistanceFromBottom = useCallback(() => {
+ const container = scrollContainerRef.current
+ if (!container) return
const distanceFromBottom =
- container.scrollHeight - container.scrollTop - container.clientHeight;
- setIsFarFromBottom(distanceFromBottom > 100);
- };
+ container.scrollHeight - container.scrollTop - container.clientHeight
+ setIsFarFromBottom(distanceFromBottom > 100)
+ }, [])
useEffect(() => {
- recomputeDistanceFromBottom();
- }, [recomputeDistanceFromBottom, ...triggerKeys]);
+ recomputeDistanceFromBottom()
+ }, [recomputeDistanceFromBottom, ...triggerKeys])
function onScroll() {
- recomputeDistanceFromBottom();
+ recomputeDistanceFromBottom()
}
return {
@@ -203,11 +202,11 @@ function useStickyAutoScroll(triggerKeys: ReadonlyArray<unknown>) {
onScroll,
enableAutoScroll,
scrollToBottom,
- } as const;
+ } as const
}
export function ChatMessages() {
- const { selectedProject } = useProject();
+ const { selectedProject } = useProject()
const {
currentChatId,
setCurrentChatId,
@@ -215,12 +214,14 @@ export function ChatMessages() {
getCurrentConversation,
setConversationTitle,
getCurrentChat,
- } = usePersistentChat();
+ } = usePersistentChat()
- const activeChatIdRef = useRef<string | null>(null);
- const shouldGenerateTitleRef = useRef<boolean>(false);
+ const [input, setInput] = useState("")
+ const activeChatIdRef = useRef<string | null>(null)
+ const shouldGenerateTitleRef = useRef<boolean>(false)
+ const hasRunInitialMessageRef = useRef<boolean>(false)
- const { setDocumentIds } = useGraphHighlights();
+ const { setDocumentIds } = useGraphHighlights()
const { messages, sendMessage, status, stop, setMessages, id, regenerate } =
useChat({
@@ -232,91 +233,118 @@ export function ChatMessages() {
}),
maxSteps: 2,
onFinish: (result) => {
- const activeId = activeChatIdRef.current;
- if (!activeId) return;
- if (result.message.role !== "assistant") return;
+ const activeId = activeChatIdRef.current
+ if (!activeId) return
+ if (result.message.role !== "assistant") return
if (shouldGenerateTitleRef.current) {
const textPart = result.message.parts.find(
(p: any) => p?.type === "text",
- ) as any;
- const text = textPart?.text?.trim();
+ ) as any
+ const text = textPart?.text?.trim()
if (text) {
- shouldGenerateTitleRef.current = false;
- complete(text);
+ shouldGenerateTitleRef.current = false
+ complete(text)
}
}
},
- });
+ })
useEffect(() => {
- activeChatIdRef.current = currentChatId ?? id ?? null;
- }, [currentChatId, id]);
+ activeChatIdRef.current = currentChatId ?? id ?? null
+ }, [currentChatId, id])
+
+ useEffect(() => {
+ if (currentChatId && !hasRunInitialMessageRef.current) {
+ // Check if there's an initial message from the home page in sessionStorage
+ const storageKey = `chat-initial-${currentChatId}`
+ const initialMessage = sessionStorage.getItem(storageKey)
+
+ if (initialMessage) {
+ // Clean up the storage and send the message
+ sessionStorage.removeItem(storageKey)
+ sendMessage({ text: initialMessage })
+ hasRunInitialMessageRef.current = true
+ }
+ }
+ }, [currentChatId])
useEffect(() => {
if (id && id !== currentChatId) {
- setCurrentChatId(id);
+ setCurrentChatId(id)
}
- }, [id, currentChatId, setCurrentChatId]);
+ }, [id])
useEffect(() => {
- const msgs = getCurrentConversation();
- setMessages(msgs ?? []);
- setInput("");
- }, [currentChatId]);
+ const msgs = getCurrentConversation()
+ if (msgs && msgs.length > 0) {
+ setMessages(msgs)
+ } else if (!currentChatId) {
+ setMessages([])
+ }
+ setInput("")
+ }, [currentChatId])
useEffect(() => {
- const activeId = currentChatId ?? id;
+ const activeId = currentChatId ?? id
if (activeId && messages.length > 0) {
- setConversation(activeId, messages);
+ setConversation(activeId, messages)
}
- }, [messages, currentChatId, id, setConversation]);
+ }, [messages, currentChatId, id])
- const [input, setInput] = useState("");
const { complete } = useCompletion({
api: `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat/title`,
credentials: "include",
onFinish: (_, completion) => {
- const activeId = activeChatIdRef.current;
- if (!completion || !activeId) return;
- setConversationTitle(activeId, completion.trim());
+ const activeId = activeChatIdRef.current
+ if (!completion || !activeId) return
+ setConversationTitle(activeId, completion.trim())
},
- });
+ })
// Update graph highlights from the most recent tool-searchMemories output
useEffect(() => {
try {
const lastAssistant = [...messages]
.reverse()
- .find((m) => m.role === "assistant");
- if (!lastAssistant) return;
+ .find((m) => m.role === "assistant")
+ if (!lastAssistant) return
const lastSearchPart = [...(lastAssistant.parts as any[])]
.reverse()
.find(
(p) =>
p?.type === "tool-searchMemories" &&
p?.state === "output-available",
- );
- if (!lastSearchPart) return;
- const output = (lastSearchPart as any).output;
+ )
+ if (!lastSearchPart) return
+ const output = (lastSearchPart as any).output
const ids = Array.isArray(output?.results)
? ((output.results as any[])
.map((r) => r?.documentId)
.filter(Boolean) as string[])
- : [];
+ : []
if (ids.length > 0) {
- setDocumentIds(ids);
+ setDocumentIds(ids)
}
} catch {}
- }, [messages, setDocumentIds]);
+ }, [messages])
useEffect(() => {
- const currentSummary = getCurrentChat();
+ const currentSummary = getCurrentChat()
const hasTitle = Boolean(
currentSummary?.title && currentSummary.title.trim().length > 0,
- );
- shouldGenerateTitleRef.current = !hasTitle;
- }, [getCurrentChat]);
+ )
+ shouldGenerateTitleRef.current = !hasTitle
+ }, [])
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault()
+ sendMessage({ text: input })
+ setInput("")
+ }
+ }
+
const {
scrollContainerRef,
bottomRef,
@@ -324,39 +352,48 @@ export function ChatMessages() {
onScroll,
enableAutoScroll,
scrollToBottom,
- } = useStickyAutoScroll([messages, status]);
+ } = useStickyAutoScroll([messages, status])
return (
- <>
- <div className="relative grow">
+ <div className="h-full flex flex-col w-full">
+ <div className="flex-1 relative">
<div
- className="flex flex-col gap-2 absolute inset-0 overflow-y-auto px-4 pt-4 pb-7 scroll-pb-7"
+ className="flex flex-col gap-2 absolute inset-0 overflow-y-auto px-4 pt-4 pb-7 scroll-pb-7 custom-scrollbar"
onScroll={onScroll}
ref={scrollContainerRef}
>
{messages.map((message) => (
<div
className={cn(
- "flex flex-col",
- message.role === "user" ? "items-end" : "items-start",
+ "flex my-2",
+ message.role === "user"
+ ? "items-center flex-row-reverse gap-2"
+ : "flex-col",
)}
key={message.id}
>
- <div className="flex flex-col gap-2 max-w-4/5 bg-white/10 py-3 px-4 rounded-lg">
+ <div
+ className={cn(
+ "flex flex-col gap-2 max-w-4/5",
+ message.role === "user"
+ ? "bg-accent/50 px-3 py-1.5 border border-border rounded-lg"
+ : "",
+ )}
+ >
{message.parts
.filter((part) =>
["text", "tool-searchMemories", "tool-addMemory"].includes(
part.type,
),
)
- .map((part) => {
+ .map((part, index) => {
switch (part.type) {
case "text":
return (
- <div key={message.id + part.type}>
+ <div key={`${message.id}-${part.type}-${index}`}>
<Streamdown>{part.text}</Streamdown>
</div>
- );
+ )
case "tool-searchMemories": {
switch (part.state) {
case "input-available":
@@ -364,46 +401,46 @@ export function ChatMessages() {
return (
<div
className="text-sm flex items-center gap-2 text-muted-foreground"
- key={message.id + part.type}
+ key={`${message.id}-${part.type}-${index}`}
>
<Spinner className="size-4" /> Searching
memories...
</div>
- );
+ )
case "output-error":
return (
<div
className="text-sm flex items-center gap-2 text-muted-foreground"
- key={message.id + part.type}
+ key={`${message.id}-${part.type}-${index}`}
>
<X className="size-4" /> Error recalling
memories
</div>
- );
+ )
case "output-available": {
- const output = part.output;
+ const output = part.output
const foundCount =
typeof output === "object" &&
output !== null &&
"count" in output
? Number(output.count) || 0
- : 0;
+ : 0
// @ts-expect-error
const results = Array.isArray(output?.results)
? // @ts-expect-error
output.results
- : [];
+ : []
return (
<ExpandableMemories
foundCount={foundCount}
- key={message.id + part.type}
+ key={`${message.id}-${part.type}-${index}`}
results={results}
/>
- );
+ )
}
default:
- return null;
+ return null
}
}
case "tool-addMemory": {
@@ -412,44 +449,44 @@ export function ChatMessages() {
return (
<div
className="text-sm flex items-center gap-2 text-muted-foreground"
- key={message.id + part.type}
+ key={`${message.id}-${part.type}-${index}`}
>
<Spinner className="size-4" /> Adding memory...
</div>
- );
+ )
case "output-error":
return (
<div
className="text-sm flex items-center gap-2 text-muted-foreground"
- key={message.id + part.type}
+ key={`${message.id}-${part.type}-${index}`}
>
<X className="size-4" /> Error adding memory
</div>
- );
+ )
case "output-available":
return (
<div
className="text-sm flex items-center gap-2 text-muted-foreground"
- key={message.id + part.type}
+ key={`${message.id}-${part.type}-${index}`}
>
<Check className="size-4" /> Memory added
</div>
- );
+ )
case "input-streaming":
return (
<div
className="text-sm flex items-center gap-2 text-muted-foreground"
- key={message.id + part.type}
+ key={`${message.id}-${part.type}-${index}`}
>
<Spinner className="size-4" /> Adding memory...
</div>
- );
+ )
default:
- return null;
+ return null
}
}
default:
- return null;
+ return null
}
})}
</div>
@@ -463,8 +500,8 @@ export function ChatMessages() {
.filter((p) => p.type === "text")
?.map((p) => (p as any).text)
.join("\n") ?? "",
- );
- toast.success("Copied to clipboard");
+ )
+ toast.success("Copied to clipboard")
}}
size="icon"
variant="ghost"
@@ -503,8 +540,8 @@ export function ChatMessages() {
: "opacity-0 scale-95 pointer-events-none",
)}
onClick={() => {
- enableAutoScroll();
- scrollToBottom("smooth");
+ enableAutoScroll()
+ scrollToBottom("smooth")
}}
size="sm"
type="button"
@@ -514,41 +551,44 @@ export function ChatMessages() {
</Button>
</div>
- <form
- className="flex gap-2 px-4 pb-4 pt-1 relative"
- onSubmit={(e) => {
- e.preventDefault();
- if (status === "submitted") return;
- if (status === "streaming") {
- stop();
- return;
- }
- if (input.trim()) {
- enableAutoScroll();
- scrollToBottom("auto");
- sendMessage({ text: input });
- setInput("");
- }
- }}
- >
- <div className="absolute top-0 left-0 -mt-7 w-full h-7 bg-gradient-to-t from-background to-transparent" />
- <Input
- className="w-full"
- disabled={status === "submitted"}
- onChange={(e) => setInput(e.target.value)}
- placeholder="Say something..."
- value={input}
- />
- <Button disabled={status === "submitted"} type="submit">
- {status === "ready" ? (
- <ArrowUp className="size-4" />
- ) : status === "submitted" ? (
- <Spinner className="size-4" />
- ) : (
- <X className="size-4" />
- )}
- </Button>
- </form>
- </>
- );
+ <div className="px-4 pb-4 pt-1 relative flex-shrink-0">
+ <form
+ className="flex flex-col items-end gap-3 bg-card border border-border rounded-[22px] p-3 relative shadow-lg dark:shadow-2xl"
+ onSubmit={(e) => {
+ e.preventDefault()
+ if (status === "submitted") return
+ if (status === "streaming") {
+ stop()
+ return
+ }
+ if (input.trim()) {
+ enableAutoScroll()
+ scrollToBottom("auto")
+ sendMessage({ text: input })
+ setInput("")
+ }
+ }}
+ >
+ <textarea
+ value={input}
+ onChange={(e) => setInput(e.target.value)}
+ onKeyDown={handleKeyDown}
+ placeholder="Ask your follow-up question..."
+ className="w-full text-foreground placeholder:text-muted-foreground rounded-md outline-none resize-none text-base leading-relaxed px-3 py-3 bg-transparent"
+ rows={3}
+ />
+ <div className="absolute bottom-2 right-2">
+ <Button
+ type="submit"
+ disabled={!input.trim()}
+ className="text-primary-foreground rounded-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed bg-primary hover:bg-primary/90"
+ size="icon"
+ >
+ <ArrowUp className="size-4" />
+ </Button>
+ </div>
+ </form>
+ </div>
+ </div>
+ )
}
diff --git a/apps/web/components/views/connections-tab-content.tsx b/apps/web/components/views/connections-tab-content.tsx
index 14a63e8f..3aa06776 100644
--- a/apps/web/components/views/connections-tab-content.tsx
+++ b/apps/web/components/views/connections-tab-content.tsx
@@ -183,16 +183,16 @@ export function ConnectionsTabContent() {
return (
<div className="space-y-4">
<div className="mb-4">
- <p className="text-sm text-white/70">
+ <p className="text-sm text-foreground/70">
Connect your favorite services to import documents
</p>
{isProUser && !autumn.isLoading && (
- <p className="text-xs text-white/50 mt-1">
+ <p className="text-xs text-foreground/50 mt-1">
{connectionsUsed} of {connectionsLimit} connections used
</p>
)}
{!isProUser && !autumn.isLoading && (
- <p className="text-xs text-white/50 mt-1">
+ <p className="text-xs text-foreground/50 mt-1">
Connections require a Pro subscription
</p>
)}
@@ -208,7 +208,7 @@ export function ConnectionsTabContent() {
<p className="text-sm text-yellow-400 mb-2">
🔌 Connections are a Pro feature
</p>
- <p className="text-xs text-white/60 mb-3">
+ <p className="text-xs text-foreground/60 mb-3">
Connect Google Drive, Notion, OneDrive and more to automatically
sync your documents.
</p>
@@ -228,12 +228,12 @@ export function ConnectionsTabContent() {
{[...Array(2)].map((_, i) => (
<motion.div
animate={{ opacity: 1 }}
- className="p-4 bg-white/5 rounded-lg"
+ className="p-4 bg-foreground/5 rounded-lg"
initial={{ opacity: 0 }}
key={`skeleton-${Date.now()}-${i}`}
transition={{ delay: i * 0.1 }}
>
- <Skeleton className="h-12 w-full bg-white/10" />
+ <Skeleton className="h-12 w-full bg-foreground/10" />
</motion.div>
))}
</div>
@@ -244,8 +244,8 @@ export function ConnectionsTabContent() {
initial={{ opacity: 0, scale: 0.9 }}
transition={{ type: "spring", damping: 20 }}
>
- <p className="text-white/50 mb-2">No connections yet</p>
- <p className="text-xs text-white/40">
+ <p className="text-foreground/50 mb-2">No connections yet</p>
+ <p className="text-xs text-foreground/40">
Choose a service below to connect
</p>
</motion.div>
@@ -255,7 +255,7 @@ export function ConnectionsTabContent() {
{connections.map((connection, index) => (
<motion.div
animate={{ opacity: 1, x: 0 }}
- className="flex items-center justify-between p-3 bg-white/5 rounded-lg hover:bg-white/10 transition-colors"
+ className="flex items-center justify-between p-3 bg-foreground/5 rounded-lg hover:bg-foreground/10 transition-colors"
exit={{ opacity: 0, x: 20 }}
initial={{ opacity: 0, x: -20 }}
key={connection.id}
@@ -263,19 +263,13 @@ export function ConnectionsTabContent() {
transition={{ delay: index * 0.05 }}
>
<div className="flex items-center gap-3">
- <motion.div
- animate={{ rotate: 0, opacity: 1 }}
- initial={{ rotate: -180, opacity: 0 }}
- transition={{ delay: index * 0.05 + 0.2 }}
- >
- {getProviderIcon(connection.provider)}
- </motion.div>
+ {getProviderIcon(connection.provider)}
<div>
- <p className="font-medium text-white capitalize">
+ <p className="font-medium text-foreground capitalize">
{connection.provider.replace("-", " ")}
</p>
{connection.email && (
- <p className="text-sm text-white/60">
+ <p className="text-sm text-foreground/60">
{connection.email}
</p>
)}
@@ -286,7 +280,7 @@ export function ConnectionsTabContent() {
whileTap={{ scale: 0.9 }}
>
<Button
- className="text-white/50 hover:text-red-400"
+ className="text-foreground/50 hover:text-red-400"
disabled={deleteConnectionMutation.isPending}
onClick={() =>
deleteConnectionMutation.mutate(connection.id)
@@ -305,9 +299,7 @@ export function ConnectionsTabContent() {
{/* Available Connections Section */}
<div className="mt-6">
- <h3 className="text-lg font-medium text-white mb-4">
- Available Connections
- </h3>
+ <h3 className="text-lg font-medium mb-4">Available Connections</h3>
<div className="grid gap-3">
{Object.entries(CONNECTORS).map(([provider, config], index) => {
const Icon = config.icon
@@ -317,11 +309,9 @@ export function ConnectionsTabContent() {
initial={{ opacity: 0, y: 20 }}
key={provider}
transition={{ delay: index * 0.05 }}
- whileHover={{ scale: 1.02 }}
- whileTap={{ scale: 0.98 }}
>
<Button
- className="justify-start h-auto p-4 bg-white/5 hover:bg-white/10 border-white/10 text-white w-full"
+ className="justify-start h-auto p-4 bg-foreground/5 hover:bg-foreground/10 border-foreground/10 w-full"
disabled={addConnectionMutation.isPending}
onClick={() => {
addConnectionMutation.mutate(provider as ConnectorProvider)
@@ -331,7 +321,7 @@ export function ConnectionsTabContent() {
<Icon className="h-8 w-8 mr-3" />
<div className="text-left">
<div className="font-medium">{config.title}</div>
- <div className="text-sm text-white/60 mt-0.5">
+ <div className="text-sm text-foreground/60 mt-0.5">
{config.description}
</div>
</div>
diff --git a/apps/web/components/views/integrations.tsx b/apps/web/components/views/integrations.tsx
index b3e3c92d..dadd7dc6 100644
--- a/apps/web/components/views/integrations.tsx
+++ b/apps/web/components/views/integrations.tsx
@@ -118,7 +118,10 @@ export function IntegrationsView() {
}
}, [autumn.isLoading, autumn.customer])
- const { data: connectionsCheck } = fetchConnectionsFeature(autumn)
+ const { data: connectionsCheck } = fetchConnectionsFeature(
+ autumn,
+ !autumn.isLoading,
+ )
const connectionsUsed = connectionsCheck?.balance ?? 0
const connectionsLimit = connectionsCheck?.included_usage ?? 0
@@ -281,25 +284,25 @@ export function IntegrationsView() {
return (
<div className="space-y-4 sm:space-y-4 custom-scrollbar">
{/* iOS Shortcuts */}
- <div className="bg-white/5 rounded-xl border border-white/10 overflow-hidden">
+ <div className="bg-card rounded-xl border border-border overflow-hidden shadow-sm">
<div className="p-4 sm:p-5">
<div className="flex items-start gap-3 mb-3">
- <div className="p-2 bg-blue-500/20 rounded-lg flex-shrink-0">
- <Smartphone className="h-5 w-5 text-blue-400" />
+ <div className="p-2 bg-primary/10 rounded-lg flex-shrink-0">
+ <Smartphone className="h-5 w-5 text-primary" />
</div>
<div className="flex-1 min-w-0">
- <h3 className="text-white font-semibold text-base mb-1">
+ <h3 className="text-card-foreground font-semibold text-base mb-1">
Apple shortcuts
</h3>
- <p className="text-white/70 text-sm leading-relaxed">
+ <p className="text-muted-foreground text-sm leading-relaxed">
Add memories directly from iPhone, iPad or Mac.
</p>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-2 sm:gap-3">
<Button
- variant="ghost"
- className="flex-1 text-white hover:bg-blue-500/10 bg-[#171F59]/75 "
+ variant="secondary"
+ className="flex-1"
onClick={() => handleShortcutClick("add")}
disabled={createApiKeyMutation.isPending}
>
@@ -314,8 +317,8 @@ export function IntegrationsView() {
: "Add Memory Shortcut"}
</Button>
<Button
- variant="ghost"
- className="flex-1 text-white hover:bg-blue-500/10 bg-[#171F59]/75"
+ variant="secondary"
+ className="flex-1"
onClick={() => handleShortcutClick("search")}
disabled={createApiKeyMutation.isPending}
>
@@ -334,19 +337,19 @@ export function IntegrationsView() {
</div>
{/* Chrome Extension */}
- <div className="bg-white/5 rounded-xl border border-white/10 overflow-hidden">
+ <div className="bg-card rounded-xl border border-border overflow-hidden shadow-sm">
<div className="p-4 sm:p-5">
<div className="flex items-start gap-3">
- <div className="p-2 bg-orange-500/20 rounded-lg flex-shrink-0">
- <ChromeIcon className="h-5 w-5 text-orange-400" />
+ <div className="p-2 bg-primary/10 rounded-lg flex-shrink-0">
+ <ChromeIcon className="h-5 w-5 text-primary" />
</div>
<div className="flex-1 min-w-0 mb-3">
<div className="flex flex-col sm:flex-row sm:items-baseline sm:justify-between gap-2 mb-1">
- <h3 className="text-white font-semibold text-base">
+ <h3 className="text-card-foreground font-semibold text-base">
Chrome Extension
</h3>
<Button
- className="text-white bg-secondary w-fit"
+ className="bg-secondary text-secondary-foreground hover:bg-secondary/80 w-fit"
onClick={() => {
window.open(
"https://chromewebstore.google.com/detail/supermemory/afpgkkipfdpeaflnpoaffkcankadgjfc",
@@ -366,20 +369,20 @@ export function IntegrationsView() {
</div>
<div className="space-y-2">
<div className="flex items-center gap-3">
- <div className="w-1.5 h-1.5 bg-orange-400 rounded-full flex-shrink-0" />
- <p className="text-white/80 text-sm">
+ <div className="w-1.5 h-1.5 bg-primary rounded-full flex-shrink-0" />
+ <p className="text-muted-foreground text-sm">
Save any webpage to supermemory
</p>
</div>
<div className="flex items-center gap-3">
- <div className="w-1.5 h-1.5 bg-orange-400 rounded-full flex-shrink-0" />
- <p className="text-white/80 text-sm">
+ <div className="w-1.5 h-1.5 bg-primary rounded-full flex-shrink-0" />
+ <p className="text-muted-foreground text-sm">
Import All your Twitter Bookmarks
</p>
</div>
<div className="flex items-center gap-3">
- <div className="w-1.5 h-1.5 bg-orange-400 rounded-full flex-shrink-0" />
- <p className="text-white/80 text-sm">
+ <div className="w-1.5 h-1.5 bg-primary rounded-full flex-shrink-0" />
+ <p className="text-muted-foreground text-sm">
Bring all your chatGPT memories to Supermemory
</p>
</div>
@@ -388,12 +391,12 @@ export function IntegrationsView() {
</div>
{/* Connections Section */}
- <div className="bg-white/5 rounded-xl border border-white/10 overflow-hidden">
+ <div className="bg-card rounded-xl border border-border overflow-hidden shadow-sm">
<div className="p-4 sm:p-5">
<div className="flex items-start gap-3 mb-3">
- <div className="p-2 bg-green-500/20 rounded-lg flex-shrink-0">
+ <div className="p-2 bg-primary/10 rounded-lg flex-shrink-0">
<svg
- className="h-5 w-5 text-green-400"
+ className="h-5 w-5 text-primary"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@@ -408,14 +411,14 @@ export function IntegrationsView() {
</svg>
</div>
<div className="flex-1 min-w-0">
- <h3 className="text-white font-semibold text-base mb-1">
+ <h3 className="text-card-foreground font-semibold text-base mb-1">
Connections
</h3>
- <p className="text-white/70 text-sm leading-relaxed mb-2">
+ <p className="text-muted-foreground text-sm leading-relaxed mb-2">
Connect your accounts to sync document.
</p>
{!isProUser && (
- <p className="text-xs text-white/50">
+ <p className="text-xs text-muted-foreground/70">
Connections require a Pro subscription
</p>
)}
@@ -426,21 +429,21 @@ export function IntegrationsView() {
{!autumn.isLoading && !isProUser && (
<motion.div
animate={{ opacity: 1, y: 0 }}
- className="p-4 bg-yellow-500/10 border border-yellow-500/20 rounded-lg mb-3"
+ className="p-4 bg-accent border border-border rounded-lg mb-3"
initial={{ opacity: 0, y: -10 }}
>
- <p className="text-sm text-yellow-400 mb-2">
+ <p className="text-sm text-accent-foreground mb-2 font-medium">
🔌 Connections are a Pro feature
</p>
- <p className="text-xs text-white/60 mb-3">
+ <p className="text-xs text-muted-foreground mb-3">
Connect Google Drive, Notion, OneDrive and more to automatically
sync your documents.
</p>
<Button
- className="bg-yellow-500/20 hover:bg-yellow-500/30 text-yellow-400 border-yellow-500/30 w-full sm:w-auto"
+ className="w-full sm:w-auto"
onClick={handleUpgrade}
size="sm"
- variant="secondary"
+ variant="default"
>
Upgrade to Pro
</Button>
@@ -453,12 +456,12 @@ export function IntegrationsView() {
{Object.keys(CONNECTORS).map((_, i) => (
<motion.div
animate={{ opacity: 1 }}
- className="p-3 bg-white/5 rounded-lg"
+ className="p-3 bg-accent rounded-lg"
initial={{ opacity: 0 }}
key={`skeleton-${Date.now()}-${i}`}
transition={{ delay: i * 0.1 }}
>
- <Skeleton className="h-12 w-full bg-white/10" />
+ <Skeleton className="h-12 w-full bg-muted" />
</motion.div>
))}
</div>
@@ -474,46 +477,39 @@ export function IntegrationsView() {
return (
<motion.div
animate={{ opacity: 1, y: 0 }}
- className="flex flex-col sm:flex-row sm:items-center gap-3 p-3 bg-white/5 rounded-lg hover:bg-white/10 transition-colors"
+ className="flex flex-col sm:flex-row sm:items-center gap-3 p-3 bg-accent rounded-lg hover:bg-accent/80 transition-colors border border-border/50"
initial={{ opacity: 0, y: 20 }}
key={provider}
transition={{ delay: index * 0.05 }}
>
<div className="flex items-center gap-3 flex-1">
- <motion.div
- animate={{ rotate: 0, opacity: 1 }}
- className="flex-shrink-0"
- initial={{ rotate: -180, opacity: 0 }}
- transition={{ delay: index * 0.05 + 0.2 }}
- >
- <Icon className="h-8 w-8" />
- </motion.div>
+ <Icon className="h-8 w-8" />
<div className="flex-1 min-w-0">
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2">
- <p className="font-medium text-white text-sm">
+ <p className="font-medium text-card-foreground text-sm">
{config.title}
</p>
{isConnected ? (
<div className="flex items-center gap-1">
- <div className="w-2 h-2 bg-green-400 rounded-full" />
- <span className="text-xs text-green-400 font-medium">
+ <div className="w-2 h-2 bg-chart-2 rounded-full" />
+ <span className="text-xs text-chart-2 font-medium">
Connected
</span>
</div>
) : (
<div className="hidden sm:flex items-center gap-1">
- <div className="w-2 h-2 bg-gray-400 rounded-full" />
- <span className="text-xs text-gray-400 font-medium">
+ <div className="w-2 h-2 bg-muted-foreground rounded-full" />
+ <span className="text-xs text-muted-foreground font-medium">
Disconnected
</span>
</div>
)}
</div>
- <p className="text-xs text-white/60 mt-0.5">
+ <p className="text-xs text-muted-foreground mt-0.5">
{config.description}
</p>
{connection?.email && (
- <p className="text-xs text-white/50 mt-1">
+ <p className="text-xs text-muted-foreground/70 mt-1">
{connection.email}
</p>
)}
@@ -527,7 +523,7 @@ export function IntegrationsView() {
whileTap={{ scale: 0.95 }}
>
<Button
- className="text-white/70 hover:text-red-400 hover:bg-red-500/10 w-full sm:w-auto"
+ className="text-destructive hover:bg-destructive/10 w-full sm:w-auto"
disabled={deleteConnectionMutation.isPending}
onClick={() =>
deleteConnectionMutation.mutate(connection.id)
@@ -542,8 +538,8 @@ export function IntegrationsView() {
) : (
<div className="flex items-center justify-between gap-2 w-full sm:w-auto">
<div className="sm:hidden flex items-center gap-1">
- <div className="w-2 h-2 bg-gray-400 rounded-full" />
- <span className="text-xs text-gray-400 font-medium">
+ <div className="w-2 h-2 bg-muted-foreground rounded-full" />
+ <span className="text-xs text-muted-foreground font-medium">
Disconnected
</span>
</div>
@@ -553,7 +549,7 @@ export function IntegrationsView() {
className="flex-shrink-0"
>
<Button
- className="bg-blue-600/20 hover:bg-blue-600/30 text-blue-400 border-blue-600/30 min-w-[80px] disabled:cursor-not-allowed"
+ className="min-w-[80px] disabled:cursor-not-allowed"
disabled={
addConnectionMutation.isPending || !isProUser
}
@@ -563,7 +559,7 @@ export function IntegrationsView() {
)
}}
size="sm"
- variant="outline"
+ variant="default"
>
{addConnectionMutation.isPending &&
addConnectionMutation.variables === provider
@@ -583,14 +579,14 @@ export function IntegrationsView() {
</div>
<div className="p-3">
- <p className="text-white/70 text-sm leading-relaxed text-center">
+ <p className="text-muted-foreground text-sm leading-relaxed text-center">
More integrations are coming soon! Have a suggestion? Share it with us
on{" "}
<a
href="https://x.com/supermemoryai"
target="_blank"
rel="noopener noreferrer"
- className="text-orange-500 hover:text-orange-400 underline"
+ className="text-primary hover:text-primary/80 underline"
>
X
</a>
@@ -601,9 +597,9 @@ export function IntegrationsView() {
{/* API Key Modal */}
<Dialog open={showApiKeyModal} onOpenChange={handleDialogClose}>
<DialogPortal>
- <DialogContent className="bg-[#0f1419] border-white/10 text-white md:max-w-md z-[100]">
+ <DialogContent className="bg-card border-border text-card-foreground md:max-w-md z-[100]">
<DialogHeader>
- <DialogTitle className="text-white text-lg font-semibold">
+ <DialogTitle className="text-card-foreground text-lg font-semibold">
Setup{" "}
{selectedShortcutType === "add"
? "Add Memory"
@@ -619,7 +615,7 @@ export function IntegrationsView() {
<div className="space-y-2">
<label
htmlFor={apiKeyId}
- className="text-sm font-medium text-white/80"
+ className="text-sm font-medium text-muted-foreground"
>
Your API Key
</label>
@@ -629,16 +625,16 @@ export function IntegrationsView() {
type="text"
value={apiKey}
readOnly
- className="flex-1 bg-white/5 border border-white/20 rounded-lg px-3 py-2 text-sm text-white font-mono"
+ className="flex-1 bg-input border border-border rounded-lg px-3 py-2 text-sm text-foreground font-mono"
/>
<Button
size="sm"
variant="ghost"
onClick={handleCopyApiKey}
- className="text-white/70 hover:text-white hover:bg-white/10"
+ className="hover:bg-accent"
>
{copied ? (
- <Check className="h-4 w-4 text-green-400" />
+ <Check className="h-4 w-4 text-chart-2" />
) : (
<Copy className="h-4 w-4" />
)}
@@ -648,31 +644,31 @@ export function IntegrationsView() {
{/* Steps */}
<div className="space-y-3">
- <h4 className="text-sm font-medium text-white/80">
+ <h4 className="text-sm font-medium text-muted-foreground">
Follow these steps:
</h4>
<div className="space-y-2">
<div className="flex items-start gap-3">
- <div className="flex-shrink-0 w-6 h-6 bg-blue-500/20 text-blue-400 rounded-full flex items-center justify-center text-xs font-medium">
+ <div className="flex-shrink-0 w-6 h-6 bg-primary/20 text-primary rounded-full flex items-center justify-center text-xs font-medium">
1
</div>
- <p className="text-sm text-white/70">
+ <p className="text-sm text-muted-foreground">
Click "Add to Shortcuts" below to open the shortcut
</p>
</div>
<div className="flex items-start gap-3">
- <div className="flex-shrink-0 w-6 h-6 bg-blue-500/20 text-blue-400 rounded-full flex items-center justify-center text-xs font-medium">
+ <div className="flex-shrink-0 w-6 h-6 bg-primary/20 text-primary rounded-full flex items-center justify-center text-xs font-medium">
2
</div>
- <p className="text-sm text-white/70">
+ <p className="text-sm text-muted-foreground">
Paste your API key when prompted
</p>
</div>
<div className="flex items-start gap-3">
- <div className="flex-shrink-0 w-6 h-6 bg-blue-500/20 text-blue-400 rounded-full flex items-center justify-center text-xs font-medium">
+ <div className="flex-shrink-0 w-6 h-6 bg-primary/20 text-primary rounded-full flex items-center justify-center text-xs font-medium">
3
</div>
- <p className="text-sm text-white/70">
+ <p className="text-sm text-muted-foreground">
Start using your shortcut!
</p>
</div>
@@ -682,8 +678,9 @@ export function IntegrationsView() {
<div className="flex gap-2 pt-2">
<Button
onClick={handleOpenShortcut}
- className="flex-1 bg-blue-600 hover:bg-blue-700 text-white"
+ className="flex-1"
disabled={!selectedShortcutType}
+ variant="default"
>
<Image
src="/images/ios-shortcuts.png"
diff --git a/apps/web/components/views/profile.tsx b/apps/web/components/views/profile.tsx
index 3d49d395..93330da4 100644
--- a/apps/web/components/views/profile.tsx
+++ b/apps/web/components/views/profile.tsx
@@ -1,115 +1,61 @@
"use client"
-import { authClient } from "@lib/auth"
import { useAuth } from "@lib/auth-context"
+import {
+ fetchConnectionsFeature,
+ fetchMemoriesFeature,
+ fetchSubscriptionStatus,
+} from "@lib/queries"
import { Button } from "@repo/ui/components/button"
+import { Skeleton } from "@repo/ui/components/skeleton"
import { HeadingH3Bold } from "@repo/ui/text/heading/heading-h3-bold"
import { useCustomer } from "autumn-js/react"
-import {
- CheckCircle,
- CreditCard,
- LoaderIcon,
- LogOut,
- User,
- X,
-} from "lucide-react"
+import { CheckCircle, CreditCard, LoaderIcon, User, X } from "lucide-react"
import { motion } from "motion/react"
import Link from "next/link"
-import { useRouter } from "next/navigation"
-import { useEffect, useState } from "react"
-import { analytics } from "@/lib/analytics"
-import { $fetch } from "@lib/api"
+import { useState } from "react"
export function ProfileView() {
- const router = useRouter()
- const { user: session } = useAuth()
- const {
- customer,
- isLoading: isCustomerLoading,
- openBillingPortal,
- attach,
- } = useCustomer()
+ const { user: session, org } = useAuth()
+ const organizations = org
+ const autumn = useCustomer()
const [isLoading, setIsLoading] = useState(false)
- const [billingData, setBillingData] = useState<{
- isPro: boolean
- memoriesUsed: number
- memoriesLimit: number
- connectionsUsed: number
- connectionsLimit: number
- }>({
- isPro: false,
- memoriesUsed: 0,
- memoriesLimit: 0,
- connectionsUsed: 0,
- connectionsLimit: 0,
- })
- useEffect(() => {
- if (!isCustomerLoading) {
- const memoriesFeature = customer?.features?.memories ?? {
- usage: 0,
- included_usage: 0,
- }
- const connectionsFeature = customer?.features?.connections ?? {
- usage: 0,
- included_usage: 0,
- }
+ const {
+ data: status = {
+ consumer_pro: null,
+ },
+ isLoading: isCheckingStatus,
+ } = fetchSubscriptionStatus(autumn, !autumn.isLoading)
- setBillingData({
- isPro:
- customer?.products?.some(
- (product) => product.id === "consumer_pro",
- ) ?? false,
- memoriesUsed: memoriesFeature?.usage ?? 0,
- memoriesLimit: memoriesFeature?.included_usage ?? 0,
- connectionsUsed: connectionsFeature?.usage ?? 0,
- connectionsLimit: connectionsFeature?.included_usage ?? 0,
- })
- }
- }, [isCustomerLoading, customer])
+ const isPro = status.consumer_pro
- const handleLogout = () => {
- analytics.userSignedOut()
- authClient.signOut()
- router.push("/login")
- }
+ const { data: memoriesCheck } = fetchMemoriesFeature(
+ autumn,
+ !isCheckingStatus && !autumn.isLoading,
+ )
+ const memoriesUsed = memoriesCheck?.usage ?? 0
+ const memoriesLimit = memoriesCheck?.included_usage ?? 0
+
+ const { data: connectionsCheck } = fetchConnectionsFeature(autumn, !isCheckingStatus && !autumn.isLoading)
+ const connectionsUsed = connectionsCheck?.usage ?? 0
const handleUpgrade = async () => {
setIsLoading(true)
try {
- const upgradeResult = await attach({
+ await autumn.attach({
productId: "consumer_pro",
successUrl: "https://app.supermemory.ai/",
})
- if (
- upgradeResult.statusCode === 200 &&
- upgradeResult.data &&
- "code" in upgradeResult.data
- ) {
- const isProPlanActivated =
- upgradeResult.data.code === "new_product_attached"
- if (isProPlanActivated && session?.name && session?.email) {
- try {
- await $fetch("@post/emails/welcome/pro", {
- body: {
- email: session?.email,
- firstName: session?.name,
- },
- })
- } catch (error) {
- console.error(error)
- }
- }
- }
+ window.location.reload()
} catch (error) {
console.error(error)
setIsLoading(false)
}
}
- // Handle manage billing
const handleManageBilling = async () => {
- await openBillingPortal({
+ await autumn.openBillingPortal({
returnUrl: "https://app.supermemory.ai",
})
}
@@ -123,13 +69,13 @@ export function ProfileView() {
initial={{ opacity: 0, scale: 0.9 }}
transition={{ type: "spring", damping: 20 }}
>
- <p className="text-white/70 mb-4">
+ <p className="text-foreground/70 mb-4">
Sign in to access your profile and billing
</p>
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<Button
asChild
- className="bg-white/10 hover:bg-white/20 text-white border-white/20"
+ className="bg-muted hover:bg-muted/80 text-foreground border-border"
size="sm"
>
<Link href="/login">Sign in</Link>
@@ -143,148 +89,193 @@ export function ProfileView() {
return (
<div className="space-y-4">
{/* Profile Section */}
- <div className="bg-white/5 rounded-lg p-4 space-y-3">
- <div className="flex items-center gap-3">
- <div className="w-10 h-10 bg-white/10 rounded-full flex items-center justify-center">
- <User className="w-5 h-5 text-white/80" />
+ <div className="bg-card border border-border rounded-lg p-3 sm:p-4 space-y-4">
+ <div className="flex items-start gap-3 sm:gap-4">
+ <div className="w-12 h-12 sm:w-16 sm:h-16 bg-gradient-to-br from-blue-500 to-purple-500 rounded-full flex items-center justify-center flex-shrink-0">
+ {session?.image ? (
+ <img
+ src={session.image}
+ alt={session?.name || session?.email || "User"}
+ className="w-full h-full rounded-full object-cover"
+ />
+ ) : (
+ <User className="w-6 h-6 sm:w-8 sm:h-8 text-white" />
+ )}
</div>
- <div className="flex-1">
- <p className="text-white font-medium text-sm">{session?.email}</p>
- <p className="text-white/60 text-xs">Logged in</p>
+ <div className="flex-1 min-w-0">
+ <div className="space-y-1">
+ {session?.name && (
+ <h3 className="text-foreground font-semibold text-base sm:text-lg truncate">
+ {session.name}
+ </h3>
+ )}
+ <p className="text-foreground font-medium text-sm truncate">
+ {session?.email}
+ </p>
+ </div>
+ </div>
+ </div>
+
+ {/* Additional Profile Details */}
+ <div className="border-t border-border pt-3 space-y-2">
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4 text-xs">
+ <div>
+ <span className="text-muted-foreground block">Organization</span>
+ <span className="text-foreground font-medium">
+ {organizations?.name || "Personal"}
+ </span>
+ </div>
+ <div>
+ <span className="text-muted-foreground block">Member since</span>
+ <span className="text-foreground font-medium">
+ {session?.createdAt
+ ? new Date(session.createdAt).toLocaleDateString("en-US", {
+ month: "short",
+ year: "numeric",
+ })
+ : "Recent"}
+ </span>
+ </div>
</div>
</div>
</div>
- {isCustomerLoading ? (
- <div className="bg-white/5 rounded-lg p-4 space-y-3 flex items-center justify-center">
- <LoaderIcon className="h-4 w-4 animate-spin mr-2" />
- Loading...
+ {/* Billing Section */}
+ {autumn.isLoading || isCheckingStatus ? (
+ <div className="bg-card border border-border rounded-lg p-3 sm:p-4 space-y-3">
+ <div className="flex items-center gap-3 mb-3">
+ <Skeleton className="w-8 h-8 sm:w-10 sm:h-10 rounded-full" />
+ <div className="flex-1 space-y-2">
+ <Skeleton className="h-4 w-20" />
+ <Skeleton className="h-3 w-32" />
+ </div>
+ </div>
+ <div className="space-y-2">
+ <div className="flex justify-between items-center">
+ <Skeleton className="h-3 w-16" />
+ <Skeleton className="h-3 w-12" />
+ </div>
+ <Skeleton className="h-2 w-full rounded-full" />
+ </div>
+ <div className="flex justify-between items-center">
+ <Skeleton className="h-3 w-20" />
+ <Skeleton className="h-3 w-8" />
+ </div>
+ <div className="pt-2">
+ <Skeleton className="h-8 w-full rounded" />
+ </div>
</div>
) : (
- <>
- {/* Billing Section */}
- <div className="bg-white/5 rounded-lg p-4 space-y-3">
- <div className="flex items-center gap-3 mb-3">
- <div className="w-10 h-10 bg-white/10 rounded-full flex items-center justify-center">
- <CreditCard className="w-5 h-5 text-white/80" />
- </div>
- <div className="flex-1">
- <HeadingH3Bold className="text-white text-sm">
- {billingData.isPro ? "Pro Plan" : "Free Plan"}
- {billingData.isPro && (
- <span className="ml-2 text-xs bg-green-500/20 text-green-400 px-2 py-0.5 rounded-full">
- Active
- </span>
- )}
- </HeadingH3Bold>
- <p className="text-white/60 text-xs">
- {billingData.isPro
- ? "Expanded memory capacity"
- : "Basic plan"}
- </p>
- </div>
+ <div className="bg-card border border-border rounded-lg p-3 sm:p-4 space-y-3">
+ <div className="flex items-center gap-3 mb-3">
+ <div className="w-8 h-8 sm:w-10 sm:h-10 bg-muted rounded-full flex items-center justify-center">
+ <CreditCard className="w-4 h-4 sm:w-5 sm:h-5 text-muted-foreground" />
</div>
-
- {/* Usage Stats */}
- <div className="space-y-2">
- <div className="flex justify-between items-center">
- <span className="text-sm text-white/70">Memories</span>
- <span
- className={`text-sm ${billingData.memoriesUsed >= billingData.memoriesLimit ? "text-red-400" : "text-white/90"}`}
- >
- {billingData.memoriesUsed} / {billingData.memoriesLimit}
- </span>
- </div>
- <div className="w-full bg-white/10 rounded-full h-2">
- <div
- className={`h-2 rounded-full transition-all ${
- billingData.memoriesUsed >= billingData.memoriesLimit
- ? "bg-red-500"
- : billingData.isPro
- ? "bg-green-500"
- : "bg-blue-500"
- }`}
- style={{
- width: `${Math.min((billingData.memoriesUsed / billingData.memoriesLimit) * 100, 100)}%`,
- }}
- />
- </div>
+ <div className="flex-1">
+ <HeadingH3Bold className="text-foreground text-sm">
+ {isPro ? "Pro Plan" : "Free Plan"}
+ {isPro && (
+ <span className="ml-2 text-xs bg-green-500/20 text-green-600 dark:text-green-400 px-2 py-0.5 rounded-full">
+ Active
+ </span>
+ )}
+ </HeadingH3Bold>
+ <p className="text-muted-foreground text-xs">
+ {isPro ? "Expanded memory capacity" : "Basic plan"}
+ </p>
</div>
+ </div>
- {billingData.isPro && (
- <div className="flex justify-between items-center">
- <span className="text-sm text-white/70">Connections</span>
- <span className="text-sm text-white/90">
- {billingData.connectionsUsed} / 10
- </span>
- </div>
- )}
-
- {/* Billing Actions */}
- <div className="pt-2">
- {billingData.isPro ? (
- <motion.div
- whileHover={{ scale: 1.02 }}
- whileTap={{ scale: 0.98 }}
- >
- <Button
- className="w-full bg-white/10 hover:bg-white/20 text-white border-white/20"
- onClick={handleManageBilling}
- size="sm"
- variant="outline"
- >
- Manage Billing
- </Button>
- </motion.div>
- ) : (
- <motion.div
- whileHover={{ scale: 1.02 }}
- whileTap={{ scale: 0.98 }}
- >
- <Button
- className="w-full bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white border-0"
- disabled={isLoading || isCustomerLoading}
- onClick={handleUpgrade}
- size="sm"
- >
- {isLoading || isCustomerLoading ? (
- <>
- <LoaderIcon className="h-4 w-4 animate-spin mr-2" />
- Upgrading...
- </>
- ) : (
- "Upgrade to Pro - $15/month"
- )}
- </Button>
- </motion.div>
- )}
+ {/* Usage Stats */}
+ <div className="space-y-2">
+ <div className="flex justify-between items-center">
+ <span className="text-sm text-muted-foreground">Memories</span>
+ <span
+ className={`text-sm ${memoriesUsed >= memoriesLimit ? "text-red-500" : "text-foreground"}`}
+ >
+ {memoriesUsed} / {memoriesLimit}
+ </span>
+ </div>
+ <div className="w-full bg-muted-foreground/50 rounded-full h-2">
+ <div
+ className={`h-2 rounded-full transition-all ${
+ memoriesUsed >= memoriesLimit
+ ? "bg-red-500"
+ : isPro
+ ? "bg-green-500"
+ : "bg-blue-500"
+ }`}
+ style={{
+ width: `${Math.min((memoriesUsed / memoriesLimit) * 100, 100)}%`,
+ }}
+ />
</div>
</div>
- {/* Plan Comparison - Only show for free users */}
- {!billingData.isPro && (
- <div className="bg-white/5 rounded-lg p-4 space-y-4">
- <HeadingH3Bold className="text-white text-sm">
- Upgrade to Pro
- </HeadingH3Bold>
+ {isPro && (
+ <div className="flex justify-between items-center">
+ <span className="text-sm text-muted-foreground">Connections</span>
+ <span className="text-sm text-foreground">
+ {connectionsUsed} / 10
+ </span>
+ </div>
+ )}
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ {/* Billing Actions */}
+ <div className="pt-2">
+ {isPro ? (
+ <Button
+ className="w-full"
+ onClick={handleManageBilling}
+ size="sm"
+ variant="default"
+ >
+ Manage Billing
+ </Button>
+ ) : (
+ <Button
+ className="w-full bg-[#267ffa] hover:bg-[#267ffa]/90 text-white border-0"
+ disabled={isLoading || isCheckingStatus}
+ onClick={handleUpgrade}
+ size="lg"
+ >
+ {isLoading || isCheckingStatus ? (
+ <>
+ <LoaderIcon className="h-4 w-4 animate-spin mr-2" />
+ <span className="hidden sm:inline">Upgrading...</span>
+ <span className="sm:hidden">Loading...</span>
+ </>
+ ) : (
+ <>
+ <span className="hidden sm:inline">
+ Upgrade to Pro - $15/month
+ </span>
+ <span className="sm:hidden">Upgrade to Pro</span>
+ </>
+ )}
+ </Button>
+ )}
+ </div>
+
+ {!isPro && (
+ <div className="space-y-4">
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
{/* Free Plan */}
- <div className="p-3 bg-white/5 rounded-lg border border-white/10">
- <h4 className="font-medium text-white/90 mb-3 text-sm">
+ <div className="p-3 bg-muted/50 rounded-lg border border-border">
+ <h4 className="font-medium text-foreground mb-3 text-sm">
Free Plan
</h4>
<ul className="space-y-2">
- <li className="flex items-center gap-2 text-sm text-white/70">
- <CheckCircle className="h-4 w-4 text-green-400" />
+ <li className="flex items-center gap-2 text-xs sm:text-sm text-muted-foreground">
+ <CheckCircle className="h-3 w-3 sm:h-4 sm:w-4 text-green-500 flex-shrink-0" />
200 memories
</li>
- <li className="flex items-center gap-2 text-sm text-white/70">
- <X className="h-4 w-4 text-red-400" />
+ <li className="flex items-center gap-2 text-xs sm:text-sm text-muted-foreground">
+ <X className="h-3 w-3 sm:h-4 sm:w-4 text-red-500 flex-shrink-0" />
No connections
</li>
- <li className="flex items-center gap-2 text-sm text-white/70">
- <CheckCircle className="h-4 w-4 text-green-400" />
+ <li className="flex items-center gap-2 text-xs sm:text-sm text-muted-foreground">
+ <CheckCircle className="h-3 w-3 sm:h-4 sm:w-4 text-green-500 flex-shrink-0" />
Basic search
</li>
</ul>
@@ -292,50 +283,43 @@ export function ProfileView() {
{/* Pro Plan */}
<div className="p-3 bg-gradient-to-br from-blue-500/10 to-purple-500/10 rounded-lg border border-blue-500/20">
- <h4 className="font-medium text-white mb-3 flex items-center gap-2 text-sm">
- Pro Plan
- <span className="text-xs bg-blue-500/20 text-blue-400 px-2 py-0.5 rounded-full">
- Recommended
- </span>
+ <h4 className="font-medium text-foreground mb-3 text-sm">
+ <div className="flex items-center gap-2 flex-wrap">
+ <span>Pro Plan</span>
+ <span className="text-xs bg-blue-500/20 text-blue-600 dark:text-blue-400 px-2 py-0.5 rounded-full">
+ Recommended
+ </span>
+ </div>
</h4>
<ul className="space-y-2">
- <li className="flex items-center gap-2 text-sm text-white/90">
+ <li className="flex items-center gap-2 text-sm text-black/90">
<CheckCircle className="h-4 w-4 text-green-400" />
Unlimited memories
</li>
- <li className="flex items-center gap-2 text-sm text-white/90">
- <CheckCircle className="h-4 w-4 text-green-400" />
+ <li className="flex items-center gap-2 text-xs sm:text-sm text-foreground">
+ <CheckCircle className="h-3 w-3 sm:h-4 sm:w-4 text-green-500 flex-shrink-0" />
10 connections
</li>
- <li className="flex items-center gap-2 text-sm text-white/90">
- <CheckCircle className="h-4 w-4 text-green-400" />
+ <li className="flex items-center gap-2 text-xs sm:text-sm text-foreground">
+ <CheckCircle className="h-3 w-3 sm:h-4 sm:w-4 text-green-500 flex-shrink-0" />
Advanced search
</li>
- <li className="flex items-center gap-2 text-sm text-white/90">
- <CheckCircle className="h-4 w-4 text-green-400" />
+ <li className="flex items-center gap-2 text-xs sm:text-sm text-foreground">
+ <CheckCircle className="h-3 w-3 sm:h-4 sm:w-4 text-green-500 flex-shrink-0" />
Priority support
</li>
</ul>
</div>
</div>
- <p className="text-xs text-white/50 text-center">
+ <p className="text-xs text-muted-foreground text-center leading-relaxed">
$15/month (only for first 100 users) • Cancel anytime. No
questions asked.
</p>
</div>
)}
- </>
+ </div>
)}
-
- <Button
- className="w-full bg-red-500/20 hover:bg-red-500/30 text-red-200 border-red-500/30"
- onClick={handleLogout}
- variant="destructive"
- >
- <LogOut className="w-4 h-4 mr-2" />
- Sign Out
- </Button>
</div>
)
-}
+} \ No newline at end of file
diff --git a/apps/web/globals.css b/apps/web/globals.css
index 3571db7d..2c150cf0 100644
--- a/apps/web/globals.css
+++ b/apps/web/globals.css
@@ -1,2 +1,11 @@
@import "tailwindcss";
@plugin "@tailwindcss/typography";
+
+
+.sm-tweet-theme .react-tweet-theme {
+ --tweet-container-margin: 0px;
+}
+
+.sm-tweet-theme .tweet-header-module__A9EVQG__authorFollow {
+ display: none;
+}
diff --git a/apps/web/lib/document-icon.tsx b/apps/web/lib/document-icon.tsx
index 3a80b2e0..a0f56e63 100644
--- a/apps/web/lib/document-icon.tsx
+++ b/apps/web/lib/document-icon.tsx
@@ -1,3 +1,4 @@
+import { MCPIcon } from '@/components/menu';
import { colors } from '@repo/ui/memory-graph/constants';
import {
GoogleDocs,
@@ -14,12 +15,16 @@ import {
} from '@ui/assets/icons';
import { FileText } from 'lucide-react';
-export const getDocumentIcon = (type: string, className: string) => {
+export const getDocumentIcon = (type: string, className: string, source?: string) => {
const iconProps = {
className,
style: { color: colors.text.muted },
};
+ if(source === "mcp") {
+ return <MCPIcon {...iconProps} />;
+ }
+
switch (type) {
case 'google_doc':
return <GoogleDocs {...iconProps} />;
diff --git a/apps/web/lib/theme-provider.tsx b/apps/web/lib/theme-provider.tsx
new file mode 100644
index 00000000..7e3a551f
--- /dev/null
+++ b/apps/web/lib/theme-provider.tsx
@@ -0,0 +1,8 @@
+"use client"
+
+import { ThemeProvider as NextThemesProvider } from "next-themes"
+import type { ThemeProviderProps } from "next-themes"
+
+export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
+ return <NextThemesProvider {...props}>{children}</NextThemesProvider>
+}
diff --git a/apps/web/package.json b/apps/web/package.json
index 057532f4..61b99bcb 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -49,11 +49,13 @@
"@tanstack/react-query-devtools": "^5.84.2",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.12",
+ "@types/dompurify": "^3.2.0",
"ai": "5.0.0-beta.24",
"autumn-js": "0.0.116",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
+ "dompurify": "^3.2.7",
"dotenv": "^16.6.0",
"embla-carousel-autoplay": "^8.6.0",
"embla-carousel-react": "^8.6.0",
@@ -61,6 +63,7 @@
"is-hotkey": "^0.2.0",
"isbot": "^5.1.28",
"lucide-react": "^0.525.0",
+ "masonic": "^4.1.0",
"motion": "^12.19.2",
"next": "15.3.0",
"next-themes": "^0.4.6",
@@ -71,6 +74,7 @@
"react-dom": "^19.1.0",
"react-dropzone": "^14.3.8",
"react-error-boundary": "^6.0.0",
+ "react-tweet": "^3.2.2",
"recharts": "2",
"shadcn-dropzone": "^0.2.1",
"slate": "^0.118.0",
diff --git a/apps/web/stores/chat.ts b/apps/web/stores/chat.ts
index fe27275e..ac204442 100644
--- a/apps/web/stores/chat.ts
+++ b/apps/web/stores/chat.ts
@@ -1,6 +1,7 @@
import type { UIMessage } from "@ai-sdk/react"
import { create } from "zustand"
import { persist } from "zustand/middleware"
+import { useCallback } from "react"
/**
* Deep equality check for UIMessage arrays to prevent unnecessary state updates
@@ -206,39 +207,48 @@ export function usePersistentChat() {
const currentChatId = projectState?.currentChatId ?? null
- function setCurrentChatId(chatId: string | null): void {
- setCurrentChatIdRaw(projectId, chatId)
- }
+ const setCurrentChatId = useCallback(
+ (chatId: string | null): void => {
+ setCurrentChatIdRaw(projectId, chatId)
+ },
+ [projectId, setCurrentChatIdRaw],
+ )
- function setConversation(chatId: string, messages: UIMessage[]): void {
- setConversationRaw(projectId, chatId, messages)
- }
+ const setConversation = useCallback(
+ (chatId: string, messages: UIMessage[]): void => {
+ setConversationRaw(projectId, chatId, messages)
+ },
+ [projectId, setConversationRaw],
+ )
- function deleteConversation(chatId: string): void {
- deleteConversationRaw(projectId, chatId)
- }
+ const deleteConversation = useCallback(
+ (chatId: string): void => {
+ deleteConversationRaw(projectId, chatId)
+ },
+ [projectId, deleteConversationRaw],
+ )
- function setConversationTitle(
- chatId: string,
- title: string | undefined,
- ): void {
- setConversationTitleRaw(projectId, chatId, title)
- }
+ const setConversationTitle = useCallback(
+ (chatId: string, title: string | undefined): void => {
+ setConversationTitleRaw(projectId, chatId, title)
+ },
+ [projectId, setConversationTitleRaw],
+ )
- function getCurrentConversation(): UIMessage[] | undefined {
+ const getCurrentConversation = useCallback((): UIMessage[] | undefined => {
const convs = projectState?.conversations ?? {}
const id = currentChatId
if (!id) return undefined
return convs[id]?.messages
- }
+ }, [projectState?.conversations, currentChatId])
- function getCurrentChat(): ConversationSummary | undefined {
+ 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,
diff --git a/apps/web/stores/index.ts b/apps/web/stores/index.ts
index fb367cf7..30c33c0f 100644
--- a/apps/web/stores/index.ts
+++ b/apps/web/stores/index.ts
@@ -1,9 +1,9 @@
-import { create } from "zustand";
-import { persist } from "zustand/middleware";
+import { create } from "zustand"
+import { persist } from "zustand/middleware"
interface ProjectState {
- selectedProject: string;
- setSelectedProject: (projectId: string) => void;
+ selectedProject: string
+ setSelectedProject: (projectId: string) => void
}
export const useProjectStore = create<ProjectState>()(
@@ -16,14 +16,14 @@ export const useProjectStore = create<ProjectState>()(
name: "selectedProject",
},
),
-);
+)
interface MemoryGraphState {
- positionX: number;
- positionY: number;
- setPositionX: (x: number) => void;
- setPositionY: (y: number) => void;
- setPosition: (x: number, y: number) => void;
+ positionX: number
+ positionY: number
+ setPositionX: (x: number) => void
+ setPositionY: (y: number) => void
+ setPosition: (x: number, y: number) => void
}
export const useMemoryGraphStore = create<MemoryGraphState>()((set) => ({
@@ -32,34 +32,34 @@ export const useMemoryGraphStore = create<MemoryGraphState>()((set) => ({
setPositionX: (x) => set({ positionX: x }),
setPositionY: (y) => set({ positionY: y }),
setPosition: (x, y) => set({ positionX: x, positionY: y }),
-}));
+}))
interface ChatState {
- isOpen: boolean;
- setIsOpen: (isOpen: boolean) => void;
- toggleChat: () => void;
+ isOpen: boolean
+ setIsOpen: (isOpen: boolean) => void
+ toggleChat: () => void
}
export const useChatStore = create<ChatState>()((set, get) => ({
isOpen: false,
setIsOpen: (isOpen) => set({ isOpen }),
toggleChat: () => set({ isOpen: !get().isOpen }),
-}));
+}))
export function useProject() {
- const selectedProject = useProjectStore((state) => state.selectedProject);
+ const selectedProject = useProjectStore((state) => state.selectedProject)
const setSelectedProject = useProjectStore(
(state) => state.setSelectedProject,
- );
- return { selectedProject, setSelectedProject };
+ )
+ return { selectedProject, setSelectedProject }
}
export function useMemoryGraphPosition() {
- const positionX = useMemoryGraphStore((state) => state.positionX);
- const positionY = useMemoryGraphStore((state) => state.positionY);
- const setPositionX = useMemoryGraphStore((state) => state.setPositionX);
- const setPositionY = useMemoryGraphStore((state) => state.setPositionY);
- const setPosition = useMemoryGraphStore((state) => state.setPosition);
+ const positionX = useMemoryGraphStore((state) => state.positionX)
+ const positionY = useMemoryGraphStore((state) => state.positionY)
+ const setPositionX = useMemoryGraphStore((state) => state.setPositionX)
+ const setPositionY = useMemoryGraphStore((state) => state.setPositionY)
+ const setPosition = useMemoryGraphStore((state) => state.setPosition)
return {
x: positionX,
@@ -67,14 +67,33 @@ export function useMemoryGraphPosition() {
setX: setPositionX,
setY: setPositionY,
setPosition,
- };
+ }
}
export function useChatOpen() {
- const isOpen = useChatStore((state) => state.isOpen);
- const setIsOpen = useChatStore((state) => state.setIsOpen);
- const toggleChat = useChatStore((state) => state.toggleChat);
- return { isOpen, setIsOpen, toggleChat };
+ const isOpen = useChatStore((state) => state.isOpen)
+ const setIsOpen = useChatStore((state) => state.setIsOpen)
+ const toggleChat = useChatStore((state) => state.toggleChat)
+ return { isOpen, setIsOpen, toggleChat }
}
-export { usePersistentChat, usePersistentChatStore } from "./chat";
+interface GraphModalState {
+ isOpen: boolean
+ setIsOpen: (isOpen: boolean) => void
+ toggleGraphModal: () => void
+}
+
+export const useGraphModalStore = create<GraphModalState>()((set, get) => ({
+ isOpen: false,
+ setIsOpen: (isOpen) => set({ isOpen }),
+ toggleGraphModal: () => set({ isOpen: !get().isOpen }),
+}))
+
+export function useGraphModal() {
+ const isOpen = useGraphModalStore((state) => state.isOpen)
+ const setIsOpen = useGraphModalStore((state) => state.setIsOpen)
+ const toggleGraphModal = useGraphModalStore((state) => state.toggleGraphModal)
+ return { isOpen, setIsOpen, toggleGraphModal }
+}
+
+export { usePersistentChat, usePersistentChatStore } from "./chat"