From 641db19e35009e22b101f9e673a3af4528de2a30 Mon Sep 17 00:00:00 2001 From: Mahesh Sanikommu Date: Tue, 13 Jan 2026 17:53:28 -0800 Subject: chore: quick bugs squash across the elements and added few more changes (#671) --- apps/web/app/(auth)/login/new/page.tsx | 2 +- apps/web/app/api/og/route.ts | 156 ++++++++++ apps/web/app/new/page.tsx | 2 +- apps/web/app/onboarding/extension-form.tsx | 2 +- apps/web/app/onboarding/mcp-form.tsx | 2 +- apps/web/components/chrome-extension-button.tsx | 2 +- apps/web/components/connect-ai-modal.tsx | 2 +- .../components/new/add-document/connections.tsx | 43 ++- apps/web/components/new/add-document/index.tsx | 39 ++- apps/web/components/new/add-document/link.tsx | 150 ++++++++-- apps/web/components/new/add-document/note.tsx | 9 +- .../new/add-document/useDocumentMutations.ts | 313 --------------------- apps/web/components/new/header.tsx | 78 ++++- apps/web/components/new/settings/account.tsx | 25 +- .../components/views/add-memory/action-buttons.tsx | 2 +- apps/web/hooks/use-document-mutations.ts | 313 +++++++++++++++++++++ apps/web/hooks/use-memories-usage.ts | 35 +++ apps/web/package.json | 6 +- 18 files changed, 782 insertions(+), 399 deletions(-) create mode 100644 apps/web/app/api/og/route.ts delete mode 100644 apps/web/components/new/add-document/useDocumentMutations.ts create mode 100644 apps/web/hooks/use-document-mutations.ts create mode 100644 apps/web/hooks/use-memories-usage.ts (limited to 'apps') 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(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 ? ( <>

- - Upgrade to Pro - {" "} - to get -
- Supermemory Connections + {isUpgrading || autumn.isLoading ? ( + + + Upgrading... + + ) : ( + <> + {" "} + to get +
+ Supermemory Connections + + )}

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 ( -
+
{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", }} > -
+
- 120/200 + {isLoadingMemories + ? "…" + : hasProProduct + ? "Unlimited" + : `${memoriesUsed}/${memoriesLimit}`}
-
-
-
+ {!hasProProduct && ( +
+
+
+ )}
-
+
{activeTab === "note" && ( (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 ( -
+

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]" /> -

@@ -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]" />
@@ -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]" />

- Link Preview + Link Preview Image

-
-

{description || "Portfolio website of Mahesh Sanikommu"}

-
+ {image ? ( +
+ {title { + 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") + }} + /> +
+ ) : ( +
+ +
+ )}
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/add-document/useDocumentMutations.ts b/apps/web/components/new/add-document/useDocumentMutations.ts deleted file mode 100644 index fc57bbea..00000000 --- a/apps/web/components/new/add-document/useDocumentMutations.ts +++ /dev/null @@ -1,313 +0,0 @@ -"use client" - -import { useMutation, useQueryClient } from "@tanstack/react-query" -import { toast } from "sonner" -import { $fetch } from "@lib/api" - -interface DocumentsQueryData { - documents: unknown[] - totalCount: number -} - -interface UseDocumentMutationsOptions { - onClose: () => void -} - -export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions) { - const queryClient = useQueryClient() - - const noteMutation = useMutation({ - mutationFn: async ({ - content, - project, - }: { - content: string - project: string - }) => { - const response = await $fetch("@post/documents", { - body: { - content: content, - containerTags: [project], - metadata: { - sm_source: "consumer", - }, - }, - }) - - if (response.error) { - throw new Error(response.error?.message || "Failed to add note") - } - - return response.data - }, - onMutate: async ({ content, project }) => { - await queryClient.cancelQueries({ - queryKey: ["documents-with-memories", project], - }) - - const previousMemories = queryClient.getQueryData([ - "documents-with-memories", - project, - ]) - - const optimisticMemory = { - id: `temp-${Date.now()}`, - content: content, - url: null, - title: content.substring(0, 100), - description: "Processing content...", - containerTags: [project], - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - status: "queued", - type: "note", - metadata: { - processingStage: "queued", - processingMessage: "Added to processing queue", - }, - memoryEntries: [], - isOptimistic: true, - } - - queryClient.setQueryData( - ["documents-with-memories", project], - (old: DocumentsQueryData | undefined) => { - if (!old) return { documents: [optimisticMemory], totalCount: 1 } - return { - ...old, - documents: [optimisticMemory, ...old.documents], - totalCount: old.totalCount + 1, - } - }, - ) - - return { previousMemories } - }, - onError: (_error, variables, context) => { - if (context?.previousMemories) { - queryClient.setQueryData( - ["documents-with-memories", variables.project], - context.previousMemories, - ) - } - toast.error("Failed to add note", { - description: _error instanceof Error ? _error.message : "Unknown error", - }) - }, - onSuccess: (_data, variables) => { - toast.success("Note added successfully!", { - description: "Your note is being processed", - }) - queryClient.invalidateQueries({ - queryKey: ["documents-with-memories", variables.project], - }) - onClose() - }, - }) - - const linkMutation = useMutation({ - mutationFn: async ({ url, project }: { url: string; project: string }) => { - const response = await $fetch("@post/documents", { - body: { - content: url, - containerTags: [project], - metadata: { - sm_source: "consumer", - }, - }, - }) - - if (response.error) { - throw new Error(response.error?.message || "Failed to add link") - } - - return response.data - }, - onMutate: async ({ url, project }) => { - await queryClient.cancelQueries({ - queryKey: ["documents-with-memories", project], - }) - - const previousMemories = queryClient.getQueryData([ - "documents-with-memories", - project, - ]) - - const optimisticMemory = { - id: `temp-${Date.now()}`, - content: "", - url: url, - title: "Processing...", - description: "Extracting content...", - containerTags: [project], - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - status: "queued", - type: "link", - metadata: { - processingStage: "queued", - processingMessage: "Added to processing queue", - }, - memoryEntries: [], - isOptimistic: true, - } - - queryClient.setQueryData( - ["documents-with-memories", project], - (old: DocumentsQueryData | undefined) => { - if (!old) return { documents: [optimisticMemory], totalCount: 1 } - return { - ...old, - documents: [optimisticMemory, ...old.documents], - totalCount: old.totalCount + 1, - } - }, - ) - - return { previousMemories } - }, - onError: (_error, variables, context) => { - if (context?.previousMemories) { - queryClient.setQueryData( - ["documents-with-memories", variables.project], - context.previousMemories, - ) - } - toast.error("Failed to add link", { - description: _error instanceof Error ? _error.message : "Unknown error", - }) - }, - onSuccess: (_data, variables) => { - toast.success("Link added successfully!", { - description: "Your link is being processed", - }) - queryClient.invalidateQueries({ - queryKey: ["documents-with-memories", variables.project], - }) - onClose() - }, - }) - - const fileMutation = useMutation({ - mutationFn: async ({ - file, - title, - description, - project, - }: { - file: File - title?: string - description?: string - project: string - }) => { - const formData = new FormData() - formData.append("file", file) - formData.append("containerTags", JSON.stringify([project])) - formData.append( - "metadata", - JSON.stringify({ - sm_source: "consumer", - }), - ) - - const response = await fetch( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/v3/documents/file`, - { - method: "POST", - body: formData, - credentials: "include", - }, - ) - - if (!response.ok) { - const error = await response.json() - throw new Error(error.error || "Failed to upload file") - } - - const data = await response.json() - - if (title || description) { - await $fetch(`@patch/documents/${data.id}`, { - body: { - metadata: { - ...(title && { title }), - ...(description && { description }), - sm_source: "consumer", - }, - }, - }) - } - - return data - }, - onMutate: async ({ file, title, description, project }) => { - await queryClient.cancelQueries({ - queryKey: ["documents-with-memories", project], - }) - - const previousMemories = queryClient.getQueryData([ - "documents-with-memories", - project, - ]) - - const optimisticMemory = { - id: `temp-file-${Date.now()}`, - content: "", - url: null, - title: title || file.name, - description: description || `Uploading ${file.name}...`, - containerTags: [project], - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - status: "processing", - type: "file", - metadata: { - fileName: file.name, - fileSize: file.size, - mimeType: file.type, - }, - memoryEntries: [], - } - - queryClient.setQueryData( - ["documents-with-memories", project], - (old: DocumentsQueryData | undefined) => { - if (!old) return { documents: [optimisticMemory], totalCount: 1 } - return { - ...old, - documents: [optimisticMemory, ...old.documents], - totalCount: old.totalCount + 1, - } - }, - ) - - return { previousMemories } - }, - onError: (_error, variables, context) => { - if (context?.previousMemories) { - queryClient.setQueryData( - ["documents-with-memories", variables.project], - context.previousMemories, - ) - } - toast.error("Failed to upload file", { - description: _error instanceof Error ? _error.message : "Unknown error", - }) - }, - onSuccess: (_data, variables) => { - toast.success("File uploaded successfully!", { - description: "Your file is being processed", - }) - queryClient.invalidateQueries({ - queryKey: ["documents-with-memories", variables.project], - }) - onClose() - }, - }) - - return { - noteMutation, - linkMutation, - fileMutation, - } -} 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 (
-
- - {name && ( -
-

- {userName} -

-

- supermemory -

-
- )} -
+ + + + + + + + + Home + + + + + + Developer console + + + + + + supermemory.ai + + + +

📁 {projectName}

@@ -212,7 +259,10 @@ export function Header({ onAddMemory, onOpenMCP }: HeaderProps) { Settings - authClient.signOut()}> + { + authClient.signOut() + router.push("/login/new") + }}> Logout 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" >