aboutsummaryrefslogtreecommitdiff
path: root/apps
diff options
context:
space:
mode:
Diffstat (limited to 'apps')
-rw-r--r--apps/web/app/(auth)/login/new/page.tsx2
-rw-r--r--apps/web/app/api/og/route.ts156
-rw-r--r--apps/web/app/new/page.tsx2
-rw-r--r--apps/web/app/onboarding/extension-form.tsx2
-rw-r--r--apps/web/app/onboarding/mcp-form.tsx2
-rw-r--r--apps/web/components/chrome-extension-button.tsx2
-rw-r--r--apps/web/components/connect-ai-modal.tsx2
-rw-r--r--apps/web/components/new/add-document/connections.tsx43
-rw-r--r--apps/web/components/new/add-document/index.tsx39
-rw-r--r--apps/web/components/new/add-document/link.tsx150
-rw-r--r--apps/web/components/new/add-document/note.tsx9
-rw-r--r--apps/web/components/new/header.tsx78
-rw-r--r--apps/web/components/new/settings/account.tsx25
-rw-r--r--apps/web/components/views/add-memory/action-buttons.tsx2
-rw-r--r--apps/web/hooks/use-document-mutations.ts (renamed from apps/web/components/new/add-document/useDocumentMutations.ts)18
-rw-r--r--apps/web/hooks/use-memories-usage.ts35
-rw-r--r--apps/web/package.json6
17 files changed, 478 insertions, 95 deletions
diff --git a/apps/web/app/(auth)/login/new/page.tsx b/apps/web/app/(auth)/login/new/page.tsx
index 3589d6a1..ddea896e 100644
--- a/apps/web/app/(auth)/login/new/page.tsx
+++ b/apps/web/app/(auth)/login/new/page.tsx
@@ -13,7 +13,7 @@ import { Title1Bold } from "@ui/text/title/title-1-bold"
import { InitialHeader } from "@/components/initial-header"
import { useRouter, useSearchParams } from "next/navigation"
import { useState, useEffect } from "react"
-import { motion } from "framer-motion"
+import { motion } from "motion/react"
import { dmSansClassName } from "@/utils/fonts"
import { cn } from "@lib/utils"
import { Logo } from "@ui/assets/Logo"
diff --git a/apps/web/app/api/og/route.ts b/apps/web/app/api/og/route.ts
new file mode 100644
index 00000000..5ca6e44c
--- /dev/null
+++ b/apps/web/app/api/og/route.ts
@@ -0,0 +1,156 @@
+import ogs from "open-graph-scraper"
+
+export const runtime = "nodejs"
+
+interface OGResponse {
+ title: string
+ description: string
+ image?: string
+}
+
+function isValidUrl(urlString: string): boolean {
+ try {
+ const url = new URL(urlString)
+ return url.protocol === "http:" || url.protocol === "https:"
+ } catch {
+ return false
+ }
+}
+
+function isPrivateHost(hostname: string): boolean {
+ const lowerHost = hostname.toLowerCase()
+
+ // Block localhost variants
+ if (
+ lowerHost === "localhost" ||
+ lowerHost === "127.0.0.1" ||
+ lowerHost === "::1" ||
+ lowerHost.startsWith("127.") ||
+ lowerHost.startsWith("0.0.0.0")
+ ) {
+ return true
+ }
+
+ // Block RFC 1918 private IP ranges
+ const privateIpPatterns = [
+ /^10\./,
+ /^172\.(1[6-9]|2[0-9]|3[01])\./,
+ /^192\.168\./,
+ ]
+
+ return privateIpPatterns.some((pattern) => pattern.test(hostname))
+}
+
+function extractImageUrl(image: unknown): string | undefined {
+ if (!image) return undefined
+
+ if (typeof image === "string") {
+ return image
+ }
+
+ if (Array.isArray(image) && image.length > 0) {
+ const first = image[0]
+ if (first && typeof first === "object" && "url" in first) {
+ return String(first.url)
+ }
+ }
+
+ if (typeof image === "object" && image !== null && "url" in image) {
+ return String(image.url)
+ }
+
+ return undefined
+}
+
+function resolveImageUrl(
+ imageUrl: string | undefined,
+ baseUrl: string,
+): string | undefined {
+ if (!imageUrl) return undefined
+
+ try {
+ const url = new URL(imageUrl)
+ return url.href
+ } catch {
+ try {
+ const base = new URL(baseUrl)
+ return new URL(imageUrl, base.href).href
+ } catch {
+ return undefined
+ }
+ }
+}
+
+export async function GET(request: Request) {
+ try {
+ const { searchParams } = new URL(request.url)
+ const url = searchParams.get("url")
+
+ if (!url || !url.trim()) {
+ return Response.json(
+ { error: "Missing or invalid url parameter" },
+ { status: 400 },
+ )
+ }
+
+ const trimmedUrl = url.trim()
+
+ if (!isValidUrl(trimmedUrl)) {
+ return Response.json(
+ { error: "Invalid URL. Must be http:// or https://" },
+ { status: 400 },
+ )
+ }
+
+ const urlObj = new URL(trimmedUrl)
+ if (isPrivateHost(urlObj.hostname)) {
+ return Response.json(
+ { error: "Private/localhost URLs are not allowed" },
+ { status: 400 },
+ )
+ }
+
+ const { result, error } = await ogs({
+ url: trimmedUrl,
+ timeout: 8000,
+ fetchOptions: {
+ headers: {
+ "User-Agent":
+ "Mozilla/5.0 (compatible; SuperMemory/1.0; +https://supermemory.ai)",
+ },
+ },
+ })
+
+ if (error || !result) {
+ console.error("OG scraping error:", error)
+ return Response.json(
+ { error: "Failed to fetch Open Graph data" },
+ { status: 500 },
+ )
+ }
+
+ const ogTitle = result.ogTitle || result.twitterTitle || ""
+ const ogDescription =
+ result.ogDescription || result.twitterDescription || ""
+
+ const ogImageUrl =
+ extractImageUrl(result.ogImage) || extractImageUrl(result.twitterImage)
+
+ const resolvedImageUrl = resolveImageUrl(ogImageUrl, trimmedUrl)
+
+ const response: OGResponse = {
+ title: ogTitle,
+ description: ogDescription,
+ ...(resolvedImageUrl && { image: resolvedImageUrl }),
+ }
+
+ return Response.json(response, {
+ headers: {
+ "Cache-Control": "public, s-maxage=3600, stale-while-revalidate=86400",
+ },
+ })
+ } catch (error) {
+ console.error("OG route error:", error)
+ return Response.json({ error: "Internal server error" }, { status: 500 })
+ }
+}
diff --git a/apps/web/app/new/page.tsx b/apps/web/app/new/page.tsx
index 6e4f2cd2..3d85a427 100644
--- a/apps/web/app/new/page.tsx
+++ b/apps/web/app/new/page.tsx
@@ -9,7 +9,7 @@ import { AddDocumentModal } from "@/components/new/add-document"
import { MCPModal } from "@/components/new/mcp-modal"
import { HotkeysProvider } from "react-hotkeys-hook"
import { useHotkeys } from "react-hotkeys-hook"
-import { AnimatePresence } from "framer-motion"
+import { AnimatePresence } from "motion/react"
export default function NewPage() {
const [isAddDocumentOpen, setIsAddDocumentOpen] = useState(false)
diff --git a/apps/web/app/onboarding/extension-form.tsx b/apps/web/app/onboarding/extension-form.tsx
index 75f84557..f5f551cf 100644
--- a/apps/web/app/onboarding/extension-form.tsx
+++ b/apps/web/app/onboarding/extension-form.tsx
@@ -12,7 +12,7 @@ import {
} from "lucide-react"
import { NavMenu } from "./nav-menu"
import { useOnboarding } from "./onboarding-context"
-import { motion, AnimatePresence, type ResolvedValues } from "framer-motion"
+import { motion, AnimatePresence, type ResolvedValues } from "motion/react"
import { useEffect, useMemo, useRef, useState, useLayoutEffect } from "react"
import React from "react"
import { cn } from "@lib/utils"
diff --git a/apps/web/app/onboarding/mcp-form.tsx b/apps/web/app/onboarding/mcp-form.tsx
index 84ba0838..ee0652f5 100644
--- a/apps/web/app/onboarding/mcp-form.tsx
+++ b/apps/web/app/onboarding/mcp-form.tsx
@@ -14,7 +14,7 @@ import { CheckIcon, CircleCheckIcon, CopyIcon, LoaderIcon } from "lucide-react"
import { TextMorph } from "@/components/text-morph"
import { NavMenu } from "./nav-menu"
import { cn } from "@lib/utils"
-import { motion, AnimatePresence } from "framer-motion"
+import { motion, AnimatePresence } from "motion/react"
import { useQuery } from "@tanstack/react-query"
import { $fetch } from "@lib/api"
diff --git a/apps/web/components/chrome-extension-button.tsx b/apps/web/components/chrome-extension-button.tsx
index 3f7510e3..30a8b982 100644
--- a/apps/web/components/chrome-extension-button.tsx
+++ b/apps/web/components/chrome-extension-button.tsx
@@ -11,7 +11,7 @@ import {
TwitterIcon,
} from "lucide-react"
import { useEffect, useState } from "react"
-import { motion } from "framer-motion"
+import { motion } from "motion/react"
import Image from "next/image"
import { analytics } from "@/lib/analytics"
import { useIsMobile } from "@hooks/use-mobile"
diff --git a/apps/web/components/connect-ai-modal.tsx b/apps/web/components/connect-ai-modal.tsx
index 942af105..d9353e6d 100644
--- a/apps/web/components/connect-ai-modal.tsx
+++ b/apps/web/components/connect-ai-modal.tsx
@@ -32,7 +32,7 @@ import { z } from "zod/v4"
import { analytics } from "@/lib/analytics"
import { cn } from "@lib/utils"
import type { Project } from "@repo/lib/types"
-import { motion, AnimatePresence } from "framer-motion"
+import { motion, AnimatePresence } from "motion/react"
const clients = {
cursor: "Cursor",
diff --git a/apps/web/components/new/add-document/connections.tsx b/apps/web/components/new/add-document/connections.tsx
index 2ba6513f..5a89cf70 100644
--- a/apps/web/components/new/add-document/connections.tsx
+++ b/apps/web/components/new/add-document/connections.tsx
@@ -53,6 +53,7 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
const [isProUser, setIsProUser] = useState(false)
const [connectingProvider, setConnectingProvider] =
useState<ConnectorProvider | null>(null)
+ const [isUpgrading, setIsUpgrading] = useState(false)
// Check Pro status
useEffect(() => {
@@ -65,6 +66,20 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
}
}, [autumn.isLoading, autumn.customer])
+ const handleUpgrade = async () => {
+ setIsUpgrading(true)
+ try {
+ await autumn.attach({
+ productId: "consumer_pro",
+ successUrl: window.location.href,
+ })
+ } catch (error) {
+ console.error("Upgrade error:", error)
+ toast.error("Failed to start upgrade process")
+ setIsUpgrading(false)
+ }
+ }
+
// Check connections feature limits
const { data: connectionsCheck } = fetchConnectionsFeature(
autumn,
@@ -359,15 +374,25 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
{!isProUser ? (
<>
<p className="text-[14px] text-[#737373] mb-4 text-center">
- <a
- href="/pricing"
- className="underline text-[#737373] hover:text-white"
- >
- Upgrade to Pro
- </a>{" "}
- to get
- <br />
- Supermemory Connections
+ {isUpgrading || autumn.isLoading ? (
+ <span className="inline-flex items-center gap-2">
+ <Loader className="h-4 w-4 animate-spin" />
+ Upgrading...
+ </span>
+ ) : (
+ <>
+ <button
+ type="button"
+ onClick={handleUpgrade}
+ className="underline text-[#737373] hover:text-white transition-colors cursor-pointer"
+ >
+ Upgrade to Pro
+ </button>{" "}
+ to get
+ <br />
+ Supermemory Connections
+ </>
+ )}
</p>
<div className="space-y-2 text-[14px]">
<div className="flex items-center gap-2">
diff --git a/apps/web/components/new/add-document/index.tsx b/apps/web/components/new/add-document/index.tsx
index fe4e8966..9e117912 100644
--- a/apps/web/components/new/add-document/index.tsx
+++ b/apps/web/components/new/add-document/index.tsx
@@ -30,7 +30,9 @@ import {
DropdownMenuTrigger,
} from "@repo/ui/components/dropdown-menu"
import { toast } from "sonner"
-import { useDocumentMutations } from "./useDocumentMutations"
+import { useDocumentMutations } from "../../../hooks/use-document-mutations"
+import { useCustomer } from "autumn-js/react"
+import { useMemoriesUsage } from "@/hooks/use-memories-usage"
type TabType = "note" | "link" | "file" | "connect"
@@ -132,6 +134,15 @@ export function AddDocument({
onClose,
})
+ const autumn = useCustomer()
+ const {
+ memoriesUsed,
+ memoriesLimit,
+ hasProProduct,
+ isLoading: isLoadingMemories,
+ usagePercent,
+ } = useMemoriesUsage(autumn)
+
useEffect(() => {
setLocalSelectedProject(globalSelectedProject)
}, [globalSelectedProject])
@@ -242,7 +253,7 @@ export function AddDocument({
noteMutation.isPending || linkMutation.isPending || fileMutation.isPending
return (
- <div className="h-full flex flex-row text-white space-x-6">
+ <div className="h-full flex flex-row text-white space-x-5">
<div className="w-1/3 flex flex-col justify-between">
<div className="flex flex-col gap-1">
{tabs.map((tab) => (
@@ -266,7 +277,7 @@ export function AddDocument({
"0 2.842px 14.211px 0 rgba(0, 0, 0, 0.25), 0.711px 0.711px 0.711px 0 rgba(255, 255, 255, 0.10) inset",
}}
>
- <div className="flex justify-between items-center mb-2">
+ <div className="flex justify-between items-center">
<span
className={cn(
"text-white text-[16px] font-medium",
@@ -276,19 +287,25 @@ export function AddDocument({
Memories
</span>
<span className={cn("text-[#737373] text-sm", dmSansClassName())}>
- 120/200
+ {isLoadingMemories
+ ? "…"
+ : hasProProduct
+ ? "Unlimited"
+ : `${memoriesUsed}/${memoriesLimit}`}
</span>
</div>
- <div className="h-1.5 bg-[#0D121A] rounded-full overflow-hidden">
- <div
- className="h-full bg-[#2261CA] rounded-full"
- style={{ width: "60%" }}
- />
- </div>
+ {!hasProProduct && (
+ <div className="h-1.5 bg-[#0D121A] rounded-full overflow-hidden mt-2">
+ <div
+ className="h-full bg-[#2261CA] rounded-full"
+ style={{ width: `${usagePercent}%` }}
+ />
+ </div>
+ )}
</div>
</div>
- <div className="w-2/3 overflow-auto flex flex-col justify-between">
+ <div className="w-2/3 overflow-auto flex flex-col justify-between px-1 scrollbar-thin">
{activeTab === "note" && (
<NoteContent
onSubmit={handleNoteSubmit}
diff --git a/apps/web/components/new/add-document/link.tsx b/apps/web/components/new/add-document/link.tsx
index 0c50d194..544af86d 100644
--- a/apps/web/components/new/add-document/link.tsx
+++ b/apps/web/components/new/add-document/link.tsx
@@ -5,11 +5,14 @@ import { cn } from "@lib/utils"
import { Button } from "@ui/components/button"
import { dmSansClassName } from "@/utils/fonts"
import { useHotkeys } from "react-hotkeys-hook"
+import { Image as ImageIcon, Loader2 } from "lucide-react"
+import { toast } from "sonner"
export interface LinkData {
url: string
title: string
description: string
+ image?: string
}
interface LinkContentProps {
@@ -19,26 +22,50 @@ interface LinkContentProps {
isOpen?: boolean
}
-export function LinkContent({ onSubmit, onDataChange, isSubmitting, isOpen }: LinkContentProps) {
+export function LinkContent({
+ onSubmit,
+ onDataChange,
+ isSubmitting,
+ isOpen,
+}: LinkContentProps) {
const [url, setUrl] = useState("")
const [title, setTitle] = useState("")
const [description, setDescription] = useState("")
+ const [image, setImage] = useState<string | undefined>(undefined)
+ const [isPreviewLoading, setIsPreviewLoading] = useState(false)
const canSubmit = url.trim().length > 0 && !isSubmitting
const handleSubmit = () => {
if (canSubmit && onSubmit) {
- onSubmit({ url, title, description })
+ let normalizedUrl = url.trim()
+ if (
+ !normalizedUrl.startsWith("http://") &&
+ !normalizedUrl.startsWith("https://")
+ ) {
+ normalizedUrl = `https://${normalizedUrl}`
+ }
+ onSubmit({ url: normalizedUrl, title, description })
}
}
- const updateData = (newUrl: string, newTitle: string, newDescription: string) => {
- onDataChange?.({ url: newUrl, title: newTitle, description: newDescription })
+ const updateData = (
+ newUrl: string,
+ newTitle: string,
+ newDescription: string,
+ newImage?: string,
+ ) => {
+ onDataChange?.({
+ url: newUrl,
+ title: newTitle,
+ description: newDescription,
+ ...(newImage && { image: newImage }),
+ })
}
const handleUrlChange = (newUrl: string) => {
setUrl(newUrl)
- updateData(newUrl, title, description)
+ updateData(newUrl, title, description, image)
}
const handleTitleChange = (newTitle: string) => {
@@ -48,7 +75,60 @@ export function LinkContent({ onSubmit, onDataChange, isSubmitting, isOpen }: Li
const handleDescriptionChange = (newDescription: string) => {
setDescription(newDescription)
- updateData(url, title, newDescription)
+ updateData(url, title, newDescription, image)
+ }
+
+ const handlePreviewLink = async () => {
+ if (!url.trim()) {
+ toast.error("Please enter a URL first")
+ return
+ }
+
+ let normalizedUrl = url.trim()
+ if (
+ !normalizedUrl.startsWith("http://") &&
+ !normalizedUrl.startsWith("https://")
+ ) {
+ normalizedUrl = `https://${normalizedUrl}`
+ setUrl(normalizedUrl)
+ updateData(normalizedUrl, title, description, image)
+ }
+
+ setIsPreviewLoading(true)
+ try {
+ const response = await fetch(
+ `/api/og?url=${encodeURIComponent(normalizedUrl)}`,
+ )
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}))
+ throw new Error(errorData.error || "Failed to fetch preview")
+ }
+
+ const data = await response.json()
+
+ const newTitle = data.title || ""
+ const newDescription = data.description || ""
+ const newImage = data.image || undefined
+
+ setTitle(newTitle)
+ setDescription(newDescription)
+ setImage(newImage)
+ updateData(url, newTitle, newDescription, newImage)
+
+ if (!newTitle && !newDescription && !newImage) {
+ toast.info("No Open Graph data found for this URL")
+ } else {
+ toast.success("Preview loaded successfully")
+ }
+ } catch (error) {
+ console.error("Preview error:", error)
+ toast.error(
+ error instanceof Error ? error.message : "Failed to load preview",
+ )
+ } finally {
+ setIsPreviewLoading(false)
+ }
}
useHotkeys("mod+enter", handleSubmit, {
@@ -62,12 +142,13 @@ export function LinkContent({ onSubmit, onDataChange, isSubmitting, isOpen }: Li
setUrl("")
setTitle("")
setDescription("")
+ setImage(undefined)
onDataChange?.({ url: "", title: "", description: "" })
}
}, [isOpen, onDataChange])
return (
- <div className={cn("flex flex-col space-y-4 pt-4", dmSansClassName())}>
+ <div className={cn("flex flex-col space-y-4 pt-4 mb-4", dmSansClassName())}>
<div>
<p
className={cn("text-[16px] font-medium pl-2 pb-2", dmSansClassName())}
@@ -79,14 +160,24 @@ export function LinkContent({ onSubmit, onDataChange, isSubmitting, isOpen }: Li
type="text"
value={url}
onChange={(e) => handleUrlChange(e.target.value)}
- placeholder="https://maheshthedev.me"
+ placeholder="https://example.com"
disabled={isSubmitting}
- className={cn(
- "w-full p-4 rounded-xl bg-[#14161A] shadow-inside-out disabled:opacity-50",
- )}
+ className="w-full p-4 rounded-xl bg-[#14161A] shadow-inside-out disabled:opacity-50 outline-1 outline-transparent focus:outline-[#525D6EB2]"
/>
- <Button variant="linkPreview" className="absolute right-2 top-2" disabled={isSubmitting}>
- Preview Link
+ <Button
+ variant="linkPreview"
+ className="absolute right-2 top-2"
+ disabled={isSubmitting || isPreviewLoading || !url.trim()}
+ onClick={handlePreviewLink}
+ >
+ {isPreviewLoading ? (
+ <>
+ <Loader2 className="size-4 animate-spin mr-2" />
+ Loading...
+ </>
+ ) : (
+ "Preview Link"
+ )}
</Button>
</div>
</div>
@@ -100,8 +191,8 @@ export function LinkContent({ onSubmit, onDataChange, isSubmitting, isOpen }: Li
value={title}
onChange={(e) => handleTitleChange(e.target.value)}
placeholder="Mahesh Sanikommu - Portfolio"
- disabled={isSubmitting}
- className="w-full px-4 py-3 bg-[#0F1217] rounded-xl disabled:opacity-50"
+ disabled
+ className="w-full px-4 py-3 bg-[#0F1217] rounded-xl disabled:opacity-50 outline-1 outline-transparent focus:outline-[#525D6EB2]"
/>
</div>
<div>
@@ -112,17 +203,34 @@ export function LinkContent({ onSubmit, onDataChange, isSubmitting, isOpen }: Li
value={description}
onChange={(e) => handleDescriptionChange(e.target.value)}
placeholder="Portfolio website of Mahesh Sanikommu"
- disabled={isSubmitting}
- className="w-full px-4 py-3 bg-[#0F1217] rounded-xl disabled:opacity-50"
+ disabled
+ className="w-full px-4 py-3 bg-[#0F1217] rounded-xl resize-none disabled:opacity-50 outline-1 outline-transparent focus:outline-[#525D6EB2]"
/>
</div>
<div>
<p className="pl-2 pb-2 font-semibold text-[16px] text-[#737373]">
- Link Preview
+ Link Preview Image
</p>
- <div className="w-full px-4 py-3 bg-[#0F1217] rounded-xl">
- <p>{description || "Portfolio website of Mahesh Sanikommu"}</p>
- </div>
+ {image ? (
+ <div className="w-full max-w-md aspect-4/2 bg-[#0F1217] rounded-xl overflow-hidden">
+ <img
+ src={image}
+ alt={title || "Link preview"}
+ className="w-full h-full object-cover"
+ onError={(e) => {
+ e.currentTarget.style.display = "none"
+ e.currentTarget.parentElement?.classList.add("opacity-50")
+ e.currentTarget.parentElement?.classList.add("flex")
+ e.currentTarget.parentElement?.classList.add("items-center")
+ e.currentTarget.parentElement?.classList.add("justify-center")
+ }}
+ />
+ </div>
+ ) : (
+ <div className="w-full max-w-md aspect-4/2 bg-[#0F1217] opacity-50 rounded-xl flex items-center justify-center">
+ <ImageIcon className="w-8 h-8 text-[#737373]" />
+ </div>
+ )}
</div>
</div>
</div>
diff --git a/apps/web/components/new/add-document/note.tsx b/apps/web/components/new/add-document/note.tsx
index c7c2a2ac..465e295a 100644
--- a/apps/web/components/new/add-document/note.tsx
+++ b/apps/web/components/new/add-document/note.tsx
@@ -10,7 +10,12 @@ interface NoteContentProps {
isOpen?: boolean
}
-export function NoteContent({ onSubmit, onContentChange, isSubmitting, isOpen }: NoteContentProps) {
+export function NoteContent({
+ onSubmit,
+ onContentChange,
+ isSubmitting,
+ isOpen,
+}: NoteContentProps) {
const [content, setContent] = useState("")
const canSubmit = content.trim().length > 0 && !isSubmitting
@@ -45,7 +50,7 @@ export function NoteContent({ onSubmit, onContentChange, isSubmitting, isOpen }:
onChange={(e) => handleContentChange(e.target.value)}
placeholder="Write your note here..."
disabled={isSubmitting}
- className="w-full h-full p-4 mb-4! rounded-[14px] bg-[#14161A] shadow-inside-out resize-none disabled:opacity-50"
+ className="w-full h-full p-4 mb-4! rounded-[14px] bg-[#14161A] shadow-inside-out resize-none disabled:opacity-50 outline-none"
/>
)
}
diff --git a/apps/web/components/new/header.tsx b/apps/web/components/new/header.tsx
index 4ef344a3..a5aa47d3 100644
--- a/apps/web/components/new/header.tsx
+++ b/apps/web/components/new/header.tsx
@@ -12,6 +12,9 @@ import {
FolderIcon,
LogOut,
Settings,
+ Home,
+ Code2,
+ ExternalLink,
} from "lucide-react"
import { Button } from "@ui/components/button"
import { cn } from "@lib/utils"
@@ -31,6 +34,7 @@ import { DEFAULT_PROJECT_ID } from "@repo/lib/constants"
import { useProjectMutations } from "@/hooks/use-project-mutations"
import { useProject } from "@/stores"
import { useRouter } from "next/navigation"
+import Link from "next/link"
import type { Project } from "@repo/lib/types"
interface HeaderProps {
@@ -69,19 +73,62 @@ export function Header({ onAddMemory, onOpenMCP }: HeaderProps) {
return (
<div className="flex p-4 justify-between items-center">
<div className="flex items-center justify-center gap-4 z-10!">
- <div className="flex items-center">
- <Logo className="h-7" />
- {name && (
- <div className="flex flex-col items-start justify-center ml-2">
- <p className="text-[#8B8B8B] text-[11px] leading-tight">
- {userName}
- </p>
- <p className="text-white font-bold text-xl leading-none -mt-1">
- supermemory
- </p>
- </div>
- )}
- </div>
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <button
+ type="button"
+ className="flex items-center rounded-lg px-2 py-1.5 -ml-2 cursor-pointer hover:bg-white/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 transition-colors"
+ >
+ <Logo className="h-7" />
+ {name && (
+ <div className="flex flex-col items-start justify-center ml-2">
+ <p className="text-[#8B8B8B] text-[11px] leading-tight">
+ {userName}
+ </p>
+ <p className="text-white font-bold text-xl leading-none -mt-1">
+ supermemory
+ </p>
+ </div>
+ )}
+ </button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent
+ align="start"
+ className="w-56 bg-[#0D121A] rounded-xl border-none p-1.5 ml-4 shadow-[0_0_20px_rgba(255,255,255,0.15)]"
+ >
+ <DropdownMenuItem
+ asChild
+ className="px-3 py-2 rounded-md hover:bg-[#293952]/40 cursor-pointer"
+ >
+ <Link href="/new">
+ <Home className="h-4 w-4" />
+ Home
+ </Link>
+ </DropdownMenuItem>
+ <DropdownMenuItem
+ asChild
+ className="px-3 py-2 rounded-md hover:bg-[#293952]/40 cursor-pointer"
+ >
+ <a
+ href="https://console.supermemory.ai"
+ target="_blank"
+ rel="noreferrer"
+ >
+ <Code2 className="h-4 w-4" />
+ Developer console
+ </a>
+ </DropdownMenuItem>
+ <DropdownMenuItem
+ asChild
+ className="px-3 py-2 rounded-md hover:bg-[#293952]/40 cursor-pointer"
+ >
+ <a href="https://supermemory.ai" target="_blank" rel="noreferrer">
+ <ExternalLink className="h-4 w-4" />
+ supermemory.ai
+ </a>
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
<div className="self-stretch w-px bg-[#FFFFFF33]" />
<div className="flex items-center gap-2">
<p>📁 {projectName}</p>
@@ -212,7 +259,10 @@ export function Header({ onAddMemory, onOpenMCP }: HeaderProps) {
<Settings className="h-4 w-4" />
Settings
</DropdownMenuItem>
- <DropdownMenuItem onClick={() => authClient.signOut()}>
+ <DropdownMenuItem onClick={() => {
+ authClient.signOut()
+ router.push("/login/new")
+ }}>
<LogOut className="h-4 w-4" />
Logout
</DropdownMenuItem>
diff --git a/apps/web/components/new/settings/account.tsx b/apps/web/components/new/settings/account.tsx
index 7ac0598d..10967947 100644
--- a/apps/web/components/new/settings/account.tsx
+++ b/apps/web/components/new/settings/account.tsx
@@ -3,8 +3,8 @@
import { dmSans125ClassName } from "@/utils/fonts"
import { cn } from "@lib/utils"
import { useAuth } from "@lib/auth-context"
-import { fetchMemoriesFeature, fetchSubscriptionStatus } from "@lib/queries"
import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar"
+import { useMemoriesUsage } from "@/hooks/use-memories-usage"
import {
Dialog,
DialogContent,
@@ -82,26 +82,13 @@ export default function Account() {
const [deleteConfirmText, setDeleteConfirmText] = useState("")
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
- // Billing data
const {
- data: status = {
- consumer_pro: { allowed: false, status: null },
- },
+ memoriesUsed,
+ memoriesLimit,
+ hasProProduct,
isLoading: isCheckingStatus,
- } = fetchSubscriptionStatus(autumn, !autumn.isLoading)
-
- const proStatus = status.consumer_pro
- const hasProProduct = proStatus?.status !== null
-
- const { data: memoriesCheck } = fetchMemoriesFeature(
- autumn,
- !autumn.isLoading && !isCheckingStatus,
- )
- const memoriesUsed = memoriesCheck?.usage ?? 0
- const memoriesLimit = memoriesCheck?.included_usage ?? 200
-
- // Calculate progress percentage
- const usagePercent = Math.min((memoriesUsed / memoriesLimit) * 100, 100)
+ usagePercent,
+ } = useMemoriesUsage(autumn)
// Handlers
const handleUpgrade = async () => {
diff --git a/apps/web/components/views/add-memory/action-buttons.tsx b/apps/web/components/views/add-memory/action-buttons.tsx
index a85042e5..ae60e9fa 100644
--- a/apps/web/components/views/add-memory/action-buttons.tsx
+++ b/apps/web/components/views/add-memory/action-buttons.tsx
@@ -40,7 +40,7 @@ export function ActionButtons({
className="flex-1 sm:flex-initial"
>
<Button
- className="w-full cursor-pointer"
+ className="w-full cursor-pointer text-black dark:text-white"
disabled={isSubmitting || isSubmitDisabled}
onClick={submitType === "button" ? onSubmit : undefined}
type={submitType}
diff --git a/apps/web/components/new/add-document/useDocumentMutations.ts b/apps/web/hooks/use-document-mutations.ts
index fc57bbea..5abd7b56 100644
--- a/apps/web/components/new/add-document/useDocumentMutations.ts
+++ b/apps/web/hooks/use-document-mutations.ts
@@ -72,11 +72,11 @@ export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions) {
queryClient.setQueryData(
["documents-with-memories", project],
(old: DocumentsQueryData | undefined) => {
- if (!old) return { documents: [optimisticMemory], totalCount: 1 }
+ const existingDocs = old?.documents ?? []
return {
...old,
- documents: [optimisticMemory, ...old.documents],
- totalCount: old.totalCount + 1,
+ documents: [optimisticMemory, ...existingDocs],
+ totalCount: (old?.totalCount ?? 0) + 1,
}
},
)
@@ -155,11 +155,11 @@ export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions) {
queryClient.setQueryData(
["documents-with-memories", project],
(old: DocumentsQueryData | undefined) => {
- if (!old) return { documents: [optimisticMemory], totalCount: 1 }
+ const existingDocs = old?.documents ?? []
return {
...old,
- documents: [optimisticMemory, ...old.documents],
- totalCount: old.totalCount + 1,
+ documents: [optimisticMemory, ...existingDocs],
+ totalCount: (old?.totalCount ?? 0) + 1,
}
},
)
@@ -272,11 +272,11 @@ export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions) {
queryClient.setQueryData(
["documents-with-memories", project],
(old: DocumentsQueryData | undefined) => {
- if (!old) return { documents: [optimisticMemory], totalCount: 1 }
+ const existingDocs = old?.documents ?? []
return {
...old,
- documents: [optimisticMemory, ...old.documents],
- totalCount: old.totalCount + 1,
+ documents: [optimisticMemory, ...existingDocs],
+ totalCount: (old?.totalCount ?? 0) + 1,
}
},
)
diff --git a/apps/web/hooks/use-memories-usage.ts b/apps/web/hooks/use-memories-usage.ts
new file mode 100644
index 00000000..fb8a5aea
--- /dev/null
+++ b/apps/web/hooks/use-memories-usage.ts
@@ -0,0 +1,35 @@
+import { fetchMemoriesFeature, fetchSubscriptionStatus } from "@lib/queries"
+import type { useCustomer } from "autumn-js/react"
+
+export function useMemoriesUsage(autumn: ReturnType<typeof useCustomer>) {
+ const {
+ data: status = {
+ consumer_pro: { allowed: false, status: null },
+ },
+ isLoading: isCheckingStatus,
+ } = fetchSubscriptionStatus(autumn, !autumn.isLoading)
+
+ const proStatus = status.consumer_pro
+ const hasProProduct = proStatus?.status !== null
+
+ const { data: memoriesCheck, isLoading: isLoadingMemories } =
+ fetchMemoriesFeature(autumn, !isCheckingStatus && !autumn.isLoading)
+
+ const memoriesUsed = memoriesCheck?.usage ?? 0
+ const memoriesLimit = memoriesCheck?.included_usage ?? 0
+
+ const isLoading = autumn.isLoading || isCheckingStatus || isLoadingMemories
+
+ const usagePercent =
+ memoriesLimit <= 0
+ ? 0
+ : Math.min(Math.max((memoriesUsed / memoriesLimit) * 100, 0), 100)
+
+ return {
+ memoriesUsed,
+ memoriesLimit,
+ hasProProduct,
+ isLoading,
+ usagePercent,
+ }
+}
diff --git a/apps/web/package.json b/apps/web/package.json
index 85e27d4f..eb744d8e 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -41,7 +41,7 @@
"@react-router/fs-routes": "^7.6.2",
"@react-router/node": "^7.6.2",
"@react-router/serve": "^7.6.2",
- "@sentry/nextjs": "^10.22.0",
+ "@sentry/nextjs": "^10.33.0",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/react-form": "^1.12.4",
"@tanstack/react-query": "^5.90.14",
@@ -59,15 +59,15 @@
"embla-carousel-autoplay": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"flubber": "^0.4.2",
- "framer-motion": "^12.23.12",
"idb-keyval": "^6.2.2",
"is-hotkey": "^0.2.0",
"lucide-react": "^0.525.0",
"masonic": "^4.1.0",
- "motion": "^12.19.2",
+ "motion": "^12.26.2",
"next": "16.0.9",
"next-themes": "^0.4.6",
"nuqs": "^2.5.2",
+ "open-graph-scraper": "^6.11.0",
"pdfjs-dist": "5.4.296",
"posthog-js": "^1.257.0",
"random-word-slugs": "^0.1.7",