diff options
Diffstat (limited to 'apps/web')
| -rw-r--r-- | apps/web/app/(auth)/login/new/page.tsx | 2 | ||||
| -rw-r--r-- | apps/web/app/api/og/route.ts | 156 | ||||
| -rw-r--r-- | apps/web/app/new/page.tsx | 2 | ||||
| -rw-r--r-- | apps/web/app/onboarding/extension-form.tsx | 2 | ||||
| -rw-r--r-- | apps/web/app/onboarding/mcp-form.tsx | 2 | ||||
| -rw-r--r-- | apps/web/components/chrome-extension-button.tsx | 2 | ||||
| -rw-r--r-- | apps/web/components/connect-ai-modal.tsx | 2 | ||||
| -rw-r--r-- | apps/web/components/new/add-document/connections.tsx | 43 | ||||
| -rw-r--r-- | apps/web/components/new/add-document/index.tsx | 39 | ||||
| -rw-r--r-- | apps/web/components/new/add-document/link.tsx | 150 | ||||
| -rw-r--r-- | apps/web/components/new/add-document/note.tsx | 9 | ||||
| -rw-r--r-- | apps/web/components/new/header.tsx | 78 | ||||
| -rw-r--r-- | apps/web/components/new/settings/account.tsx | 25 | ||||
| -rw-r--r-- | apps/web/components/views/add-memory/action-buttons.tsx | 2 | ||||
| -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.ts | 35 | ||||
| -rw-r--r-- | apps/web/package.json | 6 |
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", |