aboutsummaryrefslogtreecommitdiff
path: root/apps/web/components
diff options
context:
space:
mode:
authorDhravya Shah <[email protected]>2025-10-03 02:37:50 -0700
committerDhravya Shah <[email protected]>2025-10-03 02:37:50 -0700
commitbe34a14550e03302c160310464deef541bda3154 (patch)
tree16104bc3501962a3030937426eb2982a952e7908 /apps/web/components
parentfix: docs (diff)
parentfix: raycast org selection based api key creation (#447) (diff)
downloadarchived-supermemory-be34a14550e03302c160310464deef541bda3154.tar.xz
archived-supermemory-be34a14550e03302c160310464deef541bda3154.zip
Merge branch 'main' of https://github.com/supermemoryai/supermemory
Diffstat (limited to 'apps/web/components')
-rw-r--r--apps/web/components/chrome-extension-button.tsx234
-rw-r--r--apps/web/components/content-cards/google-docs.tsx88
-rw-r--r--apps/web/components/content-cards/note.tsx96
-rw-r--r--apps/web/components/content-cards/tweet.tsx66
-rw-r--r--apps/web/components/content-cards/website.tsx88
-rw-r--r--apps/web/components/header.tsx27
-rw-r--r--apps/web/components/masonry-memory-list.tsx8
-rw-r--r--apps/web/components/views/add-memory/action-buttons.tsx118
-rw-r--r--apps/web/components/views/add-memory/index.tsx7
-rw-r--r--apps/web/components/views/billing.tsx10
-rw-r--r--apps/web/components/views/integrations.tsx246
-rw-r--r--apps/web/components/views/profile.tsx51
12 files changed, 893 insertions, 146 deletions
diff --git a/apps/web/components/chrome-extension-button.tsx b/apps/web/components/chrome-extension-button.tsx
new file mode 100644
index 00000000..5fd58cac
--- /dev/null
+++ b/apps/web/components/chrome-extension-button.tsx
@@ -0,0 +1,234 @@
+"use client"
+
+import { Button } from "@ui/components/button"
+import {
+ Bookmark,
+ Zap,
+ CircleX,
+ Users,
+ Lock,
+ ChromeIcon,
+ TwitterIcon,
+} from "lucide-react"
+import { useEffect, useState } from "react"
+import { motion } from "framer-motion"
+import Image from "next/image"
+import { analytics } from "@/lib/analytics"
+
+export function ChromeExtensionButton() {
+ const [isExtensionInstalled, setIsExtensionInstalled] = useState(false)
+ const [isChecking, setIsChecking] = useState(true)
+ const [isDismissed, setIsDismissed] = useState(false)
+ const [isMinimized, setIsMinimized] = useState(false)
+
+ useEffect(() => {
+ const dismissed =
+ localStorage.getItem("chrome-extension-dismissed") === "true"
+ setIsDismissed(dismissed)
+
+ const checkExtension = () => {
+ const message = { action: "check-extension" }
+
+ const timeout = setTimeout(() => {
+ setIsExtensionInstalled(false)
+ setIsChecking(false)
+ // Auto-minimize after 3 seconds if extension is not installed and not dismissed
+ if (!dismissed) {
+ setTimeout(() => {
+ setIsMinimized(true)
+ }, 3000)
+ }
+ }, 1000)
+
+ const handleMessage = (event: MessageEvent) => {
+ if (event.data?.action === "extension-detected") {
+ clearTimeout(timeout)
+ setIsExtensionInstalled(true)
+ setIsChecking(false)
+ window.removeEventListener("message", handleMessage)
+ }
+ }
+
+ window.addEventListener("message", handleMessage)
+
+ window.postMessage(message, "*")
+
+ return () => {
+ clearTimeout(timeout)
+ window.removeEventListener("message", handleMessage)
+ }
+ }
+
+ if (!dismissed) {
+ checkExtension()
+ } else {
+ setIsChecking(false)
+ }
+ }, [])
+
+ const handleInstall = () => {
+ analytics.extensionInstallClicked()
+ window.open(
+ "https://chromewebstore.google.com/detail/supermemory/afpgkkipfdpeaflnpoaffkcankadgjfc",
+ "_blank",
+ "noopener,noreferrer",
+ )
+ }
+
+ const handleDismiss = () => {
+ localStorage.setItem("chrome-extension-dismissed", "true")
+ setIsDismissed(true)
+ }
+
+ // Don't show if extension is installed, checking, or dismissed
+ if (isExtensionInstalled || isChecking || isDismissed) {
+ return null
+ }
+
+ return (
+ <motion.div
+ className="fixed bottom-4 right-4 z-50"
+ initial={{ opacity: 0, y: 20, scale: 0.9 }}
+ animate={{ opacity: 1, y: 0, scale: 1 }}
+ transition={{ duration: 0.3, ease: "easeOut" }}
+ >
+ <div
+ className={`bg-background/95 backdrop-blur-md shadow-xl ${
+ isMinimized
+ ? "flex items-center gap-1 rounded-full"
+ : "max-w-md w-90 rounded-2xl"
+ }`}
+ >
+ {!isMinimized && (
+ <motion.div
+ initial={{ opacity: 0, y: 10 }}
+ animate={{ opacity: 1, y: 0 }}
+ exit={{ opacity: 0, y: -10 }}
+ transition={{ duration: 0.3, ease: [0.4, 0, 0.2, 1] }}
+ className="overflow-hidden"
+ >
+ <div className="p-4 text-white bg-cover bg-center">
+ <div
+ className="p-4 rounded-lg"
+ style={{
+ backgroundImage: "url('/images/extension-bg.png')",
+ backgroundSize: "cover",
+ backgroundPosition: "center",
+ backgroundRepeat: "no-repeat",
+ }}
+ >
+ <div className="relative">
+ <h1 className="text-2xl font-bold mb-1">
+ supermemory extension
+ </h1>
+ <p className="text-sm opacity-90">
+ your second brain for the web.
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <div className="px-6 py-2 pb-4 space-y-4">
+ <div className="flex items-start gap-3">
+ <div className="w-10 h-10 bg-blue-50 border border-blue-200 rounded-lg flex items-center justify-center flex-shrink-0">
+ <TwitterIcon className="fill-blue-500 text-blue-500" />
+ </div>
+ <div>
+ <h3 className="font-semibold text-sm text-gray-800">
+ Twitter Imports
+ </h3>
+ <p className="text-xs text-gray-600">
+ Import your twitter timeline & save tweets.
+ </p>
+ </div>
+ </div>
+
+ <div className="flex items-start gap-3">
+ <div className="w-10 h-10 bg-orange-50 border border-orange-200 rounded-lg flex items-center justify-center flex-shrink-0">
+ <Bookmark className="w-5 h-5 text-orange-600" />
+ </div>
+ <div>
+ <h3 className="font-semibold text-sm text-gray-800">
+ Save All Bookmarks
+ </h3>
+ <p className="text-xs text-gray-600">
+ Instantly save any webpage to your memory.
+ </p>
+ </div>
+ </div>
+
+ <div className="flex items-start gap-3">
+ <div className="w-10 h-10 bg-green-50 border border-green-200 rounded-lg flex items-center justify-center flex-shrink-0">
+ <Zap className="w-5 h-5 text-green-600" />
+ </div>
+ <div>
+ <h3 className="font-semibold text-sm text-gray-800">
+ Charge Empty Memory
+ </h3>
+ <p className="text-xs text-gray-600">
+ Automatically capture & organize your browsing history.
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <div className="px-6 pb-4">
+ <Button
+ onClick={handleInstall}
+ className="w-full bg-white border border-[#686CFD] text-gray-800 hover:bg-gray-50 font-semibold rounded-lg h-10 flex items-center justify-center gap-3"
+ >
+ <div className="w-6 h-6 bg-[#686CFD] rounded-full flex items-center justify-center">
+ <Image
+ src="/images/extension-logo.png"
+ alt="Extension Logo"
+ width={24}
+ height={24}
+ />
+ </div>
+ Add to Chrome - It's Free
+ </Button>
+ </div>
+
+ <div className="px-6 pb-4 flex items-center justify-center gap-6 text-xs text-gray-500">
+ <div className="flex items-center gap-1">
+ <Users className="w-3 h-3" />
+ <span>4K+ users</span>
+ </div>
+ <div className="flex items-center gap-1">
+ <Lock className="w-3 h-3" />
+ <span>Privacy first</span>
+ </div>
+ </div>
+ </motion.div>
+ )}
+
+ {isMinimized && (
+ <div className="relative flex items-center w-full group">
+ <Button
+ size={"lg"}
+ onClick={handleInstall}
+ className="text-xs rounded-full"
+ style={{
+ backgroundImage: "url('/images/extension-bg.png')",
+ backgroundSize: "cover",
+ backgroundPosition: "center",
+ backgroundRepeat: "no-repeat",
+ }}
+ >
+ <ChromeIcon className="h-3 w-3 mr-1" />
+ Get Extension
+ </Button>
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={handleDismiss}
+ className="absolute top-[-16px] right-[-12px] h-6 w-6 p-0 text-muted-foreground hover:text-foreground opacity-0 group-hover:opacity-75 transition-opacity duration-200"
+ >
+ <CircleX className="w-4 h-4" />
+ </Button>
+ </div>
+ )}
+ </div>
+ </motion.div>
+ )
+}
diff --git a/apps/web/components/content-cards/google-docs.tsx b/apps/web/components/content-cards/google-docs.tsx
index 22f06f77..0306876d 100644
--- a/apps/web/components/content-cards/google-docs.tsx
+++ b/apps/web/components/content-cards/google-docs.tsx
@@ -2,8 +2,18 @@
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 {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from "@repo/ui/components/alert-dialog"
+import { ExternalLink, FileText, Brain, Trash2 } from "lucide-react"
import { cn } from "@lib/utils"
import { colors } from "@repo/ui/memory-graph/constants"
import { getPastelBackgroundColor } from "../memories-utils"
@@ -14,6 +24,7 @@ interface GoogleDocsCardProps {
description?: string | null
className?: string
onClick?: () => void
+ onDelete?: () => void
showExternalLink?: boolean
activeMemories?: Array<{ id: string; isForgotten?: boolean }>
lastModified?: string | Date
@@ -25,12 +36,11 @@ export const GoogleDocsCard = ({
description,
className,
onClick,
+ onDelete,
showExternalLink = true,
activeMemories,
lastModified,
}: GoogleDocsCardProps) => {
- const [imageError, setImageError] = useState(false)
-
const handleCardClick = () => {
if (onClick) {
onClick()
@@ -57,6 +67,54 @@ export const GoogleDocsCard = ({
backgroundColor: getPastelBackgroundColor(url || title || "googledocs"),
}}
>
+ {onDelete && (
+ <AlertDialog>
+ <AlertDialogTrigger asChild>
+ <button
+ className="absolute top-2 right-2 z-20 opacity-0 group-hover:opacity-100 transition-opacity p-1.5 rounded-md hover:bg-red-500/20"
+ onClick={(e) => {
+ e.stopPropagation()
+ }}
+ style={{
+ color: colors.text.muted,
+ backgroundColor: "rgba(255, 255, 255, 0.1)",
+ backdropFilter: "blur(4px)",
+ }}
+ type="button"
+ >
+ <Trash2 className="w-3.5 h-3.5" />
+ </button>
+ </AlertDialogTrigger>
+ <AlertDialogContent>
+ <AlertDialogHeader>
+ <AlertDialogTitle>Delete Document</AlertDialogTitle>
+ <AlertDialogDescription>
+ Are you sure you want to delete this document and all its
+ related memories? This action cannot be undone.
+ </AlertDialogDescription>
+ </AlertDialogHeader>
+ <AlertDialogFooter>
+ <AlertDialogCancel
+ onClick={(e) => {
+ e.stopPropagation()
+ }}
+ >
+ Cancel
+ </AlertDialogCancel>
+ <AlertDialogAction
+ className="bg-red-600 hover:bg-red-700 text-white"
+ onClick={(e) => {
+ e.stopPropagation()
+ onDelete()
+ }}
+ >
+ Delete
+ </AlertDialogAction>
+ </AlertDialogFooter>
+ </AlertDialogContent>
+ </AlertDialog>
+ )}
+
<CardContent className="p-0">
<div className="px-4 border-b border-white/10">
<div className="flex items-center justify-between">
@@ -99,16 +157,18 @@ export const GoogleDocsCard = ({
</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 className="flex items-center gap-1">
+ {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>
diff --git a/apps/web/components/content-cards/note.tsx b/apps/web/components/content-cards/note.tsx
index e7703d9b..b0014bf6 100644
--- a/apps/web/components/content-cards/note.tsx
+++ b/apps/web/components/content-cards/note.tsx
@@ -1,8 +1,19 @@
import { Badge } from "@repo/ui/components/badge"
import { Card, CardContent, CardHeader } from "@repo/ui/components/card"
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from "@repo/ui/components/alert-dialog"
import { colors } from "@repo/ui/memory-graph/constants"
-import { Brain, ExternalLink } from "lucide-react"
+import { Brain, ExternalLink, Trash2 } from "lucide-react"
import { cn } from "@lib/utils"
import {
formatDate,
@@ -32,6 +43,7 @@ export const NoteCard = ({
activeMemories,
forgottenMemories,
onOpenDetails,
+ onDelete,
}: NoteCardProps) => {
return (
<Card
@@ -47,6 +59,52 @@ export const NoteCard = ({
width: width,
}}
>
+ <AlertDialog>
+ <AlertDialogTrigger asChild>
+ <button
+ className="absolute top-2 right-2 z-20 opacity-0 group-hover:opacity-100 group-hover:cursor-pointer transition-opacity p-1.5 rounded-md hover:bg-red-500/20"
+ onClick={(e) => {
+ e.stopPropagation()
+ }}
+ style={{
+ color: colors.text.muted,
+ backgroundColor: "rgba(255, 255, 255, 0.1)",
+ backdropFilter: "blur(4px)",
+ }}
+ type="button"
+ >
+ <Trash2 className="w-3.5 h-3.5" />
+ </button>
+ </AlertDialogTrigger>
+ <AlertDialogContent>
+ <AlertDialogHeader>
+ <AlertDialogTitle>Delete Document</AlertDialogTitle>
+ <AlertDialogDescription>
+ Are you sure you want to delete this document and all its related
+ memories? This action cannot be undone.
+ </AlertDialogDescription>
+ </AlertDialogHeader>
+ <AlertDialogFooter>
+ <AlertDialogCancel
+ onClick={(e) => {
+ e.stopPropagation()
+ }}
+ >
+ Cancel
+ </AlertDialogCancel>
+ <AlertDialogAction
+ className="bg-red-600 hover:bg-red-700 text-white"
+ onClick={(e) => {
+ e.stopPropagation()
+ onDelete(document)
+ }}
+ >
+ Delete
+ </AlertDialogAction>
+ </AlertDialogFooter>
+ </AlertDialogContent>
+ </AlertDialog>
+
<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">
@@ -59,23 +117,25 @@ export const NoteCard = ({
{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-1">
+ {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>
<div className="flex items-center gap-2 text-[10px] text-muted-foreground">
<span>{formatDate(document.createdAt)}</span>
</div>
diff --git a/apps/web/components/content-cards/tweet.tsx b/apps/web/components/content-cards/tweet.tsx
index 3f46d6cc..34db9eb5 100644
--- a/apps/web/components/content-cards/tweet.tsx
+++ b/apps/web/components/content-cards/tweet.tsx
@@ -14,7 +14,18 @@ import {
enrichTweet,
} from "react-tweet"
import { Badge } from "@repo/ui/components/badge"
-import { Brain } from "lucide-react"
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from "@repo/ui/components/alert-dialog"
+import { Brain, Trash2 } from "lucide-react"
import { colors } from "@repo/ui/memory-graph/constants"
import { getPastelBackgroundColor } from "../memories-utils"
@@ -71,18 +82,69 @@ const CustomTweet = ({
export const TweetCard = ({
data,
activeMemories,
+ onDelete,
}: {
data: Tweet
activeMemories?: Array<{ id: string; isForgotten?: boolean }>
+ onDelete?: () => void
}) => {
return (
<div
- className="relative transition-all"
+ className="relative transition-all group"
style={{
backgroundColor: getPastelBackgroundColor(data.id_str || "tweet"),
}}
>
<CustomTweet components={{}} tweet={data} />
+
+ {onDelete && (
+ <AlertDialog>
+ <AlertDialogTrigger asChild>
+ <button
+ className="absolute top-2 right-2 z-20 opacity-0 group-hover:opacity-100 transition-opacity p-1.5 rounded-md hover:bg-red-500/20"
+ onClick={(e) => {
+ e.stopPropagation()
+ }}
+ style={{
+ color: colors.text.muted,
+ backgroundColor: "rgba(255, 255, 255, 0.1)",
+ backdropFilter: "blur(4px)",
+ }}
+ type="button"
+ >
+ <Trash2 className="w-3.5 h-3.5" />
+ </button>
+ </AlertDialogTrigger>
+ <AlertDialogContent>
+ <AlertDialogHeader>
+ <AlertDialogTitle>Delete Document</AlertDialogTitle>
+ <AlertDialogDescription>
+ Are you sure you want to delete this document and all its
+ related memories? This action cannot be undone.
+ </AlertDialogDescription>
+ </AlertDialogHeader>
+ <AlertDialogFooter>
+ <AlertDialogCancel
+ onClick={(e) => {
+ e.stopPropagation()
+ }}
+ >
+ Cancel
+ </AlertDialogCancel>
+ <AlertDialogAction
+ className="bg-red-600 hover:bg-red-700 text-white"
+ onClick={(e) => {
+ e.stopPropagation()
+ onDelete()
+ }}
+ >
+ Delete
+ </AlertDialogAction>
+ </AlertDialogFooter>
+ </AlertDialogContent>
+ </AlertDialog>
+ )}
+
{activeMemories && activeMemories.length > 0 && (
<div className="absolute bottom-2 left-4 z-10">
<Badge
diff --git a/apps/web/components/content-cards/website.tsx b/apps/web/components/content-cards/website.tsx
index f36cd247..b3ee7df6 100644
--- a/apps/web/components/content-cards/website.tsx
+++ b/apps/web/components/content-cards/website.tsx
@@ -1,10 +1,22 @@
"use client"
import { Card, CardContent } from "@repo/ui/components/card"
-import { ExternalLink } from "lucide-react"
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from "@repo/ui/components/alert-dialog"
+import { ExternalLink, Trash2 } from "lucide-react"
import { useState } from "react"
import { cn } from "@lib/utils"
import { getPastelBackgroundColor } from "../memories-utils"
+import { colors } from "@repo/ui/memory-graph/constants"
interface WebsiteCardProps {
title: string
@@ -13,6 +25,7 @@ interface WebsiteCardProps {
description?: string
className?: string
onClick?: () => void
+ onDelete?: () => void
showExternalLink?: boolean
}
@@ -23,6 +36,7 @@ export const WebsiteCard = ({
description,
className,
onClick,
+ onDelete,
showExternalLink = true,
}: WebsiteCardProps) => {
const [imageError, setImageError] = useState(false)
@@ -51,7 +65,7 @@ export const WebsiteCard = ({
return (
<Card
className={cn(
- "cursor-pointer transition-all hover:shadow-md group overflow-hidden py-0",
+ "cursor-pointer transition-all hover:shadow-md group overflow-hidden py-0 relative",
className,
)}
onClick={handleCardClick}
@@ -59,6 +73,54 @@ export const WebsiteCard = ({
backgroundColor: getPastelBackgroundColor(url || title || "website"),
}}
>
+ {onDelete && (
+ <AlertDialog>
+ <AlertDialogTrigger asChild>
+ <button
+ className="absolute top-2 right-2 z-20 opacity-0 group-hover:opacity-100 transition-opacity p-1.5 rounded-md hover:bg-red-500/20"
+ onClick={(e) => {
+ e.stopPropagation()
+ }}
+ style={{
+ color: colors.text.muted,
+ backgroundColor: "rgba(255, 255, 255, 0.1)",
+ backdropFilter: "blur(4px)",
+ }}
+ type="button"
+ >
+ <Trash2 className="w-3.5 h-3.5" />
+ </button>
+ </AlertDialogTrigger>
+ <AlertDialogContent>
+ <AlertDialogHeader>
+ <AlertDialogTitle>Delete Document</AlertDialogTitle>
+ <AlertDialogDescription>
+ Are you sure you want to delete this document and all its
+ related memories? This action cannot be undone.
+ </AlertDialogDescription>
+ </AlertDialogHeader>
+ <AlertDialogFooter>
+ <AlertDialogCancel
+ onClick={(e) => {
+ e.stopPropagation()
+ }}
+ >
+ Cancel
+ </AlertDialogCancel>
+ <AlertDialogAction
+ className="bg-red-600 hover:bg-red-700 text-white"
+ onClick={(e) => {
+ e.stopPropagation()
+ onDelete()
+ }}
+ >
+ Delete
+ </AlertDialogAction>
+ </AlertDialogFooter>
+ </AlertDialogContent>
+ </AlertDialog>
+ )}
+
<CardContent className="p-0">
{image && !imageError && (
<div className="relative h-38 bg-gray-100 overflow-hidden">
@@ -75,16 +137,18 @@ export const WebsiteCard = ({
<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 className="flex items-center gap-1">
+ {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>
</div>
{description && (
diff --git a/apps/web/components/header.tsx b/apps/web/components/header.tsx
index b51b4b84..c055e94d 100644
--- a/apps/web/components/header.tsx
+++ b/apps/web/components/header.tsx
@@ -1,7 +1,17 @@
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 {
+ MoonIcon,
+ Plus,
+ SunIcon,
+ MonitorIcon,
+ Network,
+ User,
+ CreditCard,
+ Chrome,
+ LogOut,
+} from "lucide-react"
import {
DropdownMenuContent,
DropdownMenuTrigger,
@@ -93,14 +103,28 @@ export function Header({ onAddMemory }: { onAddMemory?: () => void }) {
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => router.push("/settings")}>
+ <User className="h-4 w-4 mr-2" />
Profile
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => router.push("/settings/billing")}
>
+ <CreditCard className="h-4 w-4 mr-2" />
Billing
</DropdownMenuItem>
<DropdownMenuItem
+ onClick={() => {
+ window.open(
+ "https://chromewebstore.google.com/detail/supermemory/afpgkkipfdpeaflnpoaffkcankadgjfc",
+ "_blank",
+ "noopener,noreferrer",
+ )
+ }}
+ >
+ <Chrome className="h-4 w-4 mr-2" />
+ Chrome Extension
+ </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()}
>
@@ -164,6 +188,7 @@ export function Header({ onAddMemory }: { onAddMemory?: () => void }) {
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => handleSignOut()}>
+ <LogOut className="h-4 w-4 mr-2" />
Logout
</DropdownMenuItem>
</DropdownMenuContent>
diff --git a/apps/web/components/masonry-memory-list.tsx b/apps/web/components/masonry-memory-list.tsx
index 2f634f74..93326e49 100644
--- a/apps/web/components/masonry-memory-list.tsx
+++ b/apps/web/components/masonry-memory-list.tsx
@@ -63,6 +63,7 @@ const DocumentCard = memo(
description={document.content}
activeMemories={activeMemories}
lastModified={document.updatedAt || document.createdAt}
+ onDelete={() => onDelete(document)}
/>
)
}
@@ -77,6 +78,7 @@ const DocumentCard = memo(
document.metadata?.sm_internal_twitter_metadata as unknown as Tweet
}
activeMemories={activeMemories}
+ onDelete={() => onDelete(document)}
/>
)
}
@@ -87,6 +89,7 @@ const DocumentCard = memo(
url={document.url}
title={document.title || "Untitled Document"}
image={document.ogImage}
+ onDelete={() => onDelete(document)}
/>
)
}
@@ -212,9 +215,7 @@ export const MasonryMemoryList = ({
) : 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="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>
@@ -232,6 +233,7 @@ export const MasonryMemoryList = ({
data-theme="light"
>
<Masonry
+ key={`masonry-${filteredDocuments.length}-${filteredDocuments.map((d) => d.id).join(",")}`}
items={filteredDocuments}
render={renderDocumentCard}
columnGutter={16}
diff --git a/apps/web/components/views/add-memory/action-buttons.tsx b/apps/web/components/views/add-memory/action-buttons.tsx
index fc901ba9..3f93fe17 100644
--- a/apps/web/components/views/add-memory/action-buttons.tsx
+++ b/apps/web/components/views/add-memory/action-buttons.tsx
@@ -1,67 +1,67 @@
-import { Button } from '@repo/ui/components/button';
-import { Loader2, type LucideIcon } from 'lucide-react';
-import { motion } from 'motion/react';
+import { Button } from "@repo/ui/components/button"
+import { Loader2, type LucideIcon } from "lucide-react"
+import { motion } from "motion/react"
interface ActionButtonsProps {
- onCancel: () => void;
- onSubmit?: () => void;
- submitText: string;
- submitIcon?: LucideIcon;
- isSubmitting?: boolean;
- isSubmitDisabled?: boolean;
- submitType?: 'button' | 'submit';
- className?: string;
+ onCancel: () => void
+ onSubmit?: () => void
+ submitText: string
+ submitIcon?: LucideIcon
+ isSubmitting?: boolean
+ isSubmitDisabled?: boolean
+ submitType?: "button" | "submit"
+ className?: string
}
export function ActionButtons({
- onCancel,
- onSubmit,
- submitText,
- submitIcon: SubmitIcon,
- isSubmitting = false,
- isSubmitDisabled = false,
- submitType = 'submit',
- className = '',
+ onCancel,
+ onSubmit,
+ submitText,
+ submitIcon: SubmitIcon,
+ isSubmitting = false,
+ isSubmitDisabled = false,
+ submitType = "submit",
+ className = "",
}: ActionButtonsProps) {
- return (
- <div className={`flex gap-3 order-1 sm:order-2 justify-end ${className}`}>
- <Button
- className="hover:bg-foreground/10 border-none flex-1 sm:flex-initial"
- onClick={onCancel}
- type="button"
- variant="ghost"
- >
- Cancel
- </Button>
+ return (
+ <div className={`flex gap-3 order-1 sm:order-2 justify-end ${className}`}>
+ <Button
+ className="hover:bg-foreground/10 border-none flex-1 sm:flex-initial"
+ onClick={onCancel}
+ type="button"
+ variant="ghost"
+ >
+ Cancel
+ </Button>
- <motion.div
- whileHover={{ scale: 1.05 }}
- whileTap={{ scale: 0.95 }}
- className="flex-1 sm:flex-initial"
- >
- <Button
- className="bg-foreground hover:bg-foreground/20 border-foreground/20 w-full"
- disabled={isSubmitting || isSubmitDisabled}
- onClick={submitType === 'button' ? onSubmit : undefined}
- type={submitType}
- >
- {isSubmitting ? (
- <>
- <Loader2 className="h-4 w-4 animate-spin mr-2" />
- {submitText.includes('Add')
- ? 'Adding...'
- : submitText.includes('Upload')
- ? 'Uploading...'
- : 'Processing...'}
- </>
- ) : (
- <>
- {SubmitIcon && <SubmitIcon className="h-4 w-4 mr-2" />}
- {submitText}
- </>
- )}
- </Button>
- </motion.div>
- </div>
- );
+ <motion.div
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ className="flex-1 sm:flex-initial"
+ >
+ <Button
+ className="w-full"
+ disabled={isSubmitting || isSubmitDisabled}
+ onClick={submitType === "button" ? onSubmit : undefined}
+ type={submitType}
+ >
+ {isSubmitting ? (
+ <>
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
+ {submitText.includes("Add")
+ ? "Adding..."
+ : submitText.includes("Upload")
+ ? "Uploading..."
+ : "Processing..."}
+ </>
+ ) : (
+ <>
+ {SubmitIcon && <SubmitIcon className="h-4 w-4 mr-2" />}
+ {submitText}
+ </>
+ )}
+ </Button>
+ </motion.div>
+ </div>
+ )
}
diff --git a/apps/web/components/views/add-memory/index.tsx b/apps/web/components/views/add-memory/index.tsx
index a78e7629..d9c6aef8 100644
--- a/apps/web/components/views/add-memory/index.tsx
+++ b/apps/web/components/views/add-memory/index.tsx
@@ -88,7 +88,10 @@ export function AddMemoryView({
const [newProjectName, setNewProjectName] = useState("")
// Check memory limits
- const { data: memoriesCheck } = fetchMemoriesFeature(autumn, !autumn.isLoading)
+ const { data: memoriesCheck } = fetchMemoriesFeature(
+ autumn,
+ !autumn.isLoading,
+ )
const memoriesUsed = memoriesCheck?.usage ?? 0
const memoriesLimit = memoriesCheck?.included_usage ?? 0
@@ -757,7 +760,7 @@ export function AddMemoryView({
{({ state, handleChange, handleBlur }) => (
<>
<Input
- className={`bg-black/5 border-black/10 text-black ${
+ className={`bg-black/5 border-black/10 ${
addContentMutation.isPending ? "opacity-50" : ""
}`}
disabled={addContentMutation.isPending}
diff --git a/apps/web/components/views/billing.tsx b/apps/web/components/views/billing.tsx
index 679b648e..25e28374 100644
--- a/apps/web/components/views/billing.tsx
+++ b/apps/web/components/views/billing.tsx
@@ -118,17 +118,9 @@ export function BillingView() {
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Memories</span>
<span className="text-sm text-foreground">
- {memoriesUsed} / {memoriesLimit}
+ Unlimited
</span>
</div>
- <div className="w-full bg-muted-foreground/50 rounded-full h-2">
- <div
- className="bg-green-500 h-2 rounded-full transition-all"
- style={{
- width: `${Math.min((memoriesUsed / memoriesLimit) * 100, 100)}%`,
- }}
- />
- </div>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
diff --git a/apps/web/components/views/integrations.tsx b/apps/web/components/views/integrations.tsx
index dadd7dc6..5a6d09d0 100644
--- a/apps/web/components/views/integrations.tsx
+++ b/apps/web/components/views/integrations.tsx
@@ -4,6 +4,7 @@ import { useAuth } from "@lib/auth-context"
import { generateId } from "@lib/generate-id"
import {
ADD_MEMORY_SHORTCUT_URL,
+ RAYCAST_EXTENSION_URL,
SEARCH_MEMORY_SHORTCUT_URL,
} from "@repo/lib/constants"
import { fetchConnectionsFeature } from "@repo/lib/queries"
@@ -20,9 +21,17 @@ import type { ConnectionResponseSchema } from "@repo/validation/api"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons"
import { useCustomer } from "autumn-js/react"
-import { Check, Copy, Smartphone, Trash2 } from "lucide-react"
+import {
+ Check,
+ Copy,
+ DownloadIcon,
+ KeyIcon,
+ Smartphone,
+ Trash2,
+} from "lucide-react"
import { motion } from "motion/react"
import Image from "next/image"
+import { useSearchParams } from "next/navigation"
import { useEffect, useId, useState } from "react"
import { toast } from "sonner"
import type { z } from "zod"
@@ -87,6 +96,7 @@ export function IntegrationsView() {
const queryClient = useQueryClient()
const { selectedProject } = useProject()
const autumn = useCustomer()
+ const searchParams = useSearchParams()
const [showApiKeyModal, setShowApiKeyModal] = useState(false)
const [apiKey, setApiKey] = useState<string>("")
const [copied, setCopied] = useState(false)
@@ -94,7 +104,12 @@ export function IntegrationsView() {
const [selectedShortcutType, setSelectedShortcutType] = useState<
"add" | "search" | null
>(null)
+ const [showRaycastApiKeyModal, setShowRaycastApiKeyModal] = useState(false)
+ const [raycastApiKey, setRaycastApiKey] = useState<string>("")
+ const [raycastCopied, setRaycastCopied] = useState(false)
+ const [hasTriggeredRaycast, setHasTriggeredRaycast] = useState(false)
const apiKeyId = useId()
+ const raycastApiKeyId = useId()
const handleUpgrade = async () => {
try {
@@ -233,7 +248,7 @@ export function IntegrationsView() {
setApiKey(apiKey)
setShowApiKeyModal(true)
setCopied(false)
- handleCopyApiKey()
+ handleCopyApiKey(apiKey)
},
onError: (error) => {
toast.error("Failed to create API key", {
@@ -242,12 +257,54 @@ export function IntegrationsView() {
},
})
+ const createRaycastApiKeyMutation = useMutation({
+ mutationFn: async () => {
+ if (!org?.id) {
+ throw new Error("Organization ID is required")
+ }
+
+ const res = await authClient.apiKey.create({
+ metadata: {
+ organizationId: org?.id,
+ type: "raycast-extension",
+ },
+ name: `raycast-${generateId().slice(0, 8)}`,
+ prefix: `sm_${org?.id}_`,
+ })
+ return res.key
+ },
+ onSuccess: (apiKey) => {
+ setRaycastApiKey(apiKey)
+ setShowRaycastApiKeyModal(true)
+ setRaycastCopied(false)
+ handleCopyApiKey(apiKey)
+ },
+ onError: (error) => {
+ toast.error("Failed to create Raycast API key", {
+ description: error instanceof Error ? error.message : "Unknown error",
+ })
+ },
+ })
+
+ useEffect(() => {
+ const qParam = searchParams.get("q")
+ if (
+ qParam === "raycast" &&
+ !hasTriggeredRaycast &&
+ !createRaycastApiKeyMutation.isPending &&
+ org?.id
+ ) {
+ setHasTriggeredRaycast(true)
+ createRaycastApiKeyMutation.mutate()
+ }
+ }, [searchParams, hasTriggeredRaycast, createRaycastApiKeyMutation, org])
+
const handleShortcutClick = (shortcutType: "add" | "search") => {
setSelectedShortcutType(shortcutType)
createApiKeyMutation.mutate()
}
- const handleCopyApiKey = async () => {
+ const handleCopyApiKey = async (apiKey: string) => {
try {
await navigator.clipboard.writeText(apiKey)
setCopied(true)
@@ -281,6 +338,18 @@ export function IntegrationsView() {
}
}
+ const handleRaycastDialogClose = (open: boolean) => {
+ setShowRaycastApiKeyModal(open)
+ if (!open) {
+ setRaycastApiKey("")
+ setRaycastCopied(false)
+ }
+ }
+
+ const handleRaycastClick = () => {
+ createRaycastApiKeyMutation.mutate()
+ }
+
return (
<div className="space-y-4 sm:space-y-4 custom-scrollbar">
{/* iOS Shortcuts */}
@@ -336,6 +405,64 @@ export function IntegrationsView() {
</div>
</div>
+ {/* Raycast Extension */}
+ <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-purple-500/10 rounded-lg flex-shrink-0">
+ <svg
+ width="24"
+ height="24"
+ viewBox="0 0 28 28"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <title>Raycast Icon</title>
+ <path
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ d="M7 18.079V21L0 14L1.46 12.54L7 18.081V18.079ZM9.921 21H7L14 28L15.46 26.54L9.921 21ZM26.535 15.462L27.996 14L13.996 0L12.538 1.466L18.077 7.004H14.73L10.864 3.146L9.404 4.606L11.809 7.01H10.129V17.876H20.994V16.196L23.399 18.6L24.859 17.14L20.994 13.274V9.927L26.535 15.462ZM7.73 6.276L6.265 7.738L7.833 9.304L9.294 7.844L7.73 6.276ZM20.162 18.708L18.702 20.17L20.268 21.738L21.73 20.276L20.162 18.708ZM4.596 9.41L3.134 10.872L7 14.738V11.815L4.596 9.41ZM16.192 21.006H13.268L17.134 24.872L18.596 23.41L16.192 21.006Z"
+ fill="#FF6363"
+ />
+ </svg>
+ </div>
+ <div className="flex-1 min-w-0">
+ <h3 className="text-card-foreground font-semibold text-base mb-1">
+ Raycast Extension
+ </h3>
+ <p className="text-muted-foreground text-sm leading-relaxed">
+ Add and search memories directly from Raycast on Mac and
+ Windows.
+ </p>
+ </div>
+ </div>
+ <div className="flex flex-col sm:flex-row gap-2 sm:gap-3">
+ <Button
+ variant="secondary"
+ className="flex-1"
+ onClick={handleRaycastClick}
+ disabled={createRaycastApiKeyMutation.isPending}
+ >
+ <KeyIcon className="h-4 w-4" />
+ {createRaycastApiKeyMutation.isPending
+ ? "Generating..."
+ : "Get API Key"}
+ </Button>
+ <Button
+ variant="secondary"
+ className="flex-1"
+ onClick={() => {
+ window.open(RAYCAST_EXTENSION_URL, "_blank")
+ analytics.extensionInstallClicked()
+ }}
+ >
+ <DownloadIcon className="h-4 w-4" />
+ Install Extension
+ </Button>
+ </div>
+ </div>
+ </div>
+
{/* Chrome Extension */}
<div className="bg-card rounded-xl border border-border overflow-hidden shadow-sm">
<div className="p-4 sm:p-5">
@@ -630,8 +757,8 @@ export function IntegrationsView() {
<Button
size="sm"
variant="ghost"
- onClick={handleCopyApiKey}
- className="hover:bg-accent"
+ onClick={() => handleCopyApiKey(apiKey)}
+ className="text-white/70 hover:text-white hover:bg-white/10"
>
{copied ? (
<Check className="h-4 w-4 text-chart-2" />
@@ -696,6 +823,115 @@ export function IntegrationsView() {
</DialogContent>
</DialogPortal>
</Dialog>
+
+ <Dialog
+ open={showRaycastApiKeyModal}
+ onOpenChange={handleRaycastDialogClose}
+ >
+ <DialogPortal>
+ <DialogContent className="bg-card border-border text-card-foreground md:max-w-md z-[100]">
+ <DialogHeader>
+ <DialogTitle className="text-card-foreground text-lg font-semibold">
+ Setup Raycast Extension
+ </DialogTitle>
+ </DialogHeader>
+
+ <div className="space-y-4">
+ {/* API Key Section */}
+ <div className="space-y-2">
+ <label
+ htmlFor={raycastApiKeyId}
+ className="text-sm font-medium text-muted-foreground"
+ >
+ Your Raycast API Key
+ </label>
+ <div className="flex items-center gap-2">
+ <input
+ id={raycastApiKeyId}
+ type="text"
+ value={raycastApiKey}
+ readOnly
+ 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(raycastApiKey)}
+ className="text-muted-foreground hover:text-foreground hover:bg-accent"
+ >
+ {raycastCopied ? (
+ <Check className="h-4 w-4 text-chart-2" />
+ ) : (
+ <Copy className="h-4 w-4" />
+ )}
+ </Button>
+ </div>
+ </div>
+
+ {/* Steps */}
+ <div className="space-y-3">
+ <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-purple-500/20 text-purple-500 rounded-full flex items-center justify-center text-xs font-medium">
+ 1
+ </div>
+ <p className="text-sm text-muted-foreground">
+ Install the Raycast extension from the Raycast Store
+ </p>
+ </div>
+ <div className="flex items-start gap-3">
+ <div className="flex-shrink-0 w-6 h-6 bg-purple-500/20 text-purple-500 rounded-full flex items-center justify-center text-xs font-medium">
+ 2
+ </div>
+ <p className="text-sm text-muted-foreground">
+ Open Raycast preferences and paste your API key
+ </p>
+ </div>
+ <div className="flex items-start gap-3">
+ <div className="flex-shrink-0 w-6 h-6 bg-purple-500/20 text-purple-500 rounded-full flex items-center justify-center text-xs font-medium">
+ 3
+ </div>
+ <p className="text-sm text-muted-foreground">
+ Use "Add Memory" or "Search Memories" commands!
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <div className="flex gap-2 pt-2">
+ <Button
+ onClick={() => {
+ window.open(RAYCAST_EXTENSION_URL, "_blank")
+ analytics.extensionInstallClicked()
+ }}
+ className="flex-1"
+ variant="default"
+ >
+ <svg
+ width="24"
+ height="24"
+ viewBox="0 0 28 28"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <title>Raycast Icon</title>
+ <path
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ d="M7 18.079V21L0 14L1.46 12.54L7 18.081V18.079ZM9.921 21H7L14 28L15.46 26.54L9.921 21ZM26.535 15.462L27.996 14L13.996 0L12.538 1.466L18.077 7.004H14.73L10.864 3.146L9.404 4.606L11.809 7.01H10.129V17.876H20.994V16.196L23.399 18.6L24.859 17.14L20.994 13.274V9.927L26.535 15.462ZM7.73 6.276L6.265 7.738L7.833 9.304L9.294 7.844L7.73 6.276ZM20.162 18.708L18.702 20.17L20.268 21.738L21.73 20.276L20.162 18.708ZM4.596 9.41L3.134 10.872L7 14.738V11.815L4.596 9.41ZM16.192 21.006H13.268L17.134 24.872L18.596 23.41L16.192 21.006Z"
+ fill="#FF6363"
+ />
+ </svg>
+ Install Extension
+ </Button>
+ </div>
+ </div>
+ </DialogContent>
+ </DialogPortal>
+ </Dialog>
</div>
)
}
diff --git a/apps/web/components/views/profile.tsx b/apps/web/components/views/profile.tsx
index 93330da4..5aa21231 100644
--- a/apps/web/components/views/profile.tsx
+++ b/apps/web/components/views/profile.tsx
@@ -37,7 +37,10 @@ export function ProfileView() {
const memoriesUsed = memoriesCheck?.usage ?? 0
const memoriesLimit = memoriesCheck?.included_usage ?? 0
- const { data: connectionsCheck } = fetchConnectionsFeature(autumn, !isCheckingStatus && !autumn.isLoading)
+ const { data: connectionsCheck } = fetchConnectionsFeature(
+ autumn,
+ !isCheckingStatus && !autumn.isLoading,
+ )
const connectionsUsed = connectionsCheck?.usage ?? 0
const handleUpgrade = async () => {
@@ -190,26 +193,32 @@ export function ProfileView() {
<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)}%`,
- }}
- />
+ {isPro ? (
+ <span className="text-sm text-foreground">Unlimited</span>
+ ) : (
+ <span
+ className={`text-sm ${memoriesUsed >= memoriesLimit ? "text-red-500" : "text-foreground"}`}
+ >
+ {memoriesUsed} / {memoriesLimit}
+ </span>
+ )}
</div>
+ {!isPro && (
+ <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>
{isPro && (
@@ -322,4 +331,4 @@ export function ProfileView() {
)}
</div>
)
-} \ No newline at end of file
+}