diff options
| author | Dhravya Shah <[email protected]> | 2025-09-18 20:34:18 -0700 |
|---|---|---|
| committer | Dhravya Shah <[email protected]> | 2025-09-18 21:03:49 -0700 |
| commit | 1fcb56908920da386900abb4ce2383374a625c72 (patch) | |
| tree | 0f9d7f695d4c9b1b85be3950fc869e0061dff3ed /apps/web | |
| parent | refetching logic change (diff) | |
| download | supermemory-09-18-formatting.tar.xz supermemory-09-18-formatting.zip | |
formatting09-18-formatting
Diffstat (limited to 'apps/web')
38 files changed, 1824 insertions, 2044 deletions
diff --git a/apps/web/app/api/emails/welcome/route.tsx b/apps/web/app/api/emails/welcome/route.tsx index 48883d6b..69e3ae07 100644 --- a/apps/web/app/api/emails/welcome/route.tsx +++ b/apps/web/app/api/emails/welcome/route.tsx @@ -5,9 +5,9 @@ export async function GET() { return new ImageResponse( <div tw="w-full h-full flex flex-col justify-center items-center"> <img - src="https://pub-1be2b1df2c7e456f8e21149e972f4caf.r2.dev/bust.png" alt="Google Logo" height={367} + src="https://pub-1be2b1df2c7e456f8e21149e972f4caf.r2.dev/bust.png" width={369} /> </div>, diff --git a/apps/web/app/global-error.tsx b/apps/web/app/global-error.tsx index 9bda5fee..786d6682 100644 --- a/apps/web/app/global-error.tsx +++ b/apps/web/app/global-error.tsx @@ -4,20 +4,24 @@ import * as Sentry from "@sentry/nextjs"; import NextError from "next/error"; import { useEffect } from "react"; -export default function GlobalError({ error }: { error: Error & { digest?: string } }) { - useEffect(() => { - Sentry.captureException(error); - }, [error]); +export default function GlobalError({ + error, +}: { + error: Error & { digest?: string }; +}) { + useEffect(() => { + Sentry.captureException(error); + }, [error]); - return ( - <html> - <body> - {/* `NextError` is the default Next.js error page component. Its type + return ( + <html> + <body> + {/* `NextError` is the default Next.js error page component. Its type definition requires a `statusCode` prop. However, since the App Router does not expose status codes for errors, we simply pass 0 to render a generic error message. */} - <NextError statusCode={0} /> - </body> - </html> - ); -}
\ No newline at end of file + <NextError statusCode={0} /> + </body> + </html> + ); +} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index e6d9094d..629dced6 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,5 +1,5 @@ import type { Metadata } from "next"; -import { Inter, JetBrains_Mono } from "next/font/google"; +import { Inter, JetBrains_Mono, Space_Grotesk } from "next/font/google"; import "../globals.css"; import "@ui/globals.css"; import { AuthProvider } from "@lib/auth-context"; @@ -24,6 +24,11 @@ const mono = JetBrains_Mono({ variable: "--font-mono", }); +const spaceGrotesk = Space_Grotesk({ + subsets: ["latin"], + variable: "--font-space-grotesk", +}); + export const metadata: Metadata = { metadataBase: new URL("https://app.supermemory.ai"), description: "Your memories, wherever you are", @@ -38,7 +43,7 @@ export default function RootLayout({ return ( <html className="dark bg-sm-black" lang="en"> <body - className={`${sans.variable} ${mono.variable} antialiased bg-[#0f1419]`} + className={`${sans.variable} ${mono.variable} ${spaceGrotesk.variable} antialiased bg-[#0f1419]`} > <AutumnProvider backendUrl={ diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index d2dd6b4a..bc87a087 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,14 +1,14 @@ -"use client" - -import { useIsMobile } from "@hooks/use-mobile" -import { useAuth } from "@lib/auth-context" -import { $fetch } from "@repo/lib/api" -import { MemoryGraph } from "@repo/ui/memory-graph" -import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api" -import { useInfiniteQuery, useQuery } from "@tanstack/react-query" -import { Logo, LogoFull } from "@ui/assets/Logo" -import { Button } from "@ui/components/button" -import { GlassMenuEffect } from "@ui/other/glass-effect" +"use client"; + +import { useIsMobile } from "@hooks/use-mobile"; +import { useAuth } from "@lib/auth-context"; +import { $fetch } from "@repo/lib/api"; +import { MemoryGraph } from "@repo/ui/memory-graph"; +import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"; +import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; +import { Logo, LogoFull } from "@ui/assets/Logo"; +import { Button } from "@ui/components/button"; +import { GlassMenuEffect } from "@ui/other/glass-effect"; import { HelpCircle, LayoutGrid, @@ -16,58 +16,59 @@ import { LoaderIcon, MessageSquare, Unplug, -} from "lucide-react" -import { AnimatePresence, motion } from "motion/react" -import Link from "next/link" -import { useCallback, useEffect, useMemo, useState } from "react" -import type { z } from "zod" -import { ConnectAIModal } from "@/components/connect-ai-modal" -import { InstallPrompt } from "@/components/install-prompt" -import { MemoryListView } from "@/components/memory-list-view" -import Menu from "@/components/menu" -import { ProjectSelector } from "@/components/project-selector" -import { ReferralUpgradeModal } from "@/components/referral-upgrade-modal" -import type { TourStep } from "@/components/tour" -import { TourAlertDialog, useTour } from "@/components/tour" -import { AddMemoryView } from "@/components/views/add-memory" -import { ChatRewrite } from "@/components/views/chat" -import { TOUR_STEP_IDS, TOUR_STORAGE_KEY } from "@/lib/tour-constants" -import { useViewMode } from "@/lib/view-mode-context" -import { useChatOpen, useProject } from "@/stores" -import { useGraphHighlights } from "@/stores/highlights" - -type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema> -type DocumentWithMemories = DocumentsResponse["documents"][0] +} from "lucide-react"; +import { AnimatePresence, motion } from "motion/react"; +import Link from "next/link"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import type { z } from "zod"; +import { ConnectAIModal } from "@/components/connect-ai-modal"; +import { GetStarted } from "@/components/get-started"; +import { InstallPrompt } from "@/components/install-prompt"; +import { MemoryListView } from "@/components/memory-list-view"; +import Menu from "@/components/menu"; +import { ProjectSelector } from "@/components/project-selector"; +import { ReferralUpgradeModal } from "@/components/referral-upgrade-modal"; +import type { TourStep } from "@/components/tour"; +import { TourAlertDialog, useTour } from "@/components/tour"; +import { AddMemoryView } from "@/components/views/add-memory"; +import { ChatRewrite } from "@/components/views/chat"; +import { TOUR_STEP_IDS, TOUR_STORAGE_KEY } from "@/lib/tour-constants"; +import { useViewMode } from "@/lib/view-mode-context"; +import { useChatOpen, useProject } from "@/stores"; +import { useGraphHighlights } from "@/stores/highlights"; + +type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>; +type DocumentWithMemories = DocumentsResponse["documents"][0]; const MemoryGraphPage = () => { - const { documentIds: allHighlightDocumentIds } = useGraphHighlights() - const isMobile = useIsMobile() - const { viewMode, setViewMode, isInitialized } = useViewMode() - const { selectedProject } = useProject() - const { setSteps, isTourCompleted } = useTour() - const { isOpen, setIsOpen } = useChatOpen() - const [injectedDocs, setInjectedDocs] = useState<DocumentWithMemories[]>([]) - const [showAddMemoryView, setShowAddMemoryView] = useState(false) - const [showReferralModal, setShowReferralModal] = useState(false) - const [showConnectAIModal, setShowConnectAIModal] = useState(false) - const [isHelpHovered, setIsHelpHovered] = useState(false) + const { documentIds: allHighlightDocumentIds } = useGraphHighlights(); + const isMobile = useIsMobile(); + const { viewMode, setViewMode, isInitialized } = useViewMode(); + const { selectedProject } = useProject(); + const { setSteps, isTourCompleted } = useTour(); + const { isOpen, setIsOpen } = useChatOpen(); + const [injectedDocs, setInjectedDocs] = useState<DocumentWithMemories[]>([]); + const [showAddMemoryView, setShowAddMemoryView] = useState(false); + const [showReferralModal, setShowReferralModal] = useState(false); + const [showConnectAIModal, setShowConnectAIModal] = useState(false); + const [isHelpHovered, setIsHelpHovered] = useState(false); // Fetch projects meta to detect experimental flag const { data: projectsMeta = [] } = useQuery({ queryKey: ["projects"], queryFn: async () => { - const response = await $fetch("@get/projects") - return response.data?.projects ?? [] + const response = await $fetch("@get/projects"); + return response.data?.projects ?? []; }, staleTime: 5 * 60 * 1000, - }) + }); const isCurrentProjectExperimental = !!projectsMeta.find( (p: any) => p.containerTag === selectedProject, - )?.isExperimental + )?.isExperimental; // Tour state - const [showTourDialog, setShowTourDialog] = useState(false) + const [showTourDialog, setShowTourDialog] = useState(false); // Define tour steps with useMemo to prevent recreation const tourSteps: TourStep[] = useMemo(() => { @@ -200,37 +201,37 @@ const MemoryGraphPage = () => { selectorId: TOUR_STEP_IDS.FLOATING_CHAT, position: "left", }, - ] - }, []) + ]; + }, []); // Check if tour has been completed before useEffect(() => { - const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === "true" + const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === "true"; if (!hasCompletedTour && !isTourCompleted) { const timer = setTimeout(() => { - setShowTourDialog(true) - setShowConnectAIModal(false) - }, 1000) // Show after 1 second - return () => clearTimeout(timer) + setShowTourDialog(true); + setShowConnectAIModal(false); + }, 1000); // Show after 1 second + return () => clearTimeout(timer); } - }, [isTourCompleted]) + }, [isTourCompleted]); // Set up tour steps useEffect(() => { - setSteps(tourSteps) - }, [setSteps, tourSteps]) + setSteps(tourSteps); + }, [setSteps, tourSteps]); // Save tour completion to localStorage useEffect(() => { if (isTourCompleted) { - localStorage.setItem(TOUR_STORAGE_KEY, "true") + localStorage.setItem(TOUR_STORAGE_KEY, "true"); } - }, [isTourCompleted]) + }, [isTourCompleted]); // Progressive loading via useInfiniteQuery - const IS_DEV = process.env.NODE_ENV === "development" - const PAGE_SIZE = IS_DEV ? 100 : 100 - const MAX_TOTAL = 1000 + const IS_DEV = process.env.NODE_ENV === "development"; + const PAGE_SIZE = IS_DEV ? 100 : 100; + const MAX_TOTAL = 1000; const { data, @@ -252,75 +253,76 @@ const MemoryGraphPage = () => { containerTags: selectedProject ? [selectedProject] : undefined, }, disableValidation: true, - }) + }); if (response.error) { - throw new Error(response.error?.message || "Failed to fetch documents") + throw new Error(response.error?.message || "Failed to fetch documents"); } - return response.data + return response.data; }, getNextPageParam: (lastPage, allPages) => { const loaded = allPages.reduce( (acc, p) => acc + (p.documents?.length ?? 0), 0, - ) - if (loaded >= MAX_TOTAL) return undefined + ); + if (loaded >= MAX_TOTAL) return undefined; - const { currentPage, totalPages } = lastPage.pagination + const { currentPage, totalPages } = lastPage.pagination; if (currentPage < totalPages) { - return currentPage + 1 + return currentPage + 1; } - return undefined + return undefined; }, staleTime: 5 * 60 * 1000, - }) + }); const baseDocuments = useMemo(() => { return ( data?.pages.flatMap((p: DocumentsResponse) => p.documents ?? []) ?? [] - ) - }, [data]) + ); + }, [data]); const allDocuments = useMemo(() => { - if (injectedDocs.length === 0) return baseDocuments - const byId = new Map<string, DocumentWithMemories>() - for (const d of injectedDocs) byId.set(d.id, d) - for (const d of baseDocuments) if (!byId.has(d.id)) byId.set(d.id, d) - return Array.from(byId.values()) - }, [baseDocuments, injectedDocs]) + if (injectedDocs.length === 0) return baseDocuments; + const byId = new Map<string, DocumentWithMemories>(); + for (const d of injectedDocs) byId.set(d.id, d); + for (const d of baseDocuments) if (!byId.has(d.id)) byId.set(d.id, d); + return Array.from(byId.values()); + }, [baseDocuments, injectedDocs]); - const totalLoaded = allDocuments.length - const hasMore = hasNextPage - const isLoadingMore = isFetchingNextPage + const totalLoaded = allDocuments.length; + const hasMore = hasNextPage; + const isLoadingMore = isFetchingNextPage; const loadMoreDocuments = useCallback(async (): Promise<void> => { if (hasNextPage && !isFetchingNextPage) { - await fetchNextPage() - return + await fetchNextPage(); + return; } - return - }, [hasNextPage, isFetchingNextPage, fetchNextPage]) + return; + }, [hasNextPage, isFetchingNextPage, fetchNextPage]); // Reset injected docs when project changes useEffect(() => { - setInjectedDocs([]) - }, [selectedProject]) + setInjectedDocs([]); + }, [selectedProject]); // Surgical fetch of missing highlighted documents (customId-based IDs from search) useEffect(() => { - if (!isOpen) return - if (!allHighlightDocumentIds || allHighlightDocumentIds.length === 0) return - const present = new Set<string>() + if (!isOpen) return; + if (!allHighlightDocumentIds || allHighlightDocumentIds.length === 0) + return; + const present = new Set<string>(); for (const d of [...baseDocuments, ...injectedDocs]) { - if (d.id) present.add(d.id) - if ((d as any).customId) present.add((d as any).customId as string) + if (d.id) present.add(d.id); + if ((d as any).customId) present.add((d as any).customId as string); } const missing = allHighlightDocumentIds.filter( (id: string) => !present.has(id), - ) - if (missing.length === 0) return - let cancelled = false + ); + if (missing.length === 0) return; + let cancelled = false; const run = async () => { try { const resp = await $fetch("@post/memories/documents/by-ids", { @@ -330,32 +332,32 @@ const MemoryGraphPage = () => { containerTags: selectedProject ? [selectedProject] : undefined, }, disableValidation: true, - }) - if (cancelled || (resp as any)?.error) return + }); + if (cancelled || (resp as any)?.error) return; const extraDocs = (resp as any)?.data?.documents as | DocumentWithMemories[] - | undefined - if (!extraDocs || extraDocs.length === 0) return + | undefined; + if (!extraDocs || extraDocs.length === 0) return; setInjectedDocs((prev) => { const seen = new Set<string>([ ...prev.map((d) => d.id), ...baseDocuments.map((d) => d.id), - ]) - const merged = [...prev] + ]); + const merged = [...prev]; for (const doc of extraDocs) { if (!seen.has(doc.id)) { - merged.push(doc) - seen.add(doc.id) + merged.push(doc); + seen.add(doc.id); } } - return merged - }) + return merged; + }); } catch {} - } - void run() + }; + void run(); return () => { - cancelled = true - } + cancelled = true; + }; }, [ isOpen, allHighlightDocumentIds.join("|"), @@ -363,39 +365,39 @@ const MemoryGraphPage = () => { injectedDocs, selectedProject, $fetch, - ]) + ]); // Handle view mode change const handleViewModeChange = useCallback( (mode: "graph" | "list") => { - setViewMode(mode) + setViewMode(mode); }, [setViewMode], - ) + ); useEffect(() => { - const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === "true" + const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === "true"; if (hasCompletedTour && allDocuments.length === 0 && !showTourDialog) { - setShowConnectAIModal(true) + setShowConnectAIModal(true); } else if (showTourDialog) { - setShowConnectAIModal(false) + setShowConnectAIModal(false); } - }, [allDocuments.length, showTourDialog]) + }, [allDocuments.length, showTourDialog]); // Prevent body scrolling useEffect(() => { - document.body.style.overflow = "hidden" - document.body.style.height = "100vh" - document.documentElement.style.overflow = "hidden" - document.documentElement.style.height = "100vh" + document.body.style.overflow = "hidden"; + document.body.style.height = "100vh"; + document.documentElement.style.overflow = "hidden"; + document.documentElement.style.height = "100vh"; return () => { - document.body.style.overflow = "" - document.body.style.height = "" - document.documentElement.style.overflow = "" - document.documentElement.style.height = "" - } - }, []) + document.body.style.overflow = ""; + document.body.style.height = ""; + document.documentElement.style.overflow = ""; + document.documentElement.style.height = ""; + }; + }, []); return ( <div className="relative h-screen bg-[#0f1419] overflow-hidden touch-none"> @@ -526,9 +528,9 @@ const MemoryGraphPage = () => { <button className="text-sm text-blue-400 hover:text-blue-300 transition-colors underline" onClick={(e) => { - e.stopPropagation() - setShowAddMemoryView(true) - setShowConnectAIModal(false) + e.stopPropagation(); + setShowAddMemoryView(true); + setShowConnectAIModal(false); }} type="button" > @@ -564,37 +566,7 @@ const MemoryGraphPage = () => { loadMoreDocuments={loadMoreDocuments} totalLoaded={totalLoaded} > - <div className="absolute inset-0 flex items-center justify-center"> - <ConnectAIModal - onOpenChange={setShowConnectAIModal} - open={showConnectAIModal} - > - <div className="rounded-xl overflow-hidden cursor-pointer hover:bg-white/5 transition-colors p-6"> - <div className="relative z-10 text-slate-200 text-center"> - <p className="text-lg font-medium mb-4"> - Get Started with supermemory - </p> - <div className="flex flex-col gap-3"> - <p className="text-sm text-blue-400 hover:text-blue-300 transition-colors"> - Click here to set up your AI connection - </p> - <p className="text-xs text-white/60">or</p> - <button - className="text-sm text-blue-400 hover:text-blue-300 transition-colors underline" - onClick={(e) => { - e.stopPropagation() - setShowAddMemoryView(true) - setShowConnectAIModal(false) - }} - type="button" - > - Add your first memory - </button> - </div> - </div> - </div> - </ConnectAIModal> - </div> + <GetStarted /> </MemoryListView> </motion.div> )} @@ -735,35 +707,35 @@ const MemoryGraphPage = () => { onClose={() => setShowReferralModal(false)} /> </div> - ) -} + ); +}; // Wrapper component to handle auth and waitlist checks export default function Page() { - const { user, session } = useAuth() + const { user, session } = useAuth(); useEffect(() => { - const url = new URL(window.location.href) + const url = new URL(window.location.href); const authenticateChromeExtension = url.searchParams.get( "extension-auth-success", - ) + ); if (authenticateChromeExtension) { - const sessionToken = session?.token + const sessionToken = session?.token; const userData = { email: user?.email, name: user?.name, userId: user?.id, - } + }; if (sessionToken && userData?.email) { - const encodedToken = encodeURIComponent(sessionToken) - window.postMessage({ token: encodedToken, userData }, "*") - url.searchParams.delete("extension-auth-success") - window.history.replaceState({}, "", url.toString()) + const encodedToken = encodeURIComponent(sessionToken); + window.postMessage({ token: encodedToken, userData }, "*"); + url.searchParams.delete("extension-auth-success"); + window.history.replaceState({}, "", url.toString()); } } - }, [user, session]) + }, [user, session]); // Show loading state while checking authentication and waitlist status if (!user) { @@ -774,7 +746,7 @@ export default function Page() { <p className="text-white/60">Loading...</p> </div> </div> - ) + ); } // If we have a user and they have access, show the main component @@ -783,5 +755,5 @@ export default function Page() { <MemoryGraphPage /> <InstallPrompt /> </> - ) + ); } diff --git a/apps/web/app/ref/[code]/page.tsx b/apps/web/app/ref/[code]/page.tsx index 98afcfe5..f5633029 100644 --- a/apps/web/app/ref/[code]/page.tsx +++ b/apps/web/app/ref/[code]/page.tsx @@ -55,8 +55,6 @@ export default function ReferralPage() { checkReferral(); }, [referralCode]); - - const handleCopyLink = async () => { try { await navigator.clipboard.writeText(referralLink); @@ -146,11 +144,10 @@ export default function ReferralPage() { </p> </div> - <div className="text-center"> <Link - href="https://supermemory.ai" className="text-orange-500 hover:text-orange-400 text-sm underline" + href="https://supermemory.ai" > Learn more about supermemory </Link> @@ -178,10 +175,10 @@ export default function ReferralPage() { </p> </div> <Button + className="shrink-0 border-white/10 hover:bg-white/5" onClick={handleCopyLink} size="sm" variant="outline" - className="shrink-0 border-white/10 hover:bg-white/5" > {copiedLink ? ( <CheckIcon className="w-4 h-4" /> @@ -192,9 +189,9 @@ export default function ReferralPage() { </div> <Button + className="w-full border-white/10 text-white hover:bg-white/5" onClick={handleShare} variant="outline" - className="w-full border-white/10 text-white hover:bg-white/5" > <ShareIcon className="w-4 h-4" /> Share this link diff --git a/apps/web/app/upgrade-mcp/page.tsx b/apps/web/app/upgrade-mcp/page.tsx index d62b3769..47d643a1 100644 --- a/apps/web/app/upgrade-mcp/page.tsx +++ b/apps/web/app/upgrade-mcp/page.tsx @@ -1,107 +1,107 @@ -"use client" +"use client"; -import { $fetch } from "@lib/api" -import { useSession } from "@lib/auth" -import { useMutation } from "@tanstack/react-query" -import { Logo, LogoFull } from "@ui/assets/Logo" -import { Button } from "@ui/components/button" -import { Input } from "@ui/components/input" -import { GlassMenuEffect } from "@ui/other/glass-effect" -import { ArrowRight, CheckCircle, Upload, Zap } from "lucide-react" -import { AnimatePresence, motion } from "motion/react" -import Link from "next/link" -import { useRouter, useSearchParams } from "next/navigation" -import { useEffect, useState } from "react" -import { toast } from "sonner" -import { Spinner } from "@/components/spinner" +import { $fetch } from "@lib/api"; +import { useSession } from "@lib/auth"; +import { useMutation } from "@tanstack/react-query"; +import { Logo, LogoFull } from "@ui/assets/Logo"; +import { Button } from "@ui/components/button"; +import { Input } from "@ui/components/input"; +import { GlassMenuEffect } from "@ui/other/glass-effect"; +import { ArrowRight, CheckCircle, Upload, Zap } from "lucide-react"; +import { AnimatePresence, motion } from "motion/react"; +import Link from "next/link"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; +import { Spinner } from "@/components/spinner"; interface MigrateMCPRequest { - userId: string - projectId: string + userId: string; + projectId: string; } interface MigrateMCPResponse { - success: boolean - migratedCount: number - message: string - documentIds?: string[] + success: boolean; + migratedCount: number; + message: string; + documentIds?: string[]; } export default function MigrateMCPPage() { - const router = useRouter() - const searchParams = useSearchParams() - const [mcpUrl, setMcpUrl] = useState("") - const [projectId, setProjectId] = useState("default") + const router = useRouter(); + const searchParams = useSearchParams(); + const [mcpUrl, setMcpUrl] = useState(""); + const [projectId, setProjectId] = useState("default"); - const session = useSession() + const session = useSession(); // Extract MCP URL from query parameter useEffect(() => { - const urlParam = searchParams.get("url") + const urlParam = searchParams.get("url"); if (urlParam) { - setMcpUrl(urlParam) + setMcpUrl(urlParam); } - }, [searchParams]) + }, [searchParams]); useEffect(() => { - console.log("session", session) + console.log("session", session); if (!session.isPending && !session.data) { - const redirectUrl = new URL("/login", window.location.href) - redirectUrl.searchParams.set("redirect", window.location.href) - router.push(redirectUrl.toString()) - return + const redirectUrl = new URL("/login", window.location.href); + redirectUrl.searchParams.set("redirect", window.location.href); + router.push(redirectUrl.toString()); + return; } - }, [session, router]) + }, [session, router]); // Extract userId from MCP URL const getUserIdFromUrl = (url: string) => { - return url.split("/").at(-2) || "" - } + return url.split("/").at(-2) || ""; + }; const migrateMutation = useMutation({ mutationFn: async (data: MigrateMCPRequest) => { const response = await $fetch("@post/documents/migrate-mcp", { body: data, - }) + }); if (response.error) { throw new Error( response.error?.message || "Failed to migrate documents", - ) + ); } - return response.data + return response.data; }, onSuccess: (data: MigrateMCPResponse) => { toast.success("Migration completed successfully", { description: data.message, - }) + }); // Redirect to home page after successful migration setTimeout(() => { - router.push("/?open=mcp") - }, 2000) // Wait 2 seconds to show the success message + router.push("/?open=mcp"); + }, 2000); // Wait 2 seconds to show the success message }, onError: (error: Error) => { toast.error("Migration failed", { description: error.message || "An unexpected error occurred", - }) + }); }, - }) + }); const handleSubmit = (e: React.FormEvent) => { - e.preventDefault() + e.preventDefault(); - const userId = getUserIdFromUrl(mcpUrl) + const userId = getUserIdFromUrl(mcpUrl); if (!userId) { - toast.error("Please enter a valid MCP URL") - return + toast.error("Please enter a valid MCP URL"); + return; } migrateMutation.mutate({ userId, projectId: projectId.trim() || "default", - }) - } + }); + }; return ( <div className="min-h-screen bg-[#0f1419] overflow-hidden relative"> @@ -320,5 +320,5 @@ export default function MigrateMCPPage() { </motion.div> </div> </div> - ) + ); } diff --git a/apps/web/biome.json b/apps/web/biome.json index 48649190..c33643a3 100644 --- a/apps/web/biome.json +++ b/apps/web/biome.json @@ -9,4 +9,4 @@ } } } -}
\ No newline at end of file +} diff --git a/apps/web/components/connect-ai-modal.tsx b/apps/web/components/connect-ai-modal.tsx index f3008796..cbd5a0dd 100644 --- a/apps/web/components/connect-ai-modal.tsx +++ b/apps/web/components/connect-ai-modal.tsx @@ -235,7 +235,11 @@ export function ConnectAIModal({ parent.appendChild(fallback); } }} - src={key === "mcp-url" ? "/mcp-icon.svg" : `/mcp-supported-tools/${key === "claude-code" ? "claude" : key}.png`} + src={ + key === "mcp-url" + ? "/mcp-icon.svg" + : `/mcp-supported-tools/${key === "claude-code" ? "claude" : key}.png` + } width={20} /> </div> diff --git a/apps/web/components/create-project-dialog.tsx b/apps/web/components/create-project-dialog.tsx index 1672a689..d281699e 100644 --- a/apps/web/components/create-project-dialog.tsx +++ b/apps/web/components/create-project-dialog.tsx @@ -46,12 +46,12 @@ export function CreateProjectDialog({ return ( <AnimatePresence> {open && ( - <Dialog open={open} onOpenChange={onOpenChange}> + <Dialog onOpenChange={onOpenChange} open={open}> <DialogContent className="sm:max-w-2xl bg-[#0f1419] backdrop-blur-xl border-white/10 text-white"> <motion.div - initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.95 }} + initial={{ opacity: 0, scale: 0.95 }} > <DialogHeader> <DialogTitle>Create New Project</DialogTitle> @@ -61,23 +61,23 @@ export function CreateProjectDialog({ </DialogHeader> <div className="grid gap-4 py-4"> <motion.div - initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} - transition={{ delay: 0.1 }} className="flex flex-col gap-2" + initial={{ opacity: 0, y: 10 }} + transition={{ delay: 0.1 }} > <Label htmlFor="projectName">Project Name</Label> <Input - id="projectName" className="bg-white/5 border-white/10 text-white" - placeholder="My Awesome Project" - value={projectName} + id="projectName" onChange={(e) => setProjectName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && projectName.trim()) { handleCreate(); } }} + placeholder="My Awesome Project" + value={projectName} /> <p className="text-xs text-white/50"> This will help you organize your memories @@ -90,10 +90,10 @@ export function CreateProjectDialog({ whileTap={{ scale: 0.95 }} > <Button - type="button" - variant="outline" className="bg-white/5 hover:bg-white/10 border-white/10 text-white" onClick={handleClose} + type="button" + variant="outline" > Cancel </Button> @@ -103,12 +103,12 @@ export function CreateProjectDialog({ whileTap={{ scale: 0.95 }} > <Button - type="button" className="bg-white/10 hover:bg-white/20 text-white border-white/20" disabled={ createProjectMutation.isPending || !projectName.trim() } onClick={handleCreate} + type="button" > {createProjectMutation.isPending ? ( <> diff --git a/apps/web/components/get-started.tsx b/apps/web/components/get-started.tsx index 1619ca2e..8c44de88 100644 --- a/apps/web/components/get-started.tsx +++ b/apps/web/components/get-started.tsx @@ -1,26 +1,26 @@ -import { AnimatePresence, motion } from "framer-motion" -import { Brain, Chrome, ExternalLink, Plus } from "lucide-react" -import { useEffect, useState } from "react" -import { ConnectAIModal } from "@/components/connect-ai-modal" -import { analytics } from "@/lib/analytics" +import { AnimatePresence, motion } from "framer-motion"; +import { Brain, Chrome, ExternalLink, Plus } from "lucide-react"; +import { useEffect, useState } from "react"; +import { ConnectAIModal } from "@/components/connect-ai-modal"; +import { analytics } from "@/lib/analytics"; interface GetStartedProps { - setShowAddMemoryView?: (show: boolean) => void + setShowAddMemoryView?: (show: boolean) => void; } export const GetStarted = ({ setShowAddMemoryView }: GetStartedProps) => { - const [imageLoaded, setImageLoaded] = useState(false) - const [showContent, setShowContent] = useState(false) - const [showConnectAIModal, setShowConnectAIModal] = useState(false) + const [imageLoaded, setImageLoaded] = useState(false); + const [showContent, setShowContent] = useState(false); + const [showConnectAIModal, setShowConnectAIModal] = useState(false); useEffect(() => { - const img = new Image() + const img = new Image(); img.onload = () => { - setImageLoaded(true) - setTimeout(() => setShowContent(true), 100) - } - img.src = "/images/onboarding.png" - }, []) + setImageLoaded(true); + setTimeout(() => setShowContent(true), 100); + }; + img.src = "/images/onboarding.png"; + }, []); const containerVariants = { hidden: { opacity: 0 }, @@ -33,7 +33,7 @@ export const GetStarted = ({ setShowAddMemoryView }: GetStartedProps) => { delayChildren: 0.2, }, }, - } + }; const leftColumnVariants = { hidden: { @@ -49,7 +49,7 @@ export const GetStarted = ({ setShowAddMemoryView }: GetStartedProps) => { staggerChildren: 0.1, }, }, - } + }; const rightColumnVariants = { hidden: { @@ -66,7 +66,7 @@ export const GetStarted = ({ setShowAddMemoryView }: GetStartedProps) => { staggerChildren: 0.1, }, }, - } + }; const textLineVariants = { hidden: { @@ -81,7 +81,7 @@ export const GetStarted = ({ setShowAddMemoryView }: GetStartedProps) => { ease: [0.25, 0.1, 0.25, 1] as const, }, }, - } + }; const cardVariants = { hidden: { @@ -98,7 +98,7 @@ export const GetStarted = ({ setShowAddMemoryView }: GetStartedProps) => { ease: [0.25, 0.1, 0.25, 1] as const, }, }, - } + }; const backgroundVariants = { hidden: { @@ -113,22 +113,22 @@ export const GetStarted = ({ setShowAddMemoryView }: GetStartedProps) => { ease: [0.25, 0.1, 0.25, 1] as const, }, }, - } + }; const handleChromeExtension = () => { - analytics.extensionInstallClicked() + analytics.extensionInstallClicked(); window.open( "https://chromewebstore.google.com/detail/supermemory/afpgkkipfdpeaflnpoaffkcankadgjfc", "_blank", - ) - } + ); + }; const handleAddMemory = () => { - setShowConnectAIModal(false) + setShowConnectAIModal(false); if (setShowAddMemoryView) { - setShowAddMemoryView(true) + setShowAddMemoryView(true); } - } + }; return ( <div className="fixed inset-0 overflow-hidden bg-black"> @@ -358,5 +358,5 @@ export const GetStarted = ({ setShowAddMemoryView }: GetStartedProps) => { </> )} </div> - ) -} + ); +}; diff --git a/apps/web/components/install-prompt.tsx b/apps/web/components/install-prompt.tsx index 3bbeb071..382f4fa7 100644 --- a/apps/web/components/install-prompt.tsx +++ b/apps/web/components/install-prompt.tsx @@ -70,9 +70,9 @@ export function InstallPrompt() { <AnimatePresence> <motion.div animate={{ y: 0, opacity: 1 }} + className="fixed bottom-4 left-4 right-4 z-50 mx-auto max-w-sm md:hidden" exit={{ y: 100, opacity: 0 }} initial={{ y: 100, opacity: 0 }} - className="fixed bottom-4 left-4 right-4 z-50 mx-auto max-w-sm md:hidden" > <div className="bg-black/90 backdrop-blur-md text-white rounded-2xl p-4 shadow-2xl border border-white/10"> <div className="flex items-start justify-between mb-3"> @@ -83,10 +83,10 @@ export function InstallPrompt() { <h3 className="font-semibold text-sm">Install Supermemory</h3> </div> <Button - variant="ghost" - size="sm" - onClick={handleDismiss} className="text-white/60 hover:text-white h-6 w-6 p-0" + onClick={handleDismiss} + size="sm" + variant="ghost" > <X className="w-4 h-4" /> </Button> @@ -107,19 +107,19 @@ export function InstallPrompt() { 2. Select "Add to Home Screen" ➕ </p> <Button - variant="secondary" - size="sm" - onClick={handleDismiss} className="w-full text-xs" + onClick={handleDismiss} + size="sm" + variant="secondary" > Got it </Button> </div> ) : ( <Button + className="w-full bg-[#0f1419] hover:bg-[#1a1f2a] text-white text-xs" onClick={handleInstall} size="sm" - className="w-full bg-[#0f1419] hover:bg-[#1a1f2a] text-white text-xs" > <Download className="w-3 h-3 mr-1" /> Add to Home Screen diff --git a/apps/web/components/memories/index.tsx b/apps/web/components/memories/index.tsx index 97ef57bd..0411fbe2 100644 --- a/apps/web/components/memories/index.tsx +++ b/apps/web/components/memories/index.tsx @@ -50,4 +50,4 @@ export const getSourceUrl = (document: DocumentWithMemories) => { } // Fallback to existing URL for all other document types return document.url; -};
\ No newline at end of file +}; diff --git a/apps/web/components/memories/memory-detail.tsx b/apps/web/components/memories/memory-detail.tsx index dad2a8a3..7470f9db 100644 --- a/apps/web/components/memories/memory-detail.tsx +++ b/apps/web/components/memories/memory-detail.tsx @@ -1,415 +1,422 @@ -import { getDocumentIcon } from '@/lib/document-icon'; import { - Drawer, - DrawerContent, - DrawerHeader, - DrawerTitle, -} from '@repo/ui/components/drawer'; + Drawer, + DrawerContent, + DrawerHeader, + DrawerTitle, +} from "@repo/ui/components/drawer"; import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, -} from '@repo/ui/components/sheet'; + Sheet, + SheetContent, + SheetHeader, + SheetTitle, +} from "@repo/ui/components/sheet"; import { - Tabs, - TabsList, - TabsTrigger, - TabsContent, -} from '@repo/ui/components/tabs'; -import { colors } from '@repo/ui/memory-graph/constants'; -import type { DocumentsWithMemoriesResponseSchema } from '@repo/validation/api'; -import { Badge } from '@ui/components/badge'; -import { Brain, Calendar, CircleUserRound, ExternalLink, List, Sparkles } from 'lucide-react'; -import { memo } from 'react'; -import type { z } from 'zod'; -import { formatDate, getSourceUrl } from '.'; -import { Label1Regular } from '@ui/text/label/label-1-regular'; + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@repo/ui/components/tabs"; +import { colors } from "@repo/ui/memory-graph/constants"; +import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"; +import { Badge } from "@ui/components/badge"; +import { Label1Regular } from "@ui/text/label/label-1-regular"; +import { + Brain, + Calendar, + CircleUserRound, + ExternalLink, + List, + Sparkles, +} from "lucide-react"; +import { memo } from "react"; +import type { z } from "zod"; +import { getDocumentIcon } from "@/lib/document-icon"; +import { formatDate, getSourceUrl } from "."; type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>; -type DocumentWithMemories = DocumentsResponse['documents'][0]; -type MemoryEntry = DocumentWithMemories['memoryEntries'][0]; +type DocumentWithMemories = DocumentsResponse["documents"][0]; +type MemoryEntry = DocumentWithMemories["memoryEntries"][0]; const formatDocumentType = (type: string) => { - // Special case for PDF - if (type.toLowerCase() === 'pdf') return 'PDF'; + // Special case for PDF + if (type.toLowerCase() === "pdf") return "PDF"; - // Replace underscores with spaces and capitalize each word - return type - .split('_') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) - .join(' '); + // Replace underscores with spaces and capitalize each word + return type + .split("_") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(" "); }; const MemoryDetailItem = memo(({ memory }: { memory: MemoryEntry }) => { - return ( - <button - className="p-4 rounded-lg transition-all relative overflow-hidden cursor-pointer" - style={{ - backgroundColor: memory.isLatest - ? colors.memory.primary - : 'rgba(255, 255, 255, 0.02)', - }} - tabIndex={0} - type="button" - > - <div className="flex items-start gap-2 relative z-10"> - <div - className="p-1 rounded" - style={{ - backgroundColor: memory.isLatest - ? colors.memory.secondary - : 'transparent', - }} - > - <Brain - className={`w-4 h-4 flex-shrink-0 transition-all ${ - memory.isLatest ? 'text-blue-400' : 'text-blue-400/50' - }`} - /> - </div> - <div className="flex-1 space-y-2"> - <Label1Regular - className="text-sm leading-relaxed text-left" - style={{ color: colors.text.primary }} - > - {memory.memory} - </Label1Regular> - <div className="flex gap-2 justify-between"> - <div - className="flex items-center gap-4 text-xs" - style={{ color: colors.text.muted }} - > - <span className="flex items-center gap-1"> - <Calendar className="w-3 h-3" /> - {formatDate(memory.createdAt)} - </span> - <span className="font-mono">v{memory.version}</span> - {memory.sourceRelevanceScore && ( - <span - className="flex items-center gap-1" - style={{ - color: - memory.sourceRelevanceScore > 70 - ? colors.accent.emerald - : colors.text.muted, - }} - > - <Sparkles className="w-3 h-3" /> - {memory.sourceRelevanceScore}% - </span> - )} - </div> - <div className="flex items-center gap-2 flex-wrap"> - {memory.isForgotten && ( - <Badge - className="text-xs border-red-500/30 backdrop-blur-sm" - style={{ - backgroundColor: colors.status.forgotten, - color: '#dc2626', - backdropFilter: 'blur(4px)', - WebkitBackdropFilter: 'blur(4px)', - }} - variant="destructive" - > - Forgotten - </Badge> - )} - {memory.isLatest && ( - <Badge - className="text-xs" - style={{ - backgroundColor: colors.memory.secondary, - color: colors.text.primary, - backdropFilter: 'blur(4px)', - WebkitBackdropFilter: 'blur(4px)', - }} - variant="default" - > - Latest - </Badge> - )} - {memory.forgetAfter && ( - <Badge - className="text-xs backdrop-blur-sm" - style={{ - color: colors.status.expiring, - backgroundColor: 'rgba(251, 165, 36, 0.1)', - backdropFilter: 'blur(4px)', - WebkitBackdropFilter: 'blur(4px)', - }} - variant="outline" - > - Expires: {formatDate(memory.forgetAfter)} - </Badge> - )} - </div> - </div> - </div> - </div> - </button> - ); + return ( + <button + className="p-4 rounded-lg transition-all relative overflow-hidden cursor-pointer" + style={{ + backgroundColor: memory.isLatest + ? colors.memory.primary + : "rgba(255, 255, 255, 0.02)", + }} + tabIndex={0} + type="button" + > + <div className="flex items-start gap-2 relative z-10"> + <div + className="p-1 rounded" + style={{ + backgroundColor: memory.isLatest + ? colors.memory.secondary + : "transparent", + }} + > + <Brain + className={`w-4 h-4 flex-shrink-0 transition-all ${ + memory.isLatest ? "text-blue-400" : "text-blue-400/50" + }`} + /> + </div> + <div className="flex-1 space-y-2"> + <Label1Regular + className="text-sm leading-relaxed text-left" + style={{ color: colors.text.primary }} + > + {memory.memory} + </Label1Regular> + <div className="flex gap-2 justify-between"> + <div + className="flex items-center gap-4 text-xs" + style={{ color: colors.text.muted }} + > + <span className="flex items-center gap-1"> + <Calendar className="w-3 h-3" /> + {formatDate(memory.createdAt)} + </span> + <span className="font-mono">v{memory.version}</span> + {memory.sourceRelevanceScore && ( + <span + className="flex items-center gap-1" + style={{ + color: + memory.sourceRelevanceScore > 70 + ? colors.accent.emerald + : colors.text.muted, + }} + > + <Sparkles className="w-3 h-3" /> + {memory.sourceRelevanceScore}% + </span> + )} + </div> + <div className="flex items-center gap-2 flex-wrap"> + {memory.isForgotten && ( + <Badge + className="text-xs border-red-500/30 backdrop-blur-sm" + style={{ + backgroundColor: colors.status.forgotten, + color: "#dc2626", + backdropFilter: "blur(4px)", + WebkitBackdropFilter: "blur(4px)", + }} + variant="destructive" + > + Forgotten + </Badge> + )} + {memory.isLatest && ( + <Badge + className="text-xs" + style={{ + backgroundColor: colors.memory.secondary, + color: colors.text.primary, + backdropFilter: "blur(4px)", + WebkitBackdropFilter: "blur(4px)", + }} + variant="default" + > + Latest + </Badge> + )} + {memory.forgetAfter && ( + <Badge + className="text-xs backdrop-blur-sm" + style={{ + color: colors.status.expiring, + backgroundColor: "rgba(251, 165, 36, 0.1)", + backdropFilter: "blur(4px)", + WebkitBackdropFilter: "blur(4px)", + }} + variant="outline" + > + Expires: {formatDate(memory.forgetAfter)} + </Badge> + )} + </div> + </div> + </div> + </div> + </button> + ); }); export const MemoryDetail = memo( - ({ - document, - isOpen, - onClose, - isMobile, - }: { - document: DocumentWithMemories | null; - isOpen: boolean; - onClose: () => void; - isMobile: boolean; - }) => { - if (!document) return null; + ({ + document, + isOpen, + onClose, + isMobile, + }: { + document: DocumentWithMemories | null; + isOpen: boolean; + onClose: () => void; + isMobile: boolean; + }) => { + if (!document) return null; + + const activeMemories = (document.memoryEntries || []).filter( + (m) => !m.isForgotten, + ); + const forgottenMemories = (document.memoryEntries || []).filter( + (m) => m.isForgotten, + ); - const activeMemories = document.memoryEntries.filter((m) => !m.isForgotten); - const forgottenMemories = document.memoryEntries.filter( - (m) => m.isForgotten - ); + const HeaderContent = ({ + TitleComponent, + }: { + TitleComponent: typeof SheetTitle | typeof DrawerTitle; + }) => ( + <div className="flex items-start justify-between gap-2"> + <div className="flex items-start gap-3 flex-1"> + <div + className="p-2 rounded-lg" + style={{ + backgroundColor: colors.background.secondary, + }} + > + {getDocumentIcon(document.type, "w-5 h-5")} + </div> + <div className="flex-1"> + <TitleComponent style={{ color: colors.text.primary }}> + {document.title || "Untitled Document"} + </TitleComponent> + <div + className="flex items-center gap-2 mt-1 text-xs" + style={{ color: colors.text.muted }} + > + <span>{formatDocumentType(document.type)}</span> + <span>•</span> + <span>{formatDate(document.createdAt)}</span> + {document.url && ( + <> + <span>•</span> + <button + className="flex items-center gap-1 transition-all hover:gap-2" + onClick={() => { + const sourceUrl = getSourceUrl(document); + window.open(sourceUrl ?? undefined, "_blank"); + }} + style={{ color: colors.accent.primary }} + type="button" + > + View source + <ExternalLink className="w-3 h-3" /> + </button> + </> + )} + </div> + </div> + </div> + </div> + ); - const HeaderContent = ({ - TitleComponent, - }: { - TitleComponent: typeof SheetTitle | typeof DrawerTitle; - }) => ( - <div className="flex items-start justify-between gap-2"> - <div className="flex items-start gap-3 flex-1"> - <div - className="p-2 rounded-lg" - style={{ - backgroundColor: colors.background.secondary, - }} - > - {getDocumentIcon(document.type, 'w-5 h-5')} - </div> - <div className="flex-1"> - <TitleComponent style={{ color: colors.text.primary }}> - {document.title || 'Untitled Document'} - </TitleComponent> - <div - className="flex items-center gap-2 mt-1 text-xs" - style={{ color: colors.text.muted }} - > - <span>{formatDocumentType(document.type)}</span> - <span>•</span> - <span>{formatDate(document.createdAt)}</span> - {document.url && ( - <> - <span>•</span> - <button - className="flex items-center gap-1 transition-all hover:gap-2" - onClick={() => { - const sourceUrl = getSourceUrl(document); - window.open(sourceUrl ?? undefined, '_blank'); - }} - style={{ color: colors.accent.primary }} - type="button" - > - View source - <ExternalLink className="w-3 h-3" /> - </button> - </> - )} - </div> - </div> - </div> - </div> - ); + const ContentAndSummarySection = () => { + const hasContent = document.content && document.content.trim().length > 0; + const hasSummary = document.summary && document.summary.trim().length > 0; - const ContentAndSummarySection = () => { - const hasContent = document.content && document.content.trim().length > 0; - const hasSummary = document.summary && document.summary.trim().length > 0; - - if (!hasContent && !hasSummary) return null; + if (!hasContent && !hasSummary) return null; - const defaultTab = hasContent ? 'content' : 'summary'; + const defaultTab = hasContent ? "content" : "summary"; - return ( - <div className="mt-4"> - <Tabs defaultValue={defaultTab} className="w-full"> - <TabsList - className={`grid w-full bg-white/5 border border-white/10 h-11 ${ - hasContent && hasSummary ? 'grid-cols-2' : 'grid-cols-1' - }`} - > - {hasContent && ( - <TabsTrigger - value="content" - className="text-xs bg-transparent h-8" - style={{ color: colors.text.secondary }} - > - <CircleUserRound className="w-3 h-3" /> - Original Content - </TabsTrigger> - )} - {hasSummary && ( - <TabsTrigger - value="summary" - className="text-xs flex items-center gap-1 bg-transparent h-8" - style={{ color: colors.text.secondary }} - > - <List className="w-3 h-3" /> - Summary - </TabsTrigger> - )} - </TabsList> + return ( + <div className="mt-4"> + <Tabs className="w-full" defaultValue={defaultTab}> + <TabsList + className={`grid w-full bg-white/5 border border-white/10 h-11 ${ + hasContent && hasSummary ? "grid-cols-2" : "grid-cols-1" + }`} + > + {hasContent && ( + <TabsTrigger + className="text-xs bg-transparent h-8" + style={{ color: colors.text.secondary }} + value="content" + > + <CircleUserRound className="w-3 h-3" /> + Original Content + </TabsTrigger> + )} + {hasSummary && ( + <TabsTrigger + className="text-xs flex items-center gap-1 bg-transparent h-8" + style={{ color: colors.text.secondary }} + value="summary" + > + <List className="w-3 h-3" /> + Summary + </TabsTrigger> + )} + </TabsList> - {hasContent && ( - <TabsContent value="content" className="mt-3"> - <div className="p-3 rounded-lg max-h-48 overflow-y-auto custom-scrollbar bg-white/[0.03] border border-white/[0.08]"> - <p - className="text-sm leading-relaxed whitespace-pre-wrap" - style={{ color: colors.text.primary }} - > - {document.content} - </p> - </div> - </TabsContent> - )} + {hasContent && ( + <TabsContent className="mt-3" value="content"> + <div className="p-3 rounded-lg max-h-48 overflow-y-auto custom-scrollbar bg-white/[0.03] border border-white/[0.08]"> + <p + className="text-sm leading-relaxed whitespace-pre-wrap" + style={{ color: colors.text.primary }} + > + {document.content} + </p> + </div> + </TabsContent> + )} - {hasSummary && ( - <TabsContent value="summary" className="mt-3"> - <div className="p-3 rounded-lg max-h-48 overflow-y-auto custom-scrollbar bg-indigo-500/5 border border-indigo-500/15"> - <p - className="text-sm leading-relaxed whitespace-pre-wrap" - style={{ color: colors.text.muted }} - > - {document.summary} - </p> - </div> - </TabsContent> - )} - </Tabs> - </div> - ); - }; + {hasSummary && ( + <TabsContent className="mt-3" value="summary"> + <div className="p-3 rounded-lg max-h-48 overflow-y-auto custom-scrollbar bg-indigo-500/5 border border-indigo-500/15"> + <p + className="text-sm leading-relaxed whitespace-pre-wrap" + style={{ color: colors.text.muted }} + > + {document.summary} + </p> + </div> + </TabsContent> + )} + </Tabs> + </div> + ); + }; - const MemoryContent = () => ( - <div className="space-y-6 px-6"> - {activeMemories.length > 0 && ( - <div> - <div - className="text-sm font-medium mb-2 flex items-start gap-2 py-2" - style={{ - color: colors.text.secondary, - }} - > - Active Memories ({activeMemories.length}) - </div> - <div className="space-y-3"> - {activeMemories.map((memory) => ( - <div - key={memory.id} - > - <MemoryDetailItem memory={memory} /> - </div> - ))} - </div> - </div> - )} + const MemoryContent = () => ( + <div className="space-y-6 px-6"> + {activeMemories.length > 0 && ( + <div> + <div + className="text-sm font-medium mb-2 flex items-start gap-2 py-2" + style={{ + color: colors.text.secondary, + }} + > + Active Memories ({activeMemories.length}) + </div> + <div className="space-y-3"> + {activeMemories.map((memory) => ( + <div key={memory.id}> + <MemoryDetailItem memory={memory} /> + </div> + ))} + </div> + </div> + )} - {forgottenMemories.length > 0 && ( - <div> - <div - className="text-sm font-medium mb-4 px-3 py-2 rounded-lg opacity-60" - style={{ - color: colors.text.muted, - backgroundColor: 'rgba(255, 255, 255, 0.02)', - }} - > - Forgotten Memories ({forgottenMemories.length}) - </div> - <div className="space-y-3 opacity-40"> - {forgottenMemories.map((memory) => ( - <MemoryDetailItem key={memory.id} memory={memory} /> - ))} - </div> - </div> - )} + {forgottenMemories.length > 0 && ( + <div> + <div + className="text-sm font-medium mb-4 px-3 py-2 rounded-lg opacity-60" + style={{ + color: colors.text.muted, + backgroundColor: "rgba(255, 255, 255, 0.02)", + }} + > + Forgotten Memories ({forgottenMemories.length}) + </div> + <div className="space-y-3 opacity-40"> + {forgottenMemories.map((memory) => ( + <MemoryDetailItem key={memory.id} memory={memory} /> + ))} + </div> + </div> + )} - {activeMemories.length === 0 && forgottenMemories.length === 0 && ( - <div - className="text-center py-12 rounded-lg" - style={{ - backgroundColor: 'rgba(255, 255, 255, 0.02)', - }} - > - <Brain - className="w-12 h-12 mx-auto mb-4 opacity-30" - style={{ color: colors.text.muted }} - /> - <p style={{ color: colors.text.muted }}> - No memories found for this document - </p> - </div> - )} - </div> - ); + {activeMemories.length === 0 && forgottenMemories.length === 0 && ( + <div + className="text-center py-12 rounded-lg" + style={{ + backgroundColor: "rgba(255, 255, 255, 0.02)", + }} + > + <Brain + className="w-12 h-12 mx-auto mb-4 opacity-30" + style={{ color: colors.text.muted }} + /> + <p style={{ color: colors.text.muted }}> + No memories found for this document + </p> + </div> + )} + </div> + ); - if (isMobile) { - return ( - <Drawer onOpenChange={onClose} open={isOpen}> - <DrawerContent - className="border-0 p-0 overflow-hidden max-h-[90vh]" - style={{ - backgroundColor: colors.background.secondary, - borderTop: `1px solid ${colors.document.border}`, - backdropFilter: 'blur(20px)', - WebkitBackdropFilter: 'blur(20px)', - }} - > - {/* Header section with glass effect */} - <div - className="p-4 relative border-b" - style={{ - backgroundColor: 'rgba(255, 255, 255, 0.02)', - borderBottom: `1px solid ${colors.document.border}`, - }} - > - <DrawerHeader className="pb-0 px-0 text-left"> - <HeaderContent TitleComponent={DrawerTitle} /> - </DrawerHeader> + if (isMobile) { + return ( + <Drawer onOpenChange={onClose} open={isOpen}> + <DrawerContent + className="border-0 p-0 overflow-hidden max-h-[90vh]" + style={{ + backgroundColor: colors.background.secondary, + borderTop: `1px solid ${colors.document.border}`, + backdropFilter: "blur(20px)", + WebkitBackdropFilter: "blur(20px)", + }} + > + {/* Header section with glass effect */} + <div + className="p-4 relative border-b" + style={{ + backgroundColor: "rgba(255, 255, 255, 0.02)", + borderBottom: `1px solid ${colors.document.border}`, + }} + > + <DrawerHeader className="pb-0 px-0 text-left"> + <HeaderContent TitleComponent={DrawerTitle} /> + </DrawerHeader> - <ContentAndSummarySection /> - </div> + <ContentAndSummarySection /> + </div> - <div className="flex-1 memory-drawer-scroll overflow-y-auto"> - <MemoryContent /> - </div> - </DrawerContent> - </Drawer> - ); - } + <div className="flex-1 memory-drawer-scroll overflow-y-auto"> + <MemoryContent /> + </div> + </DrawerContent> + </Drawer> + ); + } - return ( - <Sheet onOpenChange={onClose} open={isOpen}> - <SheetContent - className="w-full sm:max-w-2xl border-0 p-0 overflow-hidden" - style={{ - backgroundColor: colors.background.secondary, - }} - > - <div - className="p-6 relative" - style={{ - backgroundColor: 'rgba(255, 255, 255, 0.02)', - }} - > - <SheetHeader className="pb-0"> - <HeaderContent TitleComponent={SheetTitle} /> - </SheetHeader> + return ( + <Sheet onOpenChange={onClose} open={isOpen}> + <SheetContent + className="w-full sm:max-w-2xl border-0 p-0 overflow-hidden" + style={{ + backgroundColor: colors.background.secondary, + }} + > + <div + className="p-6 relative" + style={{ + backgroundColor: "rgba(255, 255, 255, 0.02)", + }} + > + <SheetHeader className="pb-0"> + <HeaderContent TitleComponent={SheetTitle} /> + </SheetHeader> - <ContentAndSummarySection /> - </div> + <ContentAndSummarySection /> + </div> - <div className="h-[calc(100vh-200px)] memory-sheet-scroll overflow-y-auto"> - <MemoryContent /> - </div> - </SheetContent> - </Sheet> - ); - } + <div className="h-[calc(100vh-200px)] memory-sheet-scroll overflow-y-auto"> + <MemoryContent /> + </div> + </SheetContent> + </Sheet> + ); + }, ); diff --git a/apps/web/components/memory-list-view.tsx b/apps/web/components/memory-list-view.tsx index b91e2562..af929112 100644 --- a/apps/web/components/memory-list-view.tsx +++ b/apps/web/components/memory-list-view.tsx @@ -1,9 +1,8 @@ -"use client" +"use client"; -import { useIsMobile } from "@hooks/use-mobile" -import { cn } from "@lib/utils" -import { Badge } from "@repo/ui/components/badge" -import { Card, CardContent, CardHeader } from "@repo/ui/components/card" +import { useIsMobile } from "@hooks/use-mobile"; +import { useDeleteDocument } from "@lib/queries"; +import { cn } from "@lib/utils"; import { AlertDialog, AlertDialogAction, @@ -14,43 +13,43 @@ import { AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, -} from "@repo/ui/components/alert-dialog" -import { colors } from "@repo/ui/memory-graph/constants" -import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api" -import { useVirtualizer } from "@tanstack/react-virtual" -import { Brain, ExternalLink, Sparkles, Trash2 } from "lucide-react" -import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react" -import type { z } from "zod" -import useResizeObserver from "@/hooks/use-resize-observer" -import { analytics } from "@/lib/analytics" -import { useDeleteDocument } from "@lib/queries" -import { useProject } from "@/stores" +} from "@repo/ui/components/alert-dialog"; +import { Badge } from "@repo/ui/components/badge"; +import { Card, CardContent, CardHeader } from "@repo/ui/components/card"; +import { colors } from "@repo/ui/memory-graph/constants"; +import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"; +import { useVirtualizer } from "@tanstack/react-virtual"; +import { Brain, ExternalLink, Sparkles, Trash2 } from "lucide-react"; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import type { z } from "zod"; +import useResizeObserver from "@/hooks/use-resize-observer"; +import { analytics } from "@/lib/analytics"; +import { getDocumentIcon } from "@/lib/document-icon"; +import { useProject } from "@/stores"; +import { formatDate, getSourceUrl } from "./memories"; +import { MemoryDetail } from "./memories/memory-detail"; -import { MemoryDetail } from "./memories/memory-detail" -import { getDocumentIcon } from "@/lib/document-icon" -import { formatDate, getSourceUrl } from "./memories" - -type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema> -type DocumentWithMemories = DocumentsResponse["documents"][0] +type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>; +type DocumentWithMemories = DocumentsResponse["documents"][0]; interface MemoryListViewProps { - children?: React.ReactNode - documents: DocumentWithMemories[] - isLoading: boolean - isLoadingMore: boolean - error: Error | null - totalLoaded: number - hasMore: boolean - loadMoreDocuments: () => Promise<void> + children?: React.ReactNode; + documents: DocumentWithMemories[]; + isLoading: boolean; + isLoadingMore: boolean; + error: Error | null; + totalLoaded: number; + hasMore: boolean; + loadMoreDocuments: () => Promise<void>; } const GreetingMessage = memo(() => { const getGreeting = () => { - const hour = new Date().getHours() - if (hour < 12) return "Good morning" - if (hour < 17) return "Good afternoon" - return "Good evening" - } + const hour = new Date().getHours(); + if (hour < 12) return "Good morning"; + if (hour < 17) return "Good afternoon"; + return "Good evening"; + }; return ( <div className="flex items-center gap-3 mb-3 px-4 md:mb-6 md:mt-3"> @@ -66,8 +65,8 @@ const GreetingMessage = memo(() => { </p> </div> </div> - ) -}) + ); +}); const DocumentCard = memo( ({ @@ -75,21 +74,23 @@ const DocumentCard = memo( onOpenDetails, onDelete, }: { - document: DocumentWithMemories - onOpenDetails: (document: DocumentWithMemories) => void - onDelete: (document: DocumentWithMemories) => void + document: DocumentWithMemories; + onOpenDetails: (document: DocumentWithMemories) => void; + onDelete: (document: DocumentWithMemories) => void; }) => { - const activeMemories = document.memoryEntries.filter((m) => !m.isForgotten) - const forgottenMemories = document.memoryEntries.filter( + const activeMemories = (document.memoryEntries || []).filter( + (m) => !m.isForgotten, + ); + const forgottenMemories = (document.memoryEntries || []).filter( (m) => m.isForgotten, - ) + ); return ( <Card className="h-full mx-4 p-4 transition-all cursor-pointer group relative overflow-hidden border-0 gap-2 md:w-full" onClick={() => { - analytics.documentCardClicked() - onOpenDetails(document) + analytics.documentCardClicked(); + onOpenDetails(document); }} style={{ backgroundColor: colors.document.primary, @@ -112,9 +113,9 @@ const DocumentCard = memo( <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") + e.stopPropagation(); + const sourceUrl = getSourceUrl(document); + window.open(sourceUrl ?? undefined, "_blank"); }} style={{ backgroundColor: "rgba(255, 255, 255, 0.05)", @@ -141,7 +142,20 @@ const DocumentCard = memo( )} <div className="flex items-center justify-between"> <div className="flex items-center gap-2 flex-wrap"> - {activeMemories.length > 0 && ( + {(document as any).isOptimistic && ( + <Badge + className="text-xs text-blue-200 animate-pulse" + style={{ + backgroundColor: "rgba(59, 130, 246, 0.2)", + borderColor: "rgba(59, 130, 246, 0.3)", + }} + variant="outline" + > + <Sparkles className="w-3 h-3 mr-1" /> + Supermemory updating... + </Badge> + )} + {!(document as any).isOptimistic && activeMemories.length > 0 && ( <Badge className="text-xs text-accent-foreground" style={{ @@ -173,7 +187,7 @@ const DocumentCard = memo( <button className="opacity-0 group-hover:opacity-100 transition-opacity p-1.5 rounded-md hover:bg-red-500/20" onClick={(e) => { - e.stopPropagation() + e.stopPropagation(); }} style={{ color: colors.text.muted, @@ -194,7 +208,7 @@ const DocumentCard = memo( <AlertDialogFooter> <AlertDialogCancel onClick={(e) => { - e.stopPropagation() + e.stopPropagation(); }} > Cancel @@ -202,8 +216,8 @@ const DocumentCard = memo( <AlertDialogAction className="bg-red-600 hover:bg-red-700 text-white" onClick={(e) => { - e.stopPropagation() - onDelete(document) + e.stopPropagation(); + onDelete(document); }} > Delete @@ -214,9 +228,9 @@ const DocumentCard = memo( </div> </CardContent> </Card> - ) + ); }, -) +); export const MemoryListView = ({ children, @@ -227,86 +241,86 @@ export const MemoryListView = ({ hasMore, loadMoreDocuments, }: MemoryListViewProps) => { - const [selectedSpace, _] = useState<string>("all") + const [selectedSpace, _] = useState<string>("all"); const [selectedDocument, setSelectedDocument] = - useState<DocumentWithMemories | null>(null) - const [isDetailOpen, setIsDetailOpen] = useState(false) - const parentRef = useRef<HTMLDivElement>(null) - const containerRef = useRef<HTMLDivElement>(null) - const isMobile = useIsMobile() - const { selectedProject } = useProject() - const deleteDocumentMutation = useDeleteDocument(selectedProject) + useState<DocumentWithMemories | null>(null); + const [isDetailOpen, setIsDetailOpen] = useState(false); + const parentRef = useRef<HTMLDivElement>(null); + const containerRef = useRef<HTMLDivElement>(null); + const isMobile = useIsMobile(); + const { selectedProject } = useProject(); + const deleteDocumentMutation = useDeleteDocument(selectedProject); - const gap = 14 + const gap = 14; const handleDeleteDocument = useCallback( (document: DocumentWithMemories) => { - deleteDocumentMutation.mutate(document.id) + deleteDocumentMutation.mutate(document.id); }, [deleteDocumentMutation], - ) + ); - const { width: containerWidth } = useResizeObserver(containerRef) - const columnWidth = isMobile ? containerWidth : 320 + const { width: containerWidth } = useResizeObserver(containerRef); + const columnWidth = isMobile ? containerWidth : 320; const columns = Math.max( 1, Math.floor((containerWidth + gap) / (columnWidth + gap)), - ) + ); // Filter documents based on selected space const filteredDocuments = useMemo(() => { - if (!documents) return [] + if (!documents) return []; if (selectedSpace === "all") { - return documents + return documents; } return documents .map((doc) => ({ ...doc, - memoryEntries: doc.memoryEntries.filter( + memoryEntries: (doc.memoryEntries || []).filter( (memory) => (memory.spaceContainerTag ?? memory.spaceId) === selectedSpace, ), })) - .filter((doc) => doc.memoryEntries.length > 0) - }, [documents, selectedSpace]) + .filter((doc) => (doc.memoryEntries || []).length > 0); + }, [documents, selectedSpace]); const handleOpenDetails = useCallback((document: DocumentWithMemories) => { - analytics.memoryDetailOpened() - setSelectedDocument(document) - setIsDetailOpen(true) - }, []) + analytics.memoryDetailOpened(); + setSelectedDocument(document); + setIsDetailOpen(true); + }, []); const handleCloseDetails = useCallback(() => { - setIsDetailOpen(false) - setTimeout(() => setSelectedDocument(null), 300) - }, []) + setIsDetailOpen(false); + setTimeout(() => setSelectedDocument(null), 300); + }, []); const virtualItems = useMemo(() => { - const items = [] + const items = []; for (let i = 0; i < filteredDocuments.length; i += columns) { - items.push(filteredDocuments.slice(i, i + columns)) + items.push(filteredDocuments.slice(i, i + columns)); } - return items - }, [filteredDocuments, columns]) + return items; + }, [filteredDocuments, columns]); const virtualizer = useVirtualizer({ count: virtualItems.length, getScrollElement: () => parentRef.current, overscan: 5, estimateSize: () => 200, - }) + }); useEffect(() => { - const [lastItem] = [...virtualizer.getVirtualItems()].reverse() + const [lastItem] = [...virtualizer.getVirtualItems()].reverse(); if (!lastItem || !hasMore || isLoadingMore) { - return + return; } if (lastItem.index >= virtualItems.length - 1) { - loadMoreDocuments() + loadMoreDocuments(); } }, [ hasMore, @@ -314,7 +328,7 @@ export const MemoryListView = ({ loadMoreDocuments, virtualizer.getVirtualItems(), virtualItems.length, - ]) + ]); // Always render with consistent structure return ( @@ -355,8 +369,8 @@ export const MemoryListView = ({ </div> ) : ( <div - ref={parentRef} className="h-full overflow-auto mt-20 custom-scrollbar" + ref={parentRef} > <GreetingMessage /> @@ -367,15 +381,15 @@ export const MemoryListView = ({ }} > {virtualizer.getVirtualItems().map((virtualRow) => { - const rowItems = virtualItems[virtualRow.index] - if (!rowItems) return null + const rowItems = virtualItems[virtualRow.index]; + if (!rowItems) return null; return ( <div - key={virtualRow.key} + className="absolute top-0 left-0 w-full" data-index={virtualRow.index} + key={virtualRow.key} ref={virtualizer.measureElement} - className="absolute top-0 left-0 w-full" style={{ transform: `translateY(${virtualRow.start + virtualRow.index * gap}px)`, }} @@ -389,15 +403,15 @@ export const MemoryListView = ({ > {rowItems.map((document, columnIndex) => ( <DocumentCard - key={`${document.id}-${virtualRow.index}-${columnIndex}`} document={document} - onOpenDetails={handleOpenDetails} + key={`${document.id}-${virtualRow.index}-${columnIndex}`} onDelete={handleDeleteDocument} + onOpenDetails={handleOpenDetails} /> ))} </div> </div> - ) + ); })} </div> @@ -417,10 +431,10 @@ export const MemoryListView = ({ <MemoryDetail document={selectedDocument} + isMobile={isMobile} isOpen={isDetailOpen} onClose={handleCloseDetails} - isMobile={isMobile} /> </> - ) -} + ); +}; diff --git a/apps/web/components/menu.tsx b/apps/web/components/menu.tsx index db012ab7..1d42c8d8 100644 --- a/apps/web/components/menu.tsx +++ b/apps/web/components/menu.tsx @@ -1,28 +1,28 @@ -"use client" +"use client"; -import { useIsMobile } from "@hooks/use-mobile" +import { useIsMobile } from "@hooks/use-mobile"; import { fetchConsumerProProduct, fetchMemoriesFeature, -} from "@repo/lib/queries" -import { Button } from "@repo/ui/components/button" -import { ConnectAIModal } from "./connect-ai-modal" -import { HeadingH2Bold } from "@repo/ui/text/heading/heading-h2-bold" -import { GlassMenuEffect } from "@ui/other/glass-effect" -import { useCustomer } from "autumn-js/react" -import { MessageSquareMore, Plus, Puzzle, User, X } from "lucide-react" -import { AnimatePresence, LayoutGroup, motion } from "motion/react" -import { useRouter, useSearchParams } from "next/navigation" -import { useCallback, useEffect, useState } from "react" -import { Drawer } from "vaul" -import { useMobilePanel } from "@/lib/mobile-panel-context" -import { TOUR_STEP_IDS } from "@/lib/tour-constants" -import { useChatOpen } from "@/stores" -import { ProjectSelector } from "./project-selector" -import { useTour } from "./tour" -import { AddMemoryExpandedView, AddMemoryView } from "./views/add-memory" -import { IntegrationsView } from "./views/integrations" -import { ProfileView } from "./views/profile" +} from "@repo/lib/queries"; +import { Button } from "@repo/ui/components/button"; +import { HeadingH2Bold } from "@repo/ui/text/heading/heading-h2-bold"; +import { GlassMenuEffect } from "@ui/other/glass-effect"; +import { useCustomer } from "autumn-js/react"; +import { MessageSquareMore, Plus, Puzzle, User, X } from "lucide-react"; +import { AnimatePresence, LayoutGroup, motion } from "motion/react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useCallback, useEffect, useState } from "react"; +import { Drawer } from "vaul"; +import { useMobilePanel } from "@/lib/mobile-panel-context"; +import { TOUR_STEP_IDS } from "@/lib/tour-constants"; +import { useChatOpen } from "@/stores"; +import { ConnectAIModal } from "./connect-ai-modal"; +import { ProjectSelector } from "./project-selector"; +import { useTour } from "./tour"; +import { AddMemoryExpandedView, AddMemoryView } from "./views/add-memory"; +import { IntegrationsView } from "./views/integrations"; +import { ProfileView } from "./views/profile"; const MCPIcon = ({ className }: { className?: string }) => { return ( @@ -37,13 +37,13 @@ const MCPIcon = ({ className }: { className?: string }) => { <path d="M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z" /> <path d="M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z" /> </svg> - ) -} + ); +}; function Menu({ id }: { id?: string }) { - const router = useRouter() - const searchParams = useSearchParams() - const openParam = searchParams.get("open") + const router = useRouter(); + const searchParams = useSearchParams(); + const openParam = searchParams.get("open"); // Valid view names that can be opened via URL parameter const validViews = [ @@ -52,52 +52,52 @@ function Menu({ id }: { id?: string }) { "projects", "profile", "integrations", - ] as const - type ValidView = (typeof validViews)[number] + ] as const; + type ValidView = (typeof validViews)[number]; - const [isHovered, setIsHovered] = useState(false) + const [isHovered, setIsHovered] = useState(false); const [expandedView, setExpandedView] = useState< "addUrl" | "mcp" | "projects" | "profile" | "integrations" | null - >(null) - const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false) - const [isCollapsing, setIsCollapsing] = useState(false) - const [showAddMemoryView, setShowAddMemoryView] = useState(false) - const [showConnectAIModal, setShowConnectAIModal] = useState(false) - const isMobile = useIsMobile() - const { activePanel, setActivePanel } = useMobilePanel() - const { setMenuExpanded } = useTour() - const autumn = useCustomer() - const { setIsOpen } = useChatOpen() + >(null); + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + const [isCollapsing, setIsCollapsing] = useState(false); + const [showAddMemoryView, setShowAddMemoryView] = useState(false); + const [showConnectAIModal, setShowConnectAIModal] = useState(false); + const isMobile = useIsMobile(); + const { activePanel, setActivePanel } = useMobilePanel(); + const { setMenuExpanded } = useTour(); + const autumn = useCustomer(); + const { setIsOpen } = useChatOpen(); - const { data: memoriesCheck } = fetchMemoriesFeature(autumn) + const { data: memoriesCheck } = fetchMemoriesFeature(autumn); - const memoriesUsed = memoriesCheck?.usage ?? 0 - const memoriesLimit = memoriesCheck?.included_usage ?? 0 + const memoriesUsed = memoriesCheck?.usage ?? 0; + const memoriesLimit = memoriesCheck?.included_usage ?? 0; - const { data: proCheck } = fetchConsumerProProduct(autumn) + const { data: proCheck } = fetchConsumerProProduct(autumn); useEffect(() => { if (memoriesCheck) { - console.log({ memoriesCheck }) + console.log({ memoriesCheck }); } if (proCheck) { - console.log({ proCheck }) + console.log({ proCheck }); } - }, [memoriesCheck, proCheck]) + }, [memoriesCheck, proCheck]); // Function to clear the 'open' parameter from URL const clearOpenParam = useCallback(() => { - const newSearchParams = new URLSearchParams(searchParams.toString()) - newSearchParams.delete("open") - const newUrl = `${window.location.pathname}${newSearchParams.toString() ? `?${newSearchParams.toString()}` : ""}` - router.replace(newUrl) - }, [searchParams, router]) + const newSearchParams = new URLSearchParams(searchParams.toString()); + newSearchParams.delete("open"); + const newUrl = `${window.location.pathname}${newSearchParams.toString() ? `?${newSearchParams.toString()}` : ""}`; + router.replace(newUrl); + }, [searchParams, router]); - const isProUser = proCheck?.allowed ?? false + const isProUser = proCheck?.allowed ?? false; const shouldShowLimitWarning = - !isProUser && memoriesUsed >= memoriesLimit * 0.8 + !isProUser && memoriesUsed >= memoriesLimit * 0.8; // Map menu item keys to tour IDs const menuItemTourIds: Record<string, string> = { @@ -105,7 +105,7 @@ function Menu({ id }: { id?: string }) { projects: TOUR_STEP_IDS.MENU_PROJECTS, mcp: TOUR_STEP_IDS.MENU_MCP, integrations: "", // No tour ID for integrations yet - } + }; const menuItems = [ { @@ -138,70 +138,70 @@ function Menu({ id }: { id?: string }) { key: "profile" as const, disabled: false, }, - ] + ]; const handleMenuItemClick = ( key: "chat" | "addUrl" | "mcp" | "projects" | "profile" | "integrations", ) => { if (key === "chat") { - setIsOpen(true) - setIsMobileMenuOpen(false) + setIsOpen(true); + setIsMobileMenuOpen(false); if (isMobile) { - setActivePanel("chat") + setActivePanel("chat"); } } else if (key === "mcp") { // Open ConnectAIModal directly for MCP - setIsMobileMenuOpen(false) - setExpandedView(null) - setShowConnectAIModal(true) + setIsMobileMenuOpen(false); + setExpandedView(null); + setShowConnectAIModal(true); } else { if (expandedView === key) { - setIsCollapsing(true) - setExpandedView(null) + setIsCollapsing(true); + setExpandedView(null); } else if (key === "addUrl") { - setShowAddMemoryView(true) - setExpandedView(null) + setShowAddMemoryView(true); + setExpandedView(null); } else { - setExpandedView(key) + setExpandedView(key); } if (isMobile) { - setActivePanel("menu") + setActivePanel("menu"); } } - } + }; // Handle initial view opening based on URL parameter useEffect(() => { if (openParam) { if (openParam === "chat") { - setIsOpen(true) - setIsMobileMenuOpen(false) + setIsOpen(true); + setIsMobileMenuOpen(false); if (isMobile) { - setActivePanel("chat") + setActivePanel("chat"); } } else if (openParam === "mcp") { // Open ConnectAIModal directly for MCP - setIsMobileMenuOpen(false) - setExpandedView(null) - setShowConnectAIModal(true) + setIsMobileMenuOpen(false); + setExpandedView(null); + setShowConnectAIModal(true); } else if (openParam === "addUrl") { - setShowAddMemoryView(true) - setExpandedView(null) + setShowAddMemoryView(true); + setExpandedView(null); if (isMobile) { - setIsMobileMenuOpen(true) - setActivePanel("menu") + setIsMobileMenuOpen(true); + setActivePanel("menu"); } } else if (validViews.includes(openParam as ValidView)) { // For other valid views like "profile", "integrations" - setExpandedView(openParam as ValidView) + setExpandedView(openParam as ValidView); if (isMobile) { - setIsMobileMenuOpen(true) - setActivePanel("menu") + setIsMobileMenuOpen(true); + setActivePanel("menu"); } } // Clear the parameter from URL after performing any action - clearOpenParam() + clearOpenParam(); } }, [ openParam, @@ -210,30 +210,31 @@ function Menu({ id }: { id?: string }) { setActivePanel, validViews, clearOpenParam, - ]) + ]); // Watch for active panel changes on mobile useEffect(() => { if (isMobile && activePanel !== "menu" && activePanel !== null) { // Another panel became active, close the menu - setIsMobileMenuOpen(false) - setExpandedView(null) + setIsMobileMenuOpen(false); + setExpandedView(null); } - }, [isMobile, activePanel]) + }, [isMobile, activePanel]); // Notify tour provider about expansion state changes useEffect(() => { const isExpanded = isMobile ? isMobileMenuOpen || !!expandedView - : isHovered || !!expandedView - setMenuExpanded(isExpanded) - }, [isMobile, isMobileMenuOpen, isHovered, expandedView, setMenuExpanded]) + : isHovered || !!expandedView; + setMenuExpanded(isExpanded); + }, [isMobile, isMobileMenuOpen, isHovered, expandedView, setMenuExpanded]); // Calculate width based on state - const menuWidth = expandedView || isCollapsing ? 600 : isHovered ? 160 : 56 + const menuWidth = expandedView || isCollapsing ? 600 : isHovered ? 160 : 56; // Dynamic z-index for mobile based on active panel - const mobileZIndex = isMobile && activePanel === "menu" ? "z-[70]" : "z-[100]" + const mobileZIndex = + isMobile && activePanel === "menu" ? "z-[70]" : "z-[100]"; return ( <> @@ -442,8 +443,8 @@ function Menu({ id }: { id?: string }) { <Button className="text-white/70 hover:text-white transition-colors duration-200" onClick={() => { - setIsCollapsing(true) - setExpandedView(null) + setIsCollapsing(true); + setExpandedView(null); }} size="icon" variant="ghost" @@ -479,14 +480,14 @@ function Menu({ id }: { id?: string }) { {/* Mobile Menu with Vaul Drawer */} {isMobile && ( <Drawer.Root - open={isMobileMenuOpen || !!expandedView} onOpenChange={(open) => { if (!open) { - setIsMobileMenuOpen(false) - setExpandedView(null) - setActivePanel(null) + setIsMobileMenuOpen(false); + setExpandedView(null); + setActivePanel(null); } }} + open={isMobileMenuOpen || !!expandedView} > {/* Menu Trigger Button */} {!isMobileMenuOpen && !expandedView && ( @@ -497,8 +498,8 @@ function Menu({ id }: { id?: string }) { className="w-14 h-14 flex items-center justify-center text-white rounded-full shadow-2xl" initial={{ scale: 0.8, opacity: 0 }} onClick={() => { - setIsMobileMenuOpen(true) - setActivePanel("menu") + setIsMobileMenuOpen(true); + setActivePanel("menu"); }} transition={{ duration: 0.3, @@ -594,13 +595,13 @@ function Menu({ id }: { id?: string }) { initial={{ opacity: 0, y: 10 }} layout onClick={() => { - handleMenuItemClick(item.key) + handleMenuItemClick(item.key); if ( item.key !== "mcp" && item.key !== "profile" && item.key !== "integrations" ) { - setIsMobileMenuOpen(false) + setIsMobileMenuOpen(false); } }} type="button" @@ -664,8 +665,8 @@ function Menu({ id }: { id?: string }) { <Button className="text-white/70 hover:text-white transition-colors duration-200" onClick={() => { - setIsCollapsing(true) - setExpandedView(null) + setIsCollapsing(true); + setExpandedView(null); }} size="icon" variant="ghost" @@ -707,7 +708,7 @@ function Menu({ id }: { id?: string }) { <Button className="hidden">Connect AI Assistant</Button> </ConnectAIModal> </> - ) + ); } -export default Menu +export default Menu; diff --git a/apps/web/components/project-selector.tsx b/apps/web/components/project-selector.tsx index a592c474..9392cebe 100644 --- a/apps/web/components/project-selector.tsx +++ b/apps/web/components/project-selector.tsx @@ -150,18 +150,18 @@ export function ProjectSelector() { {isOpen && ( <> <motion.div - className="fixed inset-0 z-40" - initial={{ opacity: 0 }} animate={{ opacity: 1 }} + className="fixed inset-0 z-40" exit={{ opacity: 0 }} + initial={{ opacity: 0 }} onClick={() => setIsOpen(false)} /> <motion.div - className="absolute top-full left-0 mt-1 w-56 bg-[#0f1419] backdrop-blur-xl border border-white/10 rounded-md shadow-xl z-50 overflow-hidden" - initial={{ opacity: 0, y: -5, scale: 0.98 }} animate={{ opacity: 1, y: 0, scale: 1 }} + className="absolute top-full left-0 mt-1 w-56 bg-[#0f1419] backdrop-blur-xl border border-white/10 rounded-md shadow-xl z-50 overflow-hidden" exit={{ opacity: 0, y: -5, scale: 0.98 }} + initial={{ opacity: 0, y: -5, scale: 0.98 }} transition={{ duration: 0.15 }} > <div className="p-1.5 max-h-64 overflow-y-auto"> @@ -187,14 +187,14 @@ export function ProjectSelector() { .filter((p: Project) => p.containerTag !== DEFAULT_PROJECT_ID) .map((project: Project, index: number) => ( <motion.div - key={project.id} + animate={{ opacity: 1, x: 0 }} className={`flex items-center justify-between p-2 rounded-md transition-colors group ${ selectedProject === project.containerTag ? "bg-white/15" : "hover:bg-white/8" }`} initial={{ opacity: 0, x: -5 }} - animate={{ opacity: 1, x: 0 }} + key={project.id} transition={{ delay: index * 0.03 }} > <div @@ -278,12 +278,12 @@ export function ProjectSelector() { ))} <motion.div + animate={{ opacity: 1 }} className="flex items-center gap-2 p-2 rounded-md hover:bg-white/8 transition-colors cursor-pointer border-t border-white/10 mt-1" - onClick={handleCreateNewProject} - whileHover={{ x: 1 }} initial={{ opacity: 0 }} - animate={{ opacity: 1 }} + onClick={handleCreateNewProject} transition={{ delay: (projects.length + 1) * 0.03 }} + whileHover={{ x: 1 }} > <Plus className="h-3.5 w-3.5 text-white/70" /> <span className="text-xs font-medium text-white/80"> @@ -297,8 +297,8 @@ export function ProjectSelector() { </AnimatePresence> <CreateProjectDialog - open={showCreateDialog} onOpenChange={setShowCreateDialog} + open={showCreateDialog} /> {/* Delete Project Dialog */} diff --git a/apps/web/components/referral-upgrade-modal.tsx b/apps/web/components/referral-upgrade-modal.tsx index 029bd2ae..8aa3ef3b 100644 --- a/apps/web/components/referral-upgrade-modal.tsx +++ b/apps/web/components/referral-upgrade-modal.tsx @@ -107,7 +107,7 @@ export function ReferralUpgradeModal({ if (user?.isAnonymous) { return ( - <Dialog open={isOpen} onOpenChange={onClose}> + <Dialog onOpenChange={onClose} open={isOpen}> <DialogContent className="sm:max-w-md bg-[#0f1419] backdrop-blur-xl border-white/10 text-white"> <motion.div animate={{ opacity: 1, scale: 1 }} @@ -136,7 +136,7 @@ export function ReferralUpgradeModal({ } return ( - <Dialog open={isOpen} onOpenChange={onClose}> + <Dialog onOpenChange={onClose} open={isOpen}> <DialogContent className="sm:max-w-lg bg-[#0f1419] backdrop-blur-xl border-white/10 text-white"> <motion.div animate={{ opacity: 1, scale: 1 }} @@ -177,16 +177,16 @@ export function ReferralUpgradeModal({ </div> {/* Tabs */} - <Tabs defaultValue="refer" className="w-full"> + <Tabs className="w-full" defaultValue="refer"> <TabsList className="grid w-full grid-cols-2 bg-white/5"> - <TabsTrigger value="refer" className="flex items-center gap-2"> + <TabsTrigger className="flex items-center gap-2" value="refer"> <Users className="w-4 h-4" /> Refer Friends </TabsTrigger> {!isPro && ( <TabsTrigger - value="upgrade" className="flex items-center gap-2" + value="upgrade" > <CreditCard className="w-4 h-4" /> Upgrade Plan @@ -194,7 +194,7 @@ export function ReferralUpgradeModal({ )} </TabsList> - <TabsContent value="refer" className="space-y-4 mt-6"> + <TabsContent className="space-y-4 mt-6" value="refer"> <div className="text-center"> <Gift className="w-12 h-12 text-blue-400 mx-auto mb-3" /> <HeadingH3Bold className="text-white mb-2"> @@ -241,7 +241,7 @@ export function ReferralUpgradeModal({ </TabsContent> {!isPro && ( - <TabsContent value="upgrade" className="space-y-4 mt-6"> + <TabsContent className="space-y-4 mt-6" value="upgrade"> <div className="text-center"> <CreditCard className="w-12 h-12 text-purple-400 mx-auto mb-3" /> <HeadingH3Bold className="text-white mb-2"> diff --git a/apps/web/components/text-shimmer.tsx b/apps/web/components/text-shimmer.tsx index 1825d08c..691a38cf 100644 --- a/apps/web/components/text-shimmer.tsx +++ b/apps/web/components/text-shimmer.tsx @@ -28,6 +28,7 @@ function TextShimmerComponent({ return ( <MotionComponent + animate={{ backgroundPosition: "0% center" }} className={cn( "relative inline-block bg-[length:250%_100%,auto] bg-clip-text", "text-transparent [--base-color:#a1a1aa] [--base-gradient-color:#000]", @@ -36,18 +37,18 @@ function TextShimmerComponent({ className, )} initial={{ backgroundPosition: "100% center" }} - animate={{ backgroundPosition: "0% center" }} - transition={{ - repeat: Infinity, - duration, - ease: "linear", - }} style={ { "--spread": `${dynamicSpread}px`, - backgroundImage: `var(--bg), linear-gradient(var(--base-color), var(--base-color))`, + backgroundImage: + "var(--bg), linear-gradient(var(--base-color), var(--base-color))", } as React.CSSProperties } + transition={{ + repeat: Number.POSITIVE_INFINITY, + duration, + ease: "linear", + }} > {children} </MotionComponent> diff --git a/apps/web/components/views/add-memory/action-buttons.tsx b/apps/web/components/views/add-memory/action-buttons.tsx index 6dc49304..596531b3 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-white/10 text-white 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-white/10 text-white 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-white/10 hover:bg-white/20 text-white border-white/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 + className="flex-1 sm:flex-initial" + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + > + <Button + className="bg-white/10 hover:bg-white/20 text-white border-white/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> + ); } diff --git a/apps/web/components/views/add-memory/fixed-mutation.tsx b/apps/web/components/views/add-memory/fixed-mutation.tsx deleted file mode 100644 index c71793b5..00000000 --- a/apps/web/components/views/add-memory/fixed-mutation.tsx +++ /dev/null @@ -1,244 +0,0 @@ -import { $fetch } from "@lib/api" -import { useMutation } from "@tanstack/react-query" -import { toast } from "sonner" - -// Simplified mutation that doesn't block UI with polling -export const createSimpleAddContentMutation = ( - queryClient: any, - onClose?: () => void, -) => { - return useMutation({ - mutationFn: async ({ - content, - project, - contentType, - }: { - content: string - project: string - contentType: "note" | "link" - }) => { - // Just create the memory, don't wait for processing - 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 ${contentType}`, - ) - } - - return { id: response.data.id, contentType } - }, - onMutate: async ({ content, project, contentType }) => { - // Cancel any outgoing refetches - await queryClient.cancelQueries({ - queryKey: ["documents-with-memories", project], - }) - - // Snapshot the previous value - const previousMemories = queryClient.getQueryData([ - "documents-with-memories", - project, - ]) - - // Create optimistic memory - const tempId = `temp-${Date.now()}` - const optimisticMemory = { - id: tempId, - content: contentType === "link" ? "" : content, - url: contentType === "link" ? content : null, - title: - contentType === "link" ? "Processing..." : content.substring(0, 100), - description: - contentType === "link" - ? "Extracting content..." - : "Processing content...", - containerTags: [project], - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - status: "processing", - type: contentType, - metadata: { - processingStage: "queued", - processingMessage: "Added to processing queue", - }, - memoryEntries: [], - isOptimistic: true, - } - - // Optimistically update to include the new memory - queryClient.setQueryData( - ["documents-with-memories", project], - (old: any) => { - // Handle infinite query structure - if (old?.pages) { - const newPages = [...old.pages] - if (newPages.length > 0) { - // Add to the first page - const firstPage = { ...newPages[0] } - firstPage.documents = [ - optimisticMemory, - ...(firstPage.documents || []), - ] - newPages[0] = firstPage - } else { - // No pages yet, create the first page - newPages.push({ - documents: [optimisticMemory], - pagination: { currentPage: 1, totalPages: 1, totalCount: 1 }, - totalCount: 1, - }) - } - - return { - ...old, - pages: newPages, - } - } - // Fallback for regular query structure - const newData = old - ? { - ...old, - documents: [optimisticMemory, ...(old.documents || [])], - totalCount: (old.totalCount || 0) + 1, - } - : { documents: [optimisticMemory], totalCount: 1 } - return newData - }, - ) - - return { previousMemories, optimisticId: tempId } - }, - onError: (error, variables, context) => { - // Roll back on error - if (context?.previousMemories) { - queryClient.setQueryData( - ["documents-with-memories", variables.project], - context.previousMemories, - ) - } - toast.error(`Failed to add ${variables.contentType}`, { - description: error instanceof Error ? error.message : "Unknown error", - }) - }, - onSuccess: (data, variables, context) => { - // Show success message - toast.success( - `${variables.contentType === "link" ? "Link" : "Note"} created successfully!`, - ) - - // Close modal - onClose?.() - - // Start polling for this specific memory ID - // The polling will happen in the background and update the optimistic memory when done - startMemoryPolling( - data.id, - variables.project, - context?.optimisticId, - queryClient, - ) - }, - }) -} - -// Background polling function -const startMemoryPolling = ( - memoryId: string, - project: string, - optimisticId: string | undefined, - queryClient: any, -) => { - const pollMemory = async () => { - try { - const memory = await $fetch(`@get/documents/${memoryId}`) - - if (memory.error) { - console.error("Failed to fetch memory status:", memory.error) - return false - } - - const isComplete = - memory.data?.status === "done" || - memory.data?.content || - memory.data?.memoryEntries?.length > 0 - - if (isComplete) { - // Replace optimistic memory with real data - queryClient.setQueryData( - ["documents-with-memories", project], - (old: any) => { - if (old?.pages) { - // Handle infinite query structure - const newPages = old.pages.map((page: any) => ({ - ...page, - documents: page.documents.map((doc: any) => { - if (doc.isOptimistic || doc.id === optimisticId) { - // Replace with real memory - return { - ...memory.data, - isOptimistic: false, - } - } - return doc - }), - })) - - return { - ...old, - pages: newPages, - } - } - // Handle regular query structure - return { - ...old, - documents: old.documents.map((doc: any) => { - if (doc.isOptimistic || doc.id === optimisticId) { - return { - ...memory.data, - isOptimistic: false, - } - } - return doc - }), - } - }, - ) - return true // Stop polling - } - - return false // Continue polling - } catch (error) { - console.error("Error polling memory:", error) - return false // Continue polling - } - } - - // Poll every 3 seconds, max 60 attempts (3 minutes) - let attempts = 0 - const maxAttempts = 60 - - const poll = async () => { - if (attempts >= maxAttempts) { - console.log("Memory polling timed out") - return - } - - const isComplete = await pollMemory() - attempts++ - - if (!isComplete && attempts < maxAttempts) { - setTimeout(poll, 3000) // Poll again in 3 seconds - } - } - - // Start polling after a short delay - setTimeout(poll, 2000) -} diff --git a/apps/web/components/views/add-memory/index.tsx b/apps/web/components/views/add-memory/index.tsx index 5116e952..96bb0a8b 100644 --- a/apps/web/components/views/add-memory/index.tsx +++ b/apps/web/components/views/add-memory/index.tsx @@ -1,9 +1,9 @@ -import { $fetch } from "@lib/api"; +import { $fetch } from "@lib/api" import { fetchConsumerProProduct, fetchMemoriesFeature, -} from "@repo/lib/queries"; -import { Button } from "@repo/ui/components/button"; +} from "@repo/lib/queries" +import { Button } from "@repo/ui/components/button" import { Dialog, DialogContent, @@ -11,18 +11,19 @@ import { DialogFooter, DialogHeader, DialogTitle, -} from "@repo/ui/components/dialog"; -import { Input } from "@repo/ui/components/input"; -import { Label } from "@repo/ui/components/label"; -import { Textarea } from "@repo/ui/components/textarea"; -import { useForm } from "@tanstack/react-form"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +} from "@repo/ui/components/dialog" +import { Input } from "@repo/ui/components/input" +import { Label } from "@repo/ui/components/label" +import { Textarea } from "@repo/ui/components/textarea" +import type { GetMemoryResponseSchema } from "@repo/validation/api" +import { useForm } from "@tanstack/react-form" +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { Dropzone, DropzoneContent, DropzoneEmptyState, -} from "@ui/components/shadcn-io/dropzone"; -import { useCustomer } from "autumn-js/react"; +} from "@ui/components/shadcn-io/dropzone" +import { useCustomer } from "autumn-js/react" import { Brain, FileIcon, @@ -31,19 +32,19 @@ import { PlugIcon, Plus, UploadIcon, -} from "lucide-react"; -import { AnimatePresence, motion } from "motion/react"; -import dynamic from "next/dynamic"; -import { useEffect, useState } from "react"; -import { toast } from "sonner"; -import { z } from "zod"; -import { analytics } from "@/lib/analytics"; -import { useProject } from "@/stores"; -import { ConnectionsTabContent } from "../connections-tab-content"; -import { ActionButtons } from "./action-buttons"; -import { MemoryUsageRing } from "./memory-usage-ring"; -import { ProjectSelection } from "./project-selection"; -import { TabButton } from "./tab-button"; +} from "lucide-react" +import { AnimatePresence, motion } from "motion/react" +import dynamic from "next/dynamic" +import { useEffect, useState } from "react" +import { toast } from "sonner" +import { z } from "zod" +import { analytics } from "@/lib/analytics" +import { useProject } from "@/stores" +import { ConnectionsTabContent } from "../connections-tab-content" +import { ActionButtons } from "./action-buttons" +import { MemoryUsageRing } from "./memory-usage-ring" +import { ProjectSelection } from "./project-selection" +import { TabButton } from "./tab-button" const TextEditor = dynamic( () => import("./text-editor").then((mod) => ({ default: mod.TextEditor })), @@ -64,91 +65,100 @@ const TextEditor = dynamic( ), ssr: false, }, -); +) // Simple function to extract plain text title from HTML content const getPlainTextTitle = (htmlContent: string) => { - const temp = document.createElement("div"); - temp.innerHTML = htmlContent; - const plainText = temp.textContent || temp.innerText || htmlContent; - const firstLine = plainText.split("\n")[0].trim(); + const temp = document.createElement("div") + temp.innerHTML = htmlContent + const plainText = temp.textContent || temp.innerText || htmlContent + + if (!plainText) { + return "Untitled" + } + const firstLine = plainText.split("\n")[0]?.trim() + + if (!firstLine) { + return plainText.substring(0, 100) + } + return firstLine.length > 0 ? firstLine.substring(0, 100) - : plainText.substring(0, 100); -}; + : plainText.substring(0, 100) +} export function AddMemoryView({ onClose, initialTab = "note", }: { - onClose?: () => void; - initialTab?: "note" | "link" | "file" | "connect"; + onClose?: () => void + initialTab?: "note" | "link" | "file" | "connect" }) { - const queryClient = useQueryClient(); - const { selectedProject, setSelectedProject } = useProject(); - const [showAddDialog, setShowAddDialog] = useState(true); - const [selectedFiles, setSelectedFiles] = useState<File[]>([]); + const queryClient = useQueryClient() + const { selectedProject, setSelectedProject } = useProject() + const [showAddDialog, setShowAddDialog] = useState(true) + const [selectedFiles, setSelectedFiles] = useState<File[]>([]) const [activeTab, setActiveTab] = useState< "note" | "link" | "file" | "connect" - >(initialTab); - const autumn = useCustomer(); - const [showCreateProjectDialog, setShowCreateProjectDialog] = useState(false); - const [newProjectName, setNewProjectName] = useState(""); + >(initialTab) + const autumn = useCustomer() + const [showCreateProjectDialog, setShowCreateProjectDialog] = useState(false) + const [newProjectName, setNewProjectName] = useState("") // Check memory limits - const { data: memoriesCheck } = fetchMemoriesFeature(autumn); + const { data: memoriesCheck } = fetchMemoriesFeature(autumn) - const memoriesUsed = memoriesCheck?.usage ?? 0; - const memoriesLimit = memoriesCheck?.included_usage ?? 0; + const memoriesUsed = memoriesCheck?.usage ?? 0 + const memoriesLimit = memoriesCheck?.included_usage ?? 0 // Fetch projects for the dropdown const { data: projects = [], isLoading: isLoadingProjects } = useQuery({ queryKey: ["projects"], queryFn: async () => { - const response = await $fetch("@get/projects"); + const response = await $fetch("@get/projects") if (response.error) { - throw new Error(response.error?.message || "Failed to load projects"); + throw new Error(response.error?.message || "Failed to load projects") } - return response.data?.projects || []; + return response.data?.projects || [] }, staleTime: 30 * 1000, - }); + }) // Create project mutation const createProjectMutation = useMutation({ mutationFn: async (name: string) => { const response = await $fetch("@post/projects", { body: { name }, - }); + }) if (response.error) { - throw new Error(response.error?.message || "Failed to create project"); + throw new Error(response.error?.message || "Failed to create project") } - return response.data; + return response.data }, onSuccess: (data) => { - analytics.projectCreated(); - toast.success("Project created successfully!"); - setShowCreateProjectDialog(false); - setNewProjectName(""); - queryClient.invalidateQueries({ queryKey: ["projects"] }); + analytics.projectCreated() + toast.success("Project created successfully!") + setShowCreateProjectDialog(false) + setNewProjectName("") + queryClient.invalidateQueries({ queryKey: ["projects"] }) // Set the newly created project as selected if (data?.containerTag) { - setSelectedProject(data.containerTag); + setSelectedProject(data.containerTag) // Update form values - addContentForm.setFieldValue("project", data.containerTag); - fileUploadForm.setFieldValue("project", data.containerTag); + addContentForm.setFieldValue("project", data.containerTag) + fileUploadForm.setFieldValue("project", data.containerTag) } }, onError: (error) => { toast.error("Failed to create project", { description: error instanceof Error ? error.message : "Unknown error", - }); + }) }, - }); + }) const addContentForm = useForm({ defaultValues: { @@ -160,8 +170,8 @@ export function AddMemoryView({ content: value.content, project: value.project, contentType: activeTab as "note" | "link", - }); - formApi.reset(); + }) + formApi.reset() }, validators: { onChange: z.object({ @@ -169,19 +179,19 @@ export function AddMemoryView({ project: z.string(), }), }, - }); + }) // Re-validate content field when tab changes between note/link // biome-ignore lint/correctness/useExhaustiveDependencies: It is what it is useEffect(() => { // Trigger validation of the content field when switching between note/link if (activeTab === "note" || activeTab === "link") { - const currentValue = addContentForm.getFieldValue("content"); + const currentValue = addContentForm.getFieldValue("content") if (currentValue) { - addContentForm.validateField("content", "change"); + addContentForm.validateField("content", "change") } } - }, [activeTab]); + }, [activeTab]) // Form for file upload metadata const fileUploadForm = useForm({ @@ -192,8 +202,8 @@ export function AddMemoryView({ }, onSubmit: async ({ value, formApi }) => { if (selectedFiles.length === 0) { - toast.error("Please select a file to upload"); - return; + toast.error("Please select a file to upload") + return } for (const file of selectedFiles) { @@ -202,13 +212,13 @@ export function AddMemoryView({ title: value.title || undefined, description: value.description || undefined, project: value.project, - }); + }) } - formApi.reset(); - setSelectedFiles([]); + formApi.reset() + setSelectedFiles([]) }, - }); + }) const addContentMutation = useMutation({ mutationFn: async ({ @@ -216,11 +226,11 @@ export function AddMemoryView({ project, contentType, }: { - content: string; - project: string; - contentType: "note" | "link"; + content: string + project: string + contentType: "note" | "link" }) => { - console.log("📤 Creating memory..."); + console.log("📤 Creating memory...") const response = await $fetch("@post/documents", { body: { @@ -230,34 +240,34 @@ export function AddMemoryView({ sm_source: "consumer", }, }, - }); + }) if (response.error) { throw new Error( response.error?.message || `Failed to add ${contentType}`, - ); + ) } - console.log("✅ Memory created:", response.data); - return response.data; + console.log("✅ Memory created:", response.data) + return response.data }, onMutate: async ({ content, project, contentType }) => { - console.log("🚀 OPTIMISTIC UPDATE: Starting for", contentType); + console.log("🚀 OPTIMISTIC UPDATE: Starting for", contentType) // Cancel queries to prevent conflicts await queryClient.cancelQueries({ queryKey: ["documents-with-memories", project], - }); - console.log("📞 QUERIES CANCELLED for project:", project); + }) + console.log("📞 QUERIES CANCELLED for project:", project) // Get previous data for rollback const previousMemories = queryClient.getQueryData([ "documents-with-memories", project, - ]); + ]) // Create optimistic memory with proper title - const tempId = `temp-${Date.now()}`; + const tempId = `temp-${Date.now()}` const optimisticMemory = { id: tempId, content: contentType === "link" ? "" : content, @@ -270,9 +280,9 @@ export function AddMemoryView({ updatedAt: new Date().toISOString(), memoryEntries: [], isOptimistic: true, - }; + } - console.log("🎯 Adding optimistic memory:", optimisticMemory); + console.log("🎯 Adding optimistic memory:", optimisticMemory) // Add to cache optimistically queryClient.setQueryData( @@ -280,19 +290,19 @@ export function AddMemoryView({ (old: any) => { if (old?.pages) { // Infinite query structure - const newPages = [...old.pages]; + const newPages = [...old.pages] if (newPages.length > 0) { newPages[0] = { ...newPages[0], documents: [optimisticMemory, ...(newPages[0].documents || [])], - }; + } } else { newPages.push({ documents: [optimisticMemory], pagination: { currentPage: 1, totalPages: 1, totalCount: 1 }, - }); + }) } - return { ...old, pages: newPages }; + return { ...old, pages: newPages } } // Regular query structure @@ -302,44 +312,46 @@ export function AddMemoryView({ documents: [optimisticMemory, ...(old.documents || [])], totalCount: (old.totalCount || 0) + 1, } - : { documents: [optimisticMemory], totalCount: 1 }; + : { documents: [optimisticMemory], totalCount: 1 } }, - ); + ) - return { previousMemories, optimisticId: tempId }; + return { previousMemories, optimisticId: tempId } }, onError: (error, variables, context) => { - console.log("❌ Mutation failed, rolling back"); + console.log("❌ Mutation failed, rolling back") if (context?.previousMemories) { queryClient.setQueryData( ["documents-with-memories", variables.project], context.previousMemories, - ); + ) } - toast.error(`Failed to add ${variables.contentType}`); + toast.error(`Failed to add ${variables.contentType}`) }, onSuccess: (data, variables, context) => { console.log( "✅ Mutation succeeded, starting simple polling for memory:", data.id, - ); + ) analytics.memoryAdded({ type: variables.contentType === "link" ? "link" : "note", project_id: variables.project, content_length: variables.content.length, - }); + }) toast.success( `${variables.contentType === "link" ? "Link" : "Note"} added successfully!`, - ); - setShowAddDialog(false); - onClose?.(); + ) + setShowAddDialog(false) + onClose?.() // Simple polling to replace optimistic update when ready const pollMemory = async () => { try { - const memory = await $fetch(`@get/documents/${data.id}`); - console.log("🔍 Polling memory:", memory.data); + const memory = await $fetch<z.infer<typeof GetMemoryResponseSchema>>( + `@get/documents/${data.id}`, + ) + console.log("🔍 Polling memory:", memory.data) if (memory.data && !memory.error) { // Check if memory has been processed (has memory entries, substantial content, and NOT untitled/processing) @@ -347,17 +359,16 @@ export function AddMemoryView({ memory.data.title && !memory.data.title.toLowerCase().includes("untitled") && !memory.data.title.toLowerCase().includes("processing") && - memory.data.title.length > 3; + memory.data.title.length > 3 const isReady = - memory.data.memoryEntries?.length > 0 || - (memory.data.content && - memory.data.content.length > 10 && - hasRealTitle); + memory.data.content && + memory.data.content.length > 10 && + hasRealTitle console.log("📊 Memory ready check:", { isReady, - hasMemoryEntries: memory.data.memoryEntries?.length, + hasMemoryEntries: 0, hasContent: memory.data.content?.length, title: memory.data.title, hasRealTitle, @@ -367,10 +378,10 @@ export function AddMemoryView({ titleNotProcessing: memory.data.title && !memory.data.title.toLowerCase().includes("processing"), - }); + }) if (isReady) { - console.log("✅ Memory ready, replacing optimistic update"); + console.log("✅ Memory ready, replacing optimistic update") // Replace optimistic memory with real data queryClient.setQueryData( ["documents-with-memories", variables.project], @@ -380,48 +391,48 @@ export function AddMemoryView({ ...page, documents: page.documents.map((doc: any) => { if (doc.isOptimistic && doc.id.startsWith("temp-")) { - return { ...memory.data, isOptimistic: false }; + return { ...memory.data, isOptimistic: false } } - return doc; + return doc }), - })); - return { ...old, pages: newPages }; + })) + return { ...old, pages: newPages } } - return old; + return old }, - ); - return true; // Stop polling + ) + return true // Stop polling } } - return false; // Continue polling + return false // Continue polling } catch (error) { - console.error("❌ Error polling memory:", error); - return false; + console.error("❌ Error polling memory:", error) + return false } - }; + } // Poll every 3 seconds for up to 30 attempts (90 seconds) - let attempts = 0; - const maxAttempts = 30; + let attempts = 0 + const maxAttempts = 30 const poll = async () => { if (attempts >= maxAttempts) { - console.log("⚠️ Polling stopped after max attempts"); - return; + console.log("⚠️ Polling stopped after max attempts") + return } - const isDone = await pollMemory(); - attempts++; + const isDone = await pollMemory() + attempts++ if (!isDone && attempts < maxAttempts) { - setTimeout(poll, 3000); + setTimeout(poll, 3000) } - }; + } // Start polling after 2 seconds - setTimeout(poll, 2000); + setTimeout(poll, 2000) }, - }); + }) const fileUploadMutation = useMutation({ mutationFn: async ({ @@ -430,10 +441,10 @@ export function AddMemoryView({ description, project, }: { - file: File; - title?: string; - description?: string; - project: string; + file: File + title?: string + description?: string + project: string }) => { // TEMPORARILY DISABLED: Limit check disabled // Check if user can add more memories @@ -443,9 +454,9 @@ export function AddMemoryView({ // ); // } - const formData = new FormData(); - formData.append("file", file); - formData.append("containerTags", JSON.stringify([project])); + const formData = new FormData() + formData.append("file", file) + formData.append("containerTags", JSON.stringify([project])) const response = await fetch( `${process.env.NEXT_PUBLIC_BACKEND_URL}/v3/documents/file`, @@ -454,14 +465,14 @@ export function AddMemoryView({ body: formData, credentials: "include", }, - ); + ) if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || "Failed to upload file"); + const error = await response.json() + throw new Error(error.error || "Failed to upload file") } - const data = await response.json(); + const data = await response.json() // If we have metadata, we can update the document after creation if (title || description) { @@ -473,23 +484,23 @@ export function AddMemoryView({ sm_source: "consumer", // Use "consumer" source to bypass limits }, }, - }); + }) } - return data; + return data }, // Optimistic update onMutate: async ({ file, title, description, project }) => { // Cancel any outgoing refetches await queryClient.cancelQueries({ queryKey: ["documents-with-memories", project], - }); + }) // Snapshot the previous value const previousMemories = queryClient.getQueryData([ "documents-with-memories", project, - ]); + ]) // Create optimistic memory for the file const optimisticMemory = { @@ -509,7 +520,7 @@ export function AddMemoryView({ mimeType: file.type, }, memoryEntries: [], - }; + } // Optimistically update to include the new memory queryClient.setQueryData( @@ -518,41 +529,41 @@ export function AddMemoryView({ // Handle infinite query structure if (old?.pages) { // This is an infinite query - add to the first page - const newPages = [...old.pages]; + const newPages = [...old.pages] if (newPages.length > 0) { // Add to the first page - const firstPage = { ...newPages[0] }; + const firstPage = { ...newPages[0] } firstPage.documents = [ optimisticMemory, ...(firstPage.documents || []), - ]; - newPages[0] = firstPage; + ] + newPages[0] = firstPage } else { // No pages yet, create the first page newPages.push({ documents: [optimisticMemory], pagination: { currentPage: 1, totalPages: 1, totalCount: 1 }, totalCount: 1, - }); + }) } return { ...old, pages: newPages, - }; + } } // Fallback for regular query structure - if (!old) return { documents: [optimisticMemory], totalCount: 1 }; + if (!old) return { documents: [optimisticMemory], totalCount: 1 } return { ...old, documents: [optimisticMemory, ...(old.documents || [])], totalCount: (old.totalCount || 0) + 1, - }; + } }, - ); + ) // Return a context object with the snapshotted value - return { previousMemories }; + return { previousMemories } }, // If the mutation fails, roll back to the previous value onError: (error, variables, context) => { @@ -560,11 +571,11 @@ export function AddMemoryView({ 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) => { analytics.memoryAdded({ @@ -572,18 +583,18 @@ export function AddMemoryView({ project_id: variables.project, file_size: variables.file.size, file_type: variables.file.type, - }); + }) toast.success("File uploaded successfully!", { description: "Your file is being processed", - }); - setShowAddDialog(false); - onClose?.(); + }) + setShowAddDialog(false) + onClose?.() }, // Don't invalidate queries immediately - let optimistic updates work // onSettled: () => { // queryClient.invalidateQueries({ queryKey: ["documents-with-memories"] }) // }, - }); + }) return ( <AnimatePresence mode="wait"> @@ -591,8 +602,8 @@ export function AddMemoryView({ <Dialog key="add-memory-dialog" onOpenChange={(open) => { - setShowAddDialog(open); - if (!open) onClose?.(); + setShowAddDialog(open) + if (!open) onClose?.() }} open={showAddDialog} > @@ -651,9 +662,9 @@ export function AddMemoryView({ <div className="space-y-4"> <form onSubmit={(e) => { - e.preventDefault(); - e.stopPropagation(); - addContentForm.handleSubmit(); + e.preventDefault() + e.stopPropagation() + addContentForm.handleSubmit() }} > <div className="grid gap-4"> @@ -669,9 +680,9 @@ export function AddMemoryView({ validators={{ onChange: ({ value }) => { if (!value || value.trim() === "") { - return "Note is required"; + return "Note is required" } - return undefined; + return undefined }, }} > @@ -753,9 +764,9 @@ export function AddMemoryView({ isSubmitDisabled={!addContentForm.state.canSubmit} isSubmitting={addContentMutation.isPending} onCancel={() => { - setShowAddDialog(false); - onClose?.(); - addContentForm.reset(); + setShowAddDialog(false) + onClose?.() + addContentForm.reset() }} submitIcon={Plus} submitText="Add Note" @@ -769,9 +780,9 @@ export function AddMemoryView({ <div className="space-y-4"> <form onSubmit={(e) => { - e.preventDefault(); - e.stopPropagation(); - addContentForm.handleSubmit(); + e.preventDefault() + e.stopPropagation() + addContentForm.handleSubmit() }} > <div className="grid gap-4"> @@ -793,13 +804,13 @@ export function AddMemoryView({ validators={{ onChange: ({ value }) => { if (!value || value.trim() === "") { - return "Link is required"; + return "Link is required" } try { - new URL(value); - return undefined; + new URL(value) + return undefined } catch { - return "Please enter a valid link"; + return "Please enter a valid link" } }, }} @@ -879,9 +890,9 @@ export function AddMemoryView({ isSubmitDisabled={!addContentForm.state.canSubmit} isSubmitting={addContentMutation.isPending} onCancel={() => { - setShowAddDialog(false); - onClose?.(); - addContentForm.reset(); + setShowAddDialog(false) + onClose?.() + addContentForm.reset() }} submitIcon={Plus} submitText="Add Link" @@ -895,9 +906,9 @@ export function AddMemoryView({ <div className="space-y-4"> <form onSubmit={(e) => { - e.preventDefault(); - e.stopPropagation(); - fileUploadForm.handleSubmit(); + e.preventDefault() + e.stopPropagation() + fileUploadForm.handleSubmit() }} > <div className="grid gap-4"> @@ -1031,10 +1042,10 @@ export function AddMemoryView({ isSubmitDisabled={selectedFiles.length === 0} isSubmitting={fileUploadMutation.isPending} onCancel={() => { - setShowAddDialog(false); - onClose?.(); - fileUploadForm.reset(); - setSelectedFiles([]); + setShowAddDialog(false) + onClose?.() + fileUploadForm.reset() + setSelectedFiles([]) }} submitIcon={UploadIcon} submitText="Upload File" @@ -1102,8 +1113,8 @@ export function AddMemoryView({ <Button className="bg-white/5 hover:bg-white/10 border-white/10 text-white w-full sm:w-auto" onClick={() => { - setShowCreateProjectDialog(false); - setNewProjectName(""); + setShowCreateProjectDialog(false) + setNewProjectName("") }} type="button" variant="outline" @@ -1140,19 +1151,19 @@ export function AddMemoryView({ </Dialog> )} </AnimatePresence> - ); + ) } export function AddMemoryExpandedView() { - const [showDialog, setShowDialog] = useState(false); + const [showDialog, setShowDialog] = useState(false) const [selectedTab, setSelectedTab] = useState< "note" | "link" | "file" | "connect" - >("note"); + >("note") const handleOpenDialog = (tab: "note" | "link" | "file" | "connect") => { - setSelectedTab(tab); - setShowDialog(true); - }; + setSelectedTab(tab) + setShowDialog(true) + } return ( <> @@ -1223,5 +1234,5 @@ export function AddMemoryExpandedView() { /> )} </> - ); + ) } diff --git a/apps/web/components/views/add-memory/memory-usage-ring.tsx b/apps/web/components/views/add-memory/memory-usage-ring.tsx index f6fb9836..7dbf8563 100644 --- a/apps/web/components/views/add-memory/memory-usage-ring.tsx +++ b/apps/web/components/views/add-memory/memory-usage-ring.tsx @@ -1,7 +1,7 @@ interface MemoryUsageRingProps { - memoriesUsed: number; - memoriesLimit: number; - className?: string; + memoriesUsed: number + memoriesLimit: number + className?: string } export function MemoryUsageRing({ @@ -9,10 +9,10 @@ export function MemoryUsageRing({ memoriesLimit, className = "", }: MemoryUsageRingProps) { - const usagePercentage = memoriesUsed / memoriesLimit; + const usagePercentage = memoriesUsed / memoriesLimit const strokeColor = - memoriesUsed >= memoriesLimit * 0.8 ? "rgb(251 191 36)" : "rgb(34 197 94)"; - const circumference = 2 * Math.PI * 10; + memoriesUsed >= memoriesLimit * 0.8 ? "rgb(251 191 36)" : "rgb(34 197 94)" + const circumference = 2 * Math.PI * 10 return ( <div @@ -20,27 +20,28 @@ export function MemoryUsageRing({ title={`${memoriesUsed} of ${memoriesLimit} memories used`} > <svg className="w-6 h-6 transform -rotate-90" viewBox="0 0 24 24"> + <title>Memory Usage</title> {/* Background circle */} <circle cx="12" cy="12" + fill="none" r="10" stroke="rgb(255 255 255 / 0.1)" strokeWidth="2" - fill="none" /> {/* Progress circle */} <circle + className="transition-all duration-300" cx="12" cy="12" + fill="none" r="10" stroke={strokeColor} - strokeWidth="2" - fill="none" strokeDasharray={`${circumference}`} strokeDashoffset={`${circumference * (1 - usagePercentage)}`} - className="transition-all duration-300" strokeLinecap="round" + strokeWidth="2" /> </svg> @@ -49,5 +50,5 @@ export function MemoryUsageRing({ {memoriesUsed} / {memoriesLimit} </div> </div> - ); + ) } diff --git a/apps/web/components/views/add-memory/project-selection.tsx b/apps/web/components/views/add-memory/project-selection.tsx index f23768a3..b0f79792 100644 --- a/apps/web/components/views/add-memory/project-selection.tsx +++ b/apps/web/components/views/add-memory/project-selection.tsx @@ -1,94 +1,94 @@ import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@repo/ui/components/select'; -import { Plus } from 'lucide-react'; + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@repo/ui/components/select"; +import { Plus } from "lucide-react"; interface Project { - id?: string; - containerTag: string; - name: string; + id?: string; + containerTag: string; + name: string; } interface ProjectSelectionProps { - projects: Project[]; - selectedProject: string; - onProjectChange: (value: string) => void; - onCreateProject: () => void; - disabled?: boolean; - isLoading?: boolean; - className?: string; - id?: string; + projects: Project[]; + selectedProject: string; + onProjectChange: (value: string) => void; + onCreateProject: () => void; + disabled?: boolean; + isLoading?: boolean; + className?: string; + id?: string; } export function ProjectSelection({ - projects, - selectedProject, - onProjectChange, - onCreateProject, - disabled = false, - isLoading = false, - className = '', - id = 'project-select', + projects, + selectedProject, + onProjectChange, + onCreateProject, + disabled = false, + isLoading = false, + className = "", + id = "project-select", }: ProjectSelectionProps) { - const handleValueChange = (value: string) => { - if (value === 'create-new-project') { - onCreateProject(); - } else { - onProjectChange(value); - } - }; + const handleValueChange = (value: string) => { + if (value === "create-new-project") { + onCreateProject(); + } else { + onProjectChange(value); + } + }; - return ( - <Select - key={`${id}-${selectedProject}`} - disabled={isLoading || disabled} - onValueChange={handleValueChange} - value={selectedProject} - > - <SelectTrigger - className={`bg-white/5 border-white/10 text-white ${className}`} - id={id} - > - <SelectValue placeholder="Select a project" /> - </SelectTrigger> - <SelectContent - className="bg-black/90 backdrop-blur-xl border-white/10 z-[90]" - position="popper" - sideOffset={5} - > - <SelectItem - className="text-white hover:bg-white/10" - key="default" - value="sm_project_default" - > - Default Project - </SelectItem> - {projects - .filter((p) => p.containerTag !== 'sm_project_default' && p.id) - .map((project) => ( - <SelectItem - className="text-white hover:bg-white/10" - key={project.id || project.containerTag} - value={project.containerTag} - > - {project.name} - </SelectItem> - ))} - <SelectItem - className="text-white hover:bg-white/10 border-t border-white/10 mt-1" - key="create-new" - value="create-new-project" - > - <div className="flex items-center gap-2"> - <Plus className="h-4 w-4" /> - <span>Create new project</span> - </div> - </SelectItem> - </SelectContent> - </Select> - ); + return ( + <Select + disabled={isLoading || disabled} + key={`${id}-${selectedProject}`} + onValueChange={handleValueChange} + value={selectedProject} + > + <SelectTrigger + className={`bg-white/5 border-white/10 text-white ${className}`} + id={id} + > + <SelectValue placeholder="Select a project" /> + </SelectTrigger> + <SelectContent + className="bg-black/90 backdrop-blur-xl border-white/10 z-[90]" + position="popper" + sideOffset={5} + > + <SelectItem + className="text-white hover:bg-white/10" + key="default" + value="sm_project_default" + > + Default Project + </SelectItem> + {projects + .filter((p) => p.containerTag !== "sm_project_default" && p.id) + .map((project) => ( + <SelectItem + className="text-white hover:bg-white/10" + key={project.id || project.containerTag} + value={project.containerTag} + > + {project.name} + </SelectItem> + ))} + <SelectItem + className="text-white hover:bg-white/10 border-t border-white/10 mt-1" + key="create-new" + value="create-new-project" + > + <div className="flex items-center gap-2"> + <Plus className="h-4 w-4" /> + <span>Create new project</span> + </div> + </SelectItem> + </SelectContent> + </Select> + ); } diff --git a/apps/web/components/views/add-memory/tab-button.tsx b/apps/web/components/views/add-memory/tab-button.tsx index 72dfbbd7..3afbdedf 100644 --- a/apps/web/components/views/add-memory/tab-button.tsx +++ b/apps/web/components/views/add-memory/tab-button.tsx @@ -1,28 +1,28 @@ -import type { LucideIcon } from 'lucide-react'; +import type { LucideIcon } from "lucide-react"; interface TabButtonProps { - icon: LucideIcon; - label: string; - isActive: boolean; - onClick: () => void; + icon: LucideIcon; + label: string; + isActive: boolean; + onClick: () => void; } export function TabButton({ - icon: Icon, - label, - isActive, - onClick, + icon: Icon, + label, + isActive, + onClick, }: TabButtonProps) { - return ( - <button - className={`flex items-center gap-1.5 text-xs sm:text-xs px-4 sm:px-3 py-2 sm:py-1 h-8 sm:h-6 rounded-sm transition-colors whitespace-nowrap min-w-0 ${ - isActive ? 'bg-white/10' : 'hover:bg-white/5' - }`} - onClick={onClick} - type="button" - > - <Icon className="h-4 w-4 sm:h-3 sm:w-3" /> - {label} - </button> - ); + return ( + <button + className={`flex items-center gap-1.5 text-xs sm:text-xs px-4 sm:px-3 py-2 sm:py-1 h-8 sm:h-6 rounded-sm transition-colors whitespace-nowrap min-w-0 ${ + isActive ? "bg-white/10" : "hover:bg-white/5" + }`} + onClick={onClick} + type="button" + > + <Icon className="h-4 w-4 sm:h-3 sm:w-3" /> + {label} + </button> + ); } diff --git a/apps/web/components/views/add-memory/text-editor.tsx b/apps/web/components/views/add-memory/text-editor.tsx index 5a07b8f7..79b42aa8 100644 --- a/apps/web/components/views/add-memory/text-editor.tsx +++ b/apps/web/components/views/add-memory/text-editor.tsx @@ -399,8 +399,6 @@ export function TextEditor({ title: string; }) => ( <Button - variant="ghost" - size="sm" className={cn( "h-8 w-8 !p-0 text-white/70 transition-all duration-200 rounded-sm", "hover:bg-white/15 hover:text-white hover:scale-105", @@ -408,8 +406,10 @@ export function TextEditor({ isActive && "bg-white/20 text-white", )} onMouseDown={onMouseDown} + size="sm" title={title} type="button" + variant="ghost" > <Icon className={cn( @@ -426,13 +426,20 @@ export function TextEditor({ <Slate editor={editor} initialValue={editorValue} - onValueChange={handleSlateChange} onSelectionChange={() => setSelection(editor.selection)} + onValueChange={handleSlateChange} > <Editable + className={cn( + "outline-none w-full h-full text-white placeholder:text-white/50", + disabled && "opacity-50 cursor-not-allowed", + )} + onBlur={onBlur} + onKeyDown={handleKeyDown} + placeholder={placeholder} + readOnly={disabled} renderElement={renderElement} renderLeaf={renderLeaf} - placeholder={placeholder} renderPlaceholder={({ children, attributes }) => { return ( <div {...attributes} className="mt-2"> @@ -440,13 +447,6 @@ export function TextEditor({ </div> ); }} - onKeyDown={handleKeyDown} - onBlur={onBlur} - readOnly={disabled} - className={cn( - "outline-none w-full h-full text-white placeholder:text-white/50", - disabled && "opacity-50 cursor-not-allowed", - )} style={{ minHeight: "11rem", maxHeight: "15rem", diff --git a/apps/web/components/views/chat/index.tsx b/apps/web/components/views/chat/index.tsx index 2b7befa1..c9fdf9f1 100644 --- a/apps/web/components/views/chat/index.tsx +++ b/apps/web/components/views/chat/index.tsx @@ -50,12 +50,12 @@ export function ChatRewrite() { {getCurrentChat()?.title ?? "New Chat"} </h3> <div className="flex items-center gap-2"> - <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> + <Dialog onOpenChange={setIsDialogOpen} open={isDialogOpen}> <DialogTrigger asChild> <Button - variant="outline" - size="icon" onClick={() => analytics.chatHistoryViewed()} + size="icon" + variant="outline" > <HistoryIcon className="size-4 text-muted-foreground" /> </Button> @@ -77,19 +77,19 @@ export function ChatRewrite() { const isActive = c.id === currentChatId; return ( <div - key={c.id} - role="button" - tabIndex={0} - onClick={() => { - setCurrentChatId(c.id); - setIsDialogOpen(false); - }} + aria-current={isActive ? "true" : undefined} className={cn( "flex items-center justify-between rounded-md px-3 py-2 outline-none", "transition-colors", isActive ? "bg-primary/10" : "hover:bg-muted", )} - aria-current={isActive ? "true" : undefined} + key={c.id} + onClick={() => { + setCurrentChatId(c.id); + setIsDialogOpen(false); + }} + role="button" + tabIndex={0} > <div className="min-w-0"> <div className="flex items-center gap-2"> @@ -107,14 +107,14 @@ export function ChatRewrite() { </div> </div> <Button - type="button" - variant="ghost" - size="icon" + aria-label="Delete conversation" onClick={(e) => { e.stopPropagation(); analytics.chatDeleted(); }} - aria-label="Delete conversation" + size="icon" + type="button" + variant="ghost" > <Trash2 className="size-4 text-muted-foreground" /> </Button> @@ -129,22 +129,22 @@ export function ChatRewrite() { </div> </ScrollArea> <Button - variant="outline" - size="lg" className="w-full border-dashed" onClick={handleNewChat} + size="lg" + variant="outline" > <Plus className="size-4 mr-1" /> New Conversation </Button> </DialogContent> </Dialog> - <Button variant="outline" size="icon" onClick={handleNewChat}> + <Button onClick={handleNewChat} size="icon" variant="outline"> <Plus className="size-4 text-muted-foreground" /> </Button> <Button - variant="outline" - size="icon" onClick={() => setIsOpen(false)} + size="icon" + variant="outline" > <X className="size-4 text-muted-foreground" /> </Button> diff --git a/apps/web/components/views/connections-tab-content.tsx b/apps/web/components/views/connections-tab-content.tsx index 14a63e8f..cb25a9e9 100644 --- a/apps/web/components/views/connections-tab-content.tsx +++ b/apps/web/components/views/connections-tab-content.tsx @@ -1,22 +1,22 @@ -"use client" +"use client"; -import { $fetch } from "@lib/api" -import { Button } from "@repo/ui/components/button" -import { Skeleton } from "@repo/ui/components/skeleton" -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 { Trash2 } from "lucide-react" -import { AnimatePresence, motion } from "motion/react" -import { useEffect, useState } from "react" -import { toast } from "sonner" -import type { z } from "zod" -import { analytics } from "@/lib/analytics" -import { useProject } from "@/stores" +import { $fetch } from "@lib/api"; +import { Button } from "@repo/ui/components/button"; +import { Skeleton } from "@repo/ui/components/skeleton"; +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 { Trash2 } from "lucide-react"; +import { AnimatePresence, motion } from "motion/react"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; +import type { z } from "zod"; +import { analytics } from "@/lib/analytics"; +import { useProject } from "@/stores"; // Define types -type Connection = z.infer<typeof ConnectionResponseSchema> +type Connection = z.infer<typeof ConnectionResponseSchema>; // Connector configurations const CONNECTORS = { @@ -35,27 +35,27 @@ const CONNECTORS = { description: "Access your Microsoft Office documents", icon: OneDrive, }, -} as const +} as const; -type ConnectorProvider = keyof typeof CONNECTORS +type ConnectorProvider = keyof typeof CONNECTORS; export function ConnectionsTabContent() { - const queryClient = useQueryClient() - const { selectedProject } = useProject() - const autumn = useCustomer() - const [isProUser, setIsProUser] = useState(false) + const queryClient = useQueryClient(); + const { selectedProject } = useProject(); + const autumn = useCustomer(); + const [isProUser, setIsProUser] = useState(false); const handleUpgrade = async () => { try { await autumn.attach({ productId: "consumer_pro", successUrl: "https://app.supermemory.ai/", - }) - window.location.reload() + }); + window.location.reload(); } catch (error) { - console.error(error) + console.error(error); } - } + }; // Set pro user status when autumn data loads useEffect(() => { @@ -64,16 +64,16 @@ export function ConnectionsTabContent() { autumn.customer?.products.some( (product) => product.id === "consumer_pro", ) ?? false, - ) + ); } - }, [autumn.isLoading, autumn.customer]) + }, [autumn.isLoading, autumn.customer]); // Get connections data directly from autumn customer - const connectionsFeature = autumn.customer?.features?.connections - const connectionsUsed = connectionsFeature?.usage ?? 0 - const connectionsLimit = connectionsFeature?.included_usage ?? 0 + const connectionsFeature = autumn.customer?.features?.connections; + const connectionsUsed = connectionsFeature?.usage ?? 0; + const connectionsLimit = connectionsFeature?.included_usage ?? 0; - const canAddConnection = connectionsUsed < connectionsLimit + const canAddConnection = connectionsUsed < connectionsLimit; // Fetch connections const { @@ -87,26 +87,28 @@ export function ConnectionsTabContent() { body: { containerTags: [], }, - }) + }); if (response.error) { - throw new Error(response.error?.message || "Failed to load connections") + throw new Error( + response.error?.message || "Failed to load connections", + ); } - return response.data as Connection[] + return response.data as Connection[]; }, staleTime: 30 * 1000, refetchInterval: 60 * 1000, - }) + }); // Show error toast if connections fail to load useEffect(() => { if (error) { toast.error("Failed to load connections", { description: error instanceof Error ? error.message : "Unknown error", - }) + }); } - }, [error]) + }, [error]); // Add connection mutation const addConnectionMutation = useMutation({ @@ -115,7 +117,7 @@ export function ConnectionsTabContent() { if (!canAddConnection && !isProUser) { throw new Error( "Free plan doesn't include connections. Upgrade to Pro for unlimited connections.", - ) + ); } const response = await $fetch("@post/connections/:provider", { @@ -124,61 +126,61 @@ export function ConnectionsTabContent() { redirectUrl: window.location.href, containerTags: [selectedProject], }, - }) + }); // biome-ignore lint/style/noNonNullAssertion: its fine if ("data" in response && !("error" in response.data!)) { - return response.data + return response.data; } - throw new Error(response.error?.message || "Failed to connect") + throw new Error(response.error?.message || "Failed to connect"); }, onSuccess: (data, provider) => { - analytics.connectionAdded(provider) - analytics.connectionAuthStarted() + analytics.connectionAdded(provider); + analytics.connectionAuthStarted(); autumn.track({ featureId: "connections", value: 1, - }) + }); if (data?.authLink) { - window.location.href = data.authLink + window.location.href = data.authLink; } }, onError: (error, provider) => { - analytics.connectionAuthFailed() + analytics.connectionAuthFailed(); toast.error(`Failed to connect ${provider}`, { description: error instanceof Error ? error.message : "Unknown error", - }) + }); }, - }) + }); // Delete connection mutation const deleteConnectionMutation = useMutation({ mutationFn: async (connectionId: string) => { - await $fetch(`@delete/connections/${connectionId}`) + await $fetch(`@delete/connections/${connectionId}`); }, onSuccess: () => { - analytics.connectionDeleted() + analytics.connectionDeleted(); toast.success( "Connection removal has started. supermemory will permanently delete the documents in the next few minutes.", - ) - queryClient.invalidateQueries({ queryKey: ["connections"] }) + ); + queryClient.invalidateQueries({ queryKey: ["connections"] }); }, onError: (error) => { toast.error("Failed to remove connection", { description: error instanceof Error ? error.message : "Unknown error", - }) + }); }, - }) + }); const getProviderIcon = (provider: string) => { - const connector = CONNECTORS[provider as ConnectorProvider] + const connector = CONNECTORS[provider as ConnectorProvider]; if (connector) { - const Icon = connector.icon - return <Icon className="h-10 w-10" /> + const Icon = connector.icon; + return <Icon className="h-10 w-10" />; } - return <span className="text-2xl">📎</span> - } + return <span className="text-2xl">📎</span>; + }; return ( <div className="space-y-4"> @@ -310,7 +312,7 @@ export function ConnectionsTabContent() { </h3> <div className="grid gap-3"> {Object.entries(CONNECTORS).map(([provider, config], index) => { - const Icon = config.icon + const Icon = config.icon; return ( <motion.div animate={{ opacity: 1, y: 0 }} @@ -324,7 +326,7 @@ export function ConnectionsTabContent() { className="justify-start h-auto p-4 bg-white/5 hover:bg-white/10 border-white/10 text-white w-full" disabled={addConnectionMutation.isPending} onClick={() => { - addConnectionMutation.mutate(provider as ConnectorProvider) + addConnectionMutation.mutate(provider as ConnectorProvider); }} variant="outline" > @@ -337,10 +339,10 @@ export function ConnectionsTabContent() { </div> </Button> </motion.div> - ) + ); })} </div> </div> </div> - ) + ); } diff --git a/apps/web/components/views/integrations.tsx b/apps/web/components/views/integrations.tsx index b3e3c92d..1724231f 100644 --- a/apps/web/components/views/integrations.tsx +++ b/apps/web/components/views/integrations.tsx @@ -1,35 +1,35 @@ -import { $fetch } from "@lib/api" -import { authClient } from "@lib/auth" -import { useAuth } from "@lib/auth-context" -import { generateId } from "@lib/generate-id" +import { $fetch } from "@lib/api"; +import { authClient } from "@lib/auth"; +import { useAuth } from "@lib/auth-context"; +import { generateId } from "@lib/generate-id"; import { ADD_MEMORY_SHORTCUT_URL, SEARCH_MEMORY_SHORTCUT_URL, -} from "@repo/lib/constants" -import { fetchConnectionsFeature } from "@repo/lib/queries" -import { Button } from "@repo/ui/components/button" +} from "@repo/lib/constants"; +import { fetchConnectionsFeature } from "@repo/lib/queries"; +import { Button } from "@repo/ui/components/button"; import { Dialog, DialogContent, DialogHeader, DialogPortal, DialogTitle, -} from "@repo/ui/components/dialog" -import { Skeleton } from "@repo/ui/components/skeleton" -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 { motion } from "motion/react" -import Image from "next/image" -import { useEffect, useId, useState } from "react" -import { toast } from "sonner" -import type { z } from "zod" -import { analytics } from "@/lib/analytics" -import { useProject } from "@/stores" - -type Connection = z.infer<typeof ConnectionResponseSchema> +} from "@repo/ui/components/dialog"; +import { Skeleton } from "@repo/ui/components/skeleton"; +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 { motion } from "motion/react"; +import Image from "next/image"; +import { useEffect, useId, useState } from "react"; +import { toast } from "sonner"; +import type { z } from "zod"; +import { analytics } from "@/lib/analytics"; +import { useProject } from "@/stores"; + +type Connection = z.infer<typeof ConnectionResponseSchema>; const CONNECTORS = { "google-drive": { @@ -47,66 +47,66 @@ const CONNECTORS = { description: "Access your Microsoft Office documents", icon: OneDrive, }, -} as const +} as const; -type ConnectorProvider = keyof typeof CONNECTORS +type ConnectorProvider = keyof typeof CONNECTORS; const ChromeIcon = ({ className }: { className?: string }) => ( <svg - xmlns="http://www.w3.org/2000/svg" + className={className} preserveAspectRatio="xMidYMid" viewBox="0 0 190.5 190.5" - className={className} + xmlns="http://www.w3.org/2000/svg" > <title>Google Chrome Icon</title> <path - fill="#fff" d="M95.252 142.873c26.304 0 47.627-21.324 47.627-47.628s-21.323-47.628-47.627-47.628-47.627 21.324-47.627 47.628 21.323 47.628 47.627 47.628z" + fill="#fff" /> <path - fill="#229342" d="m54.005 119.07-41.24-71.43a95.227 95.227 0 0 0-.003 95.25 95.234 95.234 0 0 0 82.496 47.61l41.24-71.43v-.011a47.613 47.613 0 0 1-17.428 17.443 47.62 47.62 0 0 1-47.632.007 47.62 47.62 0 0 1-17.433-17.437z" + fill="#229342" /> <path - fill="#fbc116" d="m136.495 119.067-41.239 71.43a95.229 95.229 0 0 0 82.489-47.622A95.24 95.24 0 0 0 190.5 95.248a95.237 95.237 0 0 0-12.772-47.623H95.249l-.01.007a47.62 47.62 0 0 1 23.819 6.372 47.618 47.618 0 0 1 17.439 17.431 47.62 47.62 0 0 1-.001 47.633z" + fill="#fbc116" /> <path - fill="#1a73e8" d="M95.252 132.961c20.824 0 37.705-16.881 37.705-37.706S116.076 57.55 95.252 57.55 57.547 74.431 57.547 95.255s16.881 37.706 37.705 37.706z" + fill="#1a73e8" /> <path - fill="#e33b2e" d="M95.252 47.628h82.479A95.237 95.237 0 0 0 142.87 12.76 95.23 95.23 0 0 0 95.245 0a95.222 95.222 0 0 0-47.623 12.767 95.23 95.23 0 0 0-34.856 34.872l41.24 71.43.011.006a47.62 47.62 0 0 1-.015-47.633 47.61 47.61 0 0 1 41.252-23.815z" + fill="#e33b2e" /> </svg> -) +); export function IntegrationsView() { - const { org } = useAuth() - const queryClient = useQueryClient() - const { selectedProject } = useProject() - const autumn = useCustomer() - const [showApiKeyModal, setShowApiKeyModal] = useState(false) - const [apiKey, setApiKey] = useState<string>("") - const [copied, setCopied] = useState(false) - const [isProUser, setIsProUser] = useState(false) + const { org } = useAuth(); + const queryClient = useQueryClient(); + const { selectedProject } = useProject(); + const autumn = useCustomer(); + const [showApiKeyModal, setShowApiKeyModal] = useState(false); + const [apiKey, setApiKey] = useState<string>(""); + const [copied, setCopied] = useState(false); + const [isProUser, setIsProUser] = useState(false); const [selectedShortcutType, setSelectedShortcutType] = useState< "add" | "search" | null - >(null) - const apiKeyId = useId() + >(null); + const apiKeyId = useId(); const handleUpgrade = async () => { try { await autumn.attach({ productId: "consumer_pro", successUrl: "https://app.supermemory.ai/", - }) - window.location.reload() + }); + window.location.reload(); } catch (error) { - console.error(error) + console.error(error); } - } + }; useEffect(() => { if (!autumn.isLoading) { @@ -114,15 +114,15 @@ export function IntegrationsView() { autumn.customer?.products.some( (product) => product.id === "consumer_pro", ) ?? false, - ) + ); } - }, [autumn.isLoading, autumn.customer]) + }, [autumn.isLoading, autumn.customer]); - const { data: connectionsCheck } = fetchConnectionsFeature(autumn) - const connectionsUsed = connectionsCheck?.balance ?? 0 - const connectionsLimit = connectionsCheck?.included_usage ?? 0 + const { data: connectionsCheck } = fetchConnectionsFeature(autumn); + const connectionsUsed = connectionsCheck?.balance ?? 0; + const connectionsLimit = connectionsCheck?.included_usage ?? 0; - const canAddConnection = connectionsUsed < connectionsLimit + const canAddConnection = connectionsUsed < connectionsLimit; const { data: connections = [], @@ -135,17 +135,19 @@ export function IntegrationsView() { body: { containerTags: [], }, - }) + }); if (response.error) { - throw new Error(response.error?.message || "Failed to load connections") + throw new Error( + response.error?.message || "Failed to load connections", + ); } - return response.data as Connection[] + return response.data as Connection[]; }, staleTime: 30 * 1000, refetchInterval: 60 * 1000, - }) + }); useEffect(() => { if (connectionsError) { @@ -154,16 +156,16 @@ export function IntegrationsView() { connectionsError instanceof Error ? connectionsError.message : "Unknown error", - }) + }); } - }, [connectionsError]) + }, [connectionsError]); const addConnectionMutation = useMutation({ mutationFn: async (provider: ConnectorProvider) => { if (!canAddConnection && !isProUser) { throw new Error( "Free plan doesn't include connections. Upgrade to Pro for unlimited connections.", - ) + ); } const response = await $fetch("@post/connections/:provider", { @@ -172,47 +174,47 @@ export function IntegrationsView() { redirectUrl: window.location.href, containerTags: [selectedProject], }, - }) + }); // biome-ignore lint/style/noNonNullAssertion: its fine if ("data" in response && !("error" in response.data!)) { - return response.data + return response.data; } - throw new Error(response.error?.message || "Failed to connect") + throw new Error(response.error?.message || "Failed to connect"); }, onSuccess: (data, provider) => { - analytics.connectionAdded(provider) - analytics.connectionAuthStarted() + analytics.connectionAdded(provider); + analytics.connectionAuthStarted(); if (data?.authLink) { - window.location.href = data.authLink + window.location.href = data.authLink; } }, onError: (error, provider) => { - analytics.connectionAuthFailed() + analytics.connectionAuthFailed(); toast.error(`Failed to connect ${provider}`, { description: error instanceof Error ? error.message : "Unknown error", - }) + }); }, - }) + }); const deleteConnectionMutation = useMutation({ mutationFn: async (connectionId: string) => { - await $fetch(`@delete/connections/${connectionId}`) + await $fetch(`@delete/connections/${connectionId}`); }, onSuccess: () => { - analytics.connectionDeleted() + analytics.connectionDeleted(); toast.success( "Connection removal has started. supermemory will permanently delete all documents related to the connection in the next few minutes.", - ) - queryClient.invalidateQueries({ queryKey: ["connections"] }) + ); + queryClient.invalidateQueries({ queryKey: ["connections"] }); }, onError: (error) => { toast.error("Failed to remove connection", { description: error instanceof Error ? error.message : "Unknown error", - }) + }); }, - }) + }); const createApiKeyMutation = useMutation({ mutationFn: async () => { @@ -223,60 +225,60 @@ export function IntegrationsView() { }, name: `ios-${generateId().slice(0, 8)}`, prefix: `sm_${org?.id}_`, - }) - return res.key + }); + return res.key; }, onSuccess: (apiKey) => { - setApiKey(apiKey) - setShowApiKeyModal(true) - setCopied(false) - handleCopyApiKey() + setApiKey(apiKey); + setShowApiKeyModal(true); + setCopied(false); + handleCopyApiKey(); }, onError: (error) => { toast.error("Failed to create API key", { description: error instanceof Error ? error.message : "Unknown error", - }) + }); }, - }) + }); const handleShortcutClick = (shortcutType: "add" | "search") => { - setSelectedShortcutType(shortcutType) - createApiKeyMutation.mutate() - } + setSelectedShortcutType(shortcutType); + createApiKeyMutation.mutate(); + }; const handleCopyApiKey = async () => { try { - await navigator.clipboard.writeText(apiKey) - setCopied(true) - toast.success("API key copied to clipboard!") - setTimeout(() => setCopied(false), 2000) + await navigator.clipboard.writeText(apiKey); + setCopied(true); + toast.success("API key copied to clipboard!"); + setTimeout(() => setCopied(false), 2000); } catch { - toast.error("Failed to copy API key") + toast.error("Failed to copy API key"); } - } + }; const handleOpenShortcut = () => { if (!selectedShortcutType) { - toast.error("No shortcut type selected") - return + toast.error("No shortcut type selected"); + return; } if (selectedShortcutType === "add") { - window.open(ADD_MEMORY_SHORTCUT_URL, "_blank") + window.open(ADD_MEMORY_SHORTCUT_URL, "_blank"); } else if (selectedShortcutType === "search") { - window.open(SEARCH_MEMORY_SHORTCUT_URL, "_blank") + window.open(SEARCH_MEMORY_SHORTCUT_URL, "_blank"); } - } + }; const handleDialogClose = (open: boolean) => { - setShowApiKeyModal(open) + setShowApiKeyModal(open); if (!open) { // Reset state when dialog closes - setSelectedShortcutType(null) - setApiKey("") - setCopied(false) + setSelectedShortcutType(null); + setApiKey(""); + setCopied(false); } - } + }; return ( <div className="space-y-4 sm:space-y-4 custom-scrollbar"> @@ -298,32 +300,32 @@ export function IntegrationsView() { </div> <div className="flex flex-col sm:flex-row gap-2 sm:gap-3"> <Button - variant="ghost" className="flex-1 text-white hover:bg-blue-500/10 bg-[#171F59]/75 " - onClick={() => handleShortcutClick("add")} disabled={createApiKeyMutation.isPending} + onClick={() => handleShortcutClick("add")} + variant="ghost" > <Image - src="/images/ios-shortcuts.png" alt="iOS Shortcuts" - width={20} height={20} + src="/images/ios-shortcuts.png" + width={20} /> {createApiKeyMutation.isPending ? "Creating..." : "Add Memory Shortcut"} </Button> <Button - variant="ghost" className="flex-1 text-white hover:bg-blue-500/10 bg-[#171F59]/75" - onClick={() => handleShortcutClick("search")} disabled={createApiKeyMutation.isPending} + onClick={() => handleShortcutClick("search")} + variant="ghost" > <Image - src="/images/ios-shortcuts.png" alt="iOS Shortcuts" - width={20} height={20} + src="/images/ios-shortcuts.png" + width={20} /> {createApiKeyMutation.isPending ? "Creating..." @@ -352,8 +354,8 @@ export function IntegrationsView() { "https://chromewebstore.google.com/detail/supermemory/afpgkkipfdpeaflnpoaffkcankadgjfc", "_blank", "noopener,noreferrer", - ) - analytics.extensionInstallClicked() + ); + analytics.extensionInstallClicked(); }} size="sm" variant="ghost" @@ -465,11 +467,11 @@ export function IntegrationsView() { ) : ( <div className="space-y-2"> {Object.entries(CONNECTORS).map(([provider, config], index) => { - const Icon = config.icon + const Icon = config.icon; const connection = connections.find( (conn) => conn.provider === provider, - ) - const isConnected = !!connection + ); + const isConnected = !!connection; return ( <motion.div @@ -548,9 +550,9 @@ export function IntegrationsView() { </span> </div> <motion.div + className="flex-shrink-0" whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} - className="flex-shrink-0" > <Button className="bg-blue-600/20 hover:bg-blue-600/30 text-blue-400 border-blue-600/30 min-w-[80px] disabled:cursor-not-allowed" @@ -560,7 +562,7 @@ export function IntegrationsView() { onClick={() => { addConnectionMutation.mutate( provider as ConnectorProvider, - ) + ); }} size="sm" variant="outline" @@ -575,7 +577,7 @@ export function IntegrationsView() { )} </div> </motion.div> - ) + ); })} </div> )} @@ -587,10 +589,10 @@ export function IntegrationsView() { More integrations are coming soon! Have a suggestion? Share it with us on{" "} <a + className="text-orange-500 hover:text-orange-400 underline" href="https://x.com/supermemoryai" - target="_blank" rel="noopener noreferrer" - className="text-orange-500 hover:text-orange-400 underline" + target="_blank" > X </a> @@ -599,7 +601,7 @@ export function IntegrationsView() { </div> {/* API Key Modal */} - <Dialog open={showApiKeyModal} onOpenChange={handleDialogClose}> + <Dialog onOpenChange={handleDialogClose} open={showApiKeyModal}> <DialogPortal> <DialogContent className="bg-[#0f1419] border-white/10 text-white md:max-w-md z-[100]"> <DialogHeader> @@ -618,24 +620,24 @@ export function IntegrationsView() { {/* API Key Section */} <div className="space-y-2"> <label - htmlFor={apiKeyId} className="text-sm font-medium text-white/80" + htmlFor={apiKeyId} > Your API Key </label> <div className="flex items-center gap-2"> <input + className="flex-1 bg-white/5 border border-white/20 rounded-lg px-3 py-2 text-sm text-white font-mono" id={apiKeyId} + readOnly type="text" value={apiKey} - readOnly - className="flex-1 bg-white/5 border border-white/20 rounded-lg px-3 py-2 text-sm text-white font-mono" /> <Button + className="text-white/70 hover:text-white hover:bg-white/10" + onClick={handleCopyApiKey} size="sm" variant="ghost" - onClick={handleCopyApiKey} - className="text-white/70 hover:text-white hover:bg-white/10" > {copied ? ( <Check className="h-4 w-4 text-green-400" /> @@ -681,16 +683,16 @@ export function IntegrationsView() { <div className="flex gap-2 pt-2"> <Button - onClick={handleOpenShortcut} className="flex-1 bg-blue-600 hover:bg-blue-700 text-white" disabled={!selectedShortcutType} + onClick={handleOpenShortcut} > <Image - src="/images/ios-shortcuts.png" alt="iOS Shortcuts" - width={16} - height={16} className="mr-2" + height={16} + src="/images/ios-shortcuts.png" + width={16} /> Add to Shortcuts </Button> @@ -700,5 +702,5 @@ export function IntegrationsView() { </DialogPortal> </Dialog> </div> - ) + ); } diff --git a/apps/web/components/views/mcp/index.tsx b/apps/web/components/views/mcp/index.tsx index e79c2716..c2cc1d5c 100644 --- a/apps/web/components/views/mcp/index.tsx +++ b/apps/web/components/views/mcp/index.tsx @@ -1,9 +1,9 @@ -import { $fetch } from "@lib/api" -import { authClient } from "@lib/auth" -import { useAuth } from "@lib/auth-context" -import { useForm } from "@tanstack/react-form" -import { useMutation } from "@tanstack/react-query" -import { Button } from "@ui/components/button" +import { $fetch } from "@lib/api"; +import { authClient } from "@lib/auth"; +import { useAuth } from "@lib/auth-context"; +import { useForm } from "@tanstack/react-form"; +import { useMutation } from "@tanstack/react-query"; +import { Button } from "@ui/components/button"; import { Dialog, DialogContent, @@ -11,18 +11,18 @@ import { DialogHeader, DialogTitle, DialogTrigger, -} from "@ui/components/dialog" -import { Input } from "@ui/components/input" -import { CopyableCell } from "@ui/copyable-cell" -import { Loader2 } from "lucide-react" -import { AnimatePresence, motion } from "motion/react" -import Image from "next/image" -import { generateSlug } from "random-word-slugs" -import { useEffect, useState } from "react" -import { toast } from "sonner" -import { z } from "zod/v4" -import { analytics } from "@/lib/analytics" -import { InstallationDialogContent } from "./installation-dialog-content" +} from "@ui/components/dialog"; +import { Input } from "@ui/components/input"; +import { CopyableCell } from "@ui/copyable-cell"; +import { Loader2 } from "lucide-react"; +import { AnimatePresence, motion } from "motion/react"; +import Image from "next/image"; +import { generateSlug } from "random-word-slugs"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; +import { z } from "zod/v4"; +import { analytics } from "@/lib/analytics"; +import { InstallationDialogContent } from "./installation-dialog-content"; // Validation schemas const mcpMigrationSchema = z.object({ @@ -33,56 +33,56 @@ const mcpMigrationSchema = z.object({ /^https:\/\/mcp\.supermemory\.ai\/[^/]+\/sse$/, "Link must be in format: https://mcp.supermemory.ai/userId/sse", ), -}) +}); export function MCPView() { - const [isMigrateDialogOpen, setIsMigrateDialogOpen] = useState(false) - const projectId = localStorage.getItem("selectedProject") ?? "default" - const { org } = useAuth() - const [apiKey, setApiKey] = useState<string>() - const [isInstallDialogOpen, setIsInstallDialogOpen] = useState(false) + const [isMigrateDialogOpen, setIsMigrateDialogOpen] = useState(false); + const projectId = localStorage.getItem("selectedProject") ?? "default"; + const { org } = useAuth(); + const [apiKey, setApiKey] = useState<string>(); + const [isInstallDialogOpen, setIsInstallDialogOpen] = useState(false); useEffect(() => { - analytics.mcpViewOpened() - }, []) + analytics.mcpViewOpened(); + }, []); const apiKeyMutation = useMutation({ mutationFn: async () => { - if (apiKey) return apiKey + if (apiKey) return apiKey; const res = await authClient.apiKey.create({ metadata: { organizationId: org?.id, }, name: generateSlug(), prefix: `sm_${org?.id}_`, - }) - return res.key + }); + return res.key; }, onSuccess: (data) => { - setApiKey(data) - setIsInstallDialogOpen(true) + setApiKey(data); + setIsInstallDialogOpen(true); }, - }) + }); // Form for MCP migration const mcpMigrationForm = useForm({ defaultValues: { url: "" }, onSubmit: async ({ value, formApi }) => { - const userId = extractUserIdFromMCPUrl(value.url) + const userId = extractUserIdFromMCPUrl(value.url); if (userId) { - migrateMCPMutation.mutate({ userId, projectId }) - formApi.reset() + migrateMCPMutation.mutate({ userId, projectId }); + formApi.reset(); } }, validators: { onChange: mcpMigrationSchema, }, - }) + }); const extractUserIdFromMCPUrl = (url: string): string | null => { - const regex = /^https:\/\/mcp\.supermemory\.ai\/([^/]+)\/sse$/ - const match = url.trim().match(regex) - return match?.[1] || null - } + const regex = /^https:\/\/mcp\.supermemory\.ai\/([^/]+)\/sse$/; + const match = url.trim().match(regex); + return match?.[1] || null; + }; // Migrate MCP mutation const migrateMCPMutation = useMutation({ @@ -90,33 +90,33 @@ export function MCPView() { userId, projectId, }: { - userId: string - projectId: string + userId: string; + projectId: string; }) => { const response = await $fetch("@post/documents/migrate-mcp", { body: { userId, projectId }, - }) + }); if (response.error) { throw new Error( response.error?.message || "Failed to migrate documents", - ) + ); } - return response.data + return response.data; }, onSuccess: (data) => { toast.success("Migration completed!", { description: `Successfully migrated ${data?.migratedCount} documents`, - }) - setIsMigrateDialogOpen(false) + }); + setIsMigrateDialogOpen(false); }, onError: (error) => { toast.error("Migration failed", { description: error instanceof Error ? error.message : "Unknown error", - }) + }); }, - }) + }); return ( <div className="space-y-6"> @@ -155,9 +155,9 @@ export function MCPView() { <Button disabled={apiKeyMutation.isPending} onClick={(e) => { - e.preventDefault() - e.stopPropagation() - apiKeyMutation.mutate() + e.preventDefault(); + e.stopPropagation(); + apiKeyMutation.mutate(); }} > Install Now @@ -213,9 +213,9 @@ export function MCPView() { </DialogHeader> <form onSubmit={(e) => { - e.preventDefault() - e.stopPropagation() - mcpMigrationForm.handleSubmit() + e.preventDefault(); + e.stopPropagation(); + mcpMigrationForm.handleSubmit(); }} > <div className="grid gap-4"> @@ -268,8 +268,8 @@ export function MCPView() { <Button className="bg-white/5 hover:bg-white/10 border-white/10 text-white" onClick={() => { - setIsMigrateDialogOpen(false) - mcpMigrationForm.reset() + setIsMigrateDialogOpen(false); + mcpMigrationForm.reset(); }} type="button" variant="outline" @@ -307,5 +307,5 @@ export function MCPView() { )} </AnimatePresence> </div> - ) + ); } diff --git a/apps/web/components/views/mcp/installation-dialog-content.tsx b/apps/web/components/views/mcp/installation-dialog-content.tsx index 4c04c6ac..3150c098 100644 --- a/apps/web/components/views/mcp/installation-dialog-content.tsx +++ b/apps/web/components/views/mcp/installation-dialog-content.tsx @@ -1,3 +1,5 @@ +import { $fetch } from "@repo/lib/api"; +import { useQuery } from "@tanstack/react-query"; import { Button } from "@ui/components/button"; import { DialogContent, @@ -6,6 +8,7 @@ import { DialogTitle, } from "@ui/components/dialog"; import { Input } from "@ui/components/input"; +import { Label } from "@ui/components/label"; import { Select, SelectContent, @@ -13,13 +16,10 @@ import { SelectTrigger, SelectValue, } from "@ui/components/select"; -import { Label } from "@ui/components/label"; import { CopyIcon } from "lucide-react"; import { useState } from "react"; import { toast } from "sonner"; import { analytics } from "@/lib/analytics"; -import { $fetch } from "@repo/lib/api"; -import { useQuery } from "@tanstack/react-query"; const clients = { cursor: "Cursor", @@ -67,7 +67,7 @@ export function InstallationDialogContent() { if (selectedProject && selectedProject !== "none") { // Remove the "sm_project_" prefix from the containerTag - const projectId = selectedProject.replace(/^sm_project_/, ''); + const projectId = selectedProject.replace(/^sm_project_/, ""); command += ` --project ${projectId}`; } @@ -79,8 +79,8 @@ export function InstallationDialogContent() { <DialogHeader> <DialogTitle>Install the supermemory MCP Server</DialogTitle> <DialogDescription> - Select the app and project you want to install supermemory MCP to, then run the - following command: + Select the app and project you want to install supermemory MCP to, + then run the following command: </DialogDescription> </DialogHeader> @@ -91,7 +91,7 @@ export function InstallationDialogContent() { onValueChange={(value) => setClient(value as keyof typeof clients)} value={client} > - <SelectTrigger id="client-select" className="w-full"> + <SelectTrigger className="w-full" id="client-select"> <SelectValue placeholder="Select client" /> </SelectTrigger> <SelectContent> @@ -107,27 +107,30 @@ export function InstallationDialogContent() { <div className="space-y-2"> <Label htmlFor="project-select">Target Project (Optional)</Label> <Select + disabled={isLoadingProjects} onValueChange={setSelectedProject} value={selectedProject || "none"} - disabled={isLoadingProjects} > - <SelectTrigger id="project-select" className="w-full"> + <SelectTrigger className="w-full" id="project-select"> <SelectValue placeholder="Select project" /> </SelectTrigger> <SelectContent className="bg-black/90 backdrop-blur-xl border-white/10"> - <SelectItem value="none" className="text-white hover:bg-white/10"> + <SelectItem className="text-white hover:bg-white/10" value="none"> Auto-select project </SelectItem> - <SelectItem value="sm_project_default" className="text-white hover:bg-white/10"> + <SelectItem + className="text-white hover:bg-white/10" + value="sm_project_default" + > Default Project </SelectItem> {projects .filter((p: Project) => p.containerTag !== "sm_project_default") .map((project: Project) => ( <SelectItem + className="text-white hover:bg-white/10" key={project.id} value={project.containerTag} - className="text-white hover:bg-white/10" > {project.name} </SelectItem> @@ -139,8 +142,8 @@ export function InstallationDialogContent() { <div className="space-y-2"> <Label htmlFor="command-input">Installation Command</Label> <Input - id="command-input" className="font-mono text-xs!" + id="command-input" readOnly value={generateInstallCommand()} /> diff --git a/apps/web/components/views/profile.tsx b/apps/web/components/views/profile.tsx index 9fa086ec..9b3dc387 100644 --- a/apps/web/components/views/profile.tsx +++ b/apps/web/components/views/profile.tsx @@ -1,10 +1,11 @@ -"use client" +"use client"; -import { authClient } from "@lib/auth" -import { useAuth } from "@lib/auth-context" -import { Button } from "@repo/ui/components/button" -import { HeadingH3Bold } from "@repo/ui/text/heading/heading-h3-bold" -import { useCustomer } from "autumn-js/react" +import { $fetch } from "@lib/api"; +import { authClient } from "@lib/auth"; +import { useAuth } from "@lib/auth-context"; +import { Button } from "@repo/ui/components/button"; +import { HeadingH3Bold } from "@repo/ui/text/heading/heading-h3-bold"; +import { useCustomer } from "autumn-js/react"; import { CheckCircle, CreditCard, @@ -12,48 +13,47 @@ import { LogOut, User, X, -} from "lucide-react" -import { motion } from "motion/react" -import Link from "next/link" -import { useRouter } from "next/navigation" -import { useEffect, useState } from "react" -import { analytics } from "@/lib/analytics" -import { $fetch } from "@lib/api" +} from "lucide-react"; +import { motion } from "motion/react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import { analytics } from "@/lib/analytics"; export function ProfileView() { - const router = useRouter() - const { user: session } = useAuth() + const router = useRouter(); + const { user: session } = useAuth(); const { customer, isLoading: isCustomerLoading, openBillingPortal, attach, - } = useCustomer() - const [isLoading, setIsLoading] = useState(false) + } = useCustomer(); + const [isLoading, setIsLoading] = useState(false); const [billingData, setBillingData] = useState<{ - isPro: boolean - memoriesUsed: number - memoriesLimit: number - connectionsUsed: number - connectionsLimit: number + isPro: boolean; + memoriesUsed: number; + memoriesLimit: number; + connectionsUsed: number; + connectionsLimit: number; }>({ isPro: false, memoriesUsed: 0, memoriesLimit: 0, connectionsUsed: 0, connectionsLimit: 0, - }) + }); useEffect(() => { if (!isCustomerLoading) { const memoriesFeature = customer?.features?.memories ?? { usage: 0, included_usage: 0, - } + }; const connectionsFeature = customer?.features?.connections ?? { usage: 0, included_usage: 0, - } + }; setBillingData({ isPro: @@ -64,30 +64,30 @@ export function ProfileView() { memoriesLimit: memoriesFeature?.included_usage ?? 0, connectionsUsed: connectionsFeature?.usage ?? 0, connectionsLimit: connectionsFeature?.included_usage ?? 0, - }) + }); } - }, [isCustomerLoading, customer]) + }, [isCustomerLoading, customer]); const handleLogout = () => { - analytics.userSignedOut() - authClient.signOut() - router.push("/login") - } + analytics.userSignedOut(); + authClient.signOut(); + router.push("/login"); + }; const handleUpgrade = async () => { - setIsLoading(true) + setIsLoading(true); try { const upgradeResult = await attach({ productId: "consumer_pro", successUrl: "https://app.supermemory.ai/", - }) + }); if ( upgradeResult.statusCode === 200 && upgradeResult.data && "code" in upgradeResult.data ) { const isProPlanActivated = - upgradeResult.data.code === "new_product_attached" + upgradeResult.data.code === "new_product_attached"; if (isProPlanActivated && session?.name && session?.email) { try { await $fetch("@post/emails/welcome/pro", { @@ -95,24 +95,24 @@ export function ProfileView() { email: session?.email, firstName: session?.name, }, - }) + }); } catch (error) { - console.error(error) + console.error(error); } } } } catch (error) { - console.error(error) - setIsLoading(false) + console.error(error); + setIsLoading(false); } - } + }; // Handle manage billing const handleManageBilling = async () => { await openBillingPortal({ returnUrl: "https://app.supermemory.ai", - }) - } + }); + }; if (session?.isAnonymous) { return ( @@ -137,7 +137,7 @@ export function ProfileView() { </motion.div> </motion.div> </div> - ) + ); } return ( @@ -337,5 +337,5 @@ export function ProfileView() { Sign Out </Button> </div> - ) + ); } diff --git a/apps/web/instrumentation-client.ts b/apps/web/instrumentation-client.ts index 2c9c9e2d..659af99f 100644 --- a/apps/web/instrumentation-client.ts +++ b/apps/web/instrumentation-client.ts @@ -2,29 +2,29 @@ // The added config here will be used whenever a users loads a page in their browser. // https://docs.sentry.io/platforms/javascript/guides/nextjs/ -import * as Sentry from '@sentry/nextjs'; +import * as Sentry from "@sentry/nextjs"; Sentry.init({ - dsn: 'https://2451ebfd1a7490f05fa7776482df81b6@o4508385422802944.ingest.us.sentry.io/4509872269819904', + dsn: "https://2451ebfd1a7490f05fa7776482df81b6@o4508385422802944.ingest.us.sentry.io/4509872269819904", - // Add optional integrations for additional features - integrations: [Sentry.replayIntegration()], + // Add optional integrations for additional features + integrations: [Sentry.replayIntegration()], - // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. - tracesSampleRate: 1, - // Enable logs to be sent to Sentry - enableLogs: true, + // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. + tracesSampleRate: 1, + // Enable logs to be sent to Sentry + enableLogs: true, - // Define how likely Replay events are sampled. - // This sets the sample rate to be 10%. You may want this to be 100% while - // in development and sample at a lower rate in production - replaysSessionSampleRate: 0.1, + // Define how likely Replay events are sampled. + // This sets the sample rate to be 10%. You may want this to be 100% while + // in development and sample at a lower rate in production + replaysSessionSampleRate: 0.1, - // Define how likely Replay events are sampled when an error occurs. - replaysOnErrorSampleRate: 1.0, + // Define how likely Replay events are sampled when an error occurs. + replaysOnErrorSampleRate: 1.0, - // Setting this option to true will print useful information to the console while you're setting up Sentry. - debug: false, + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, }); export const onRouterTransitionStart = Sentry.captureRouterTransitionStart; diff --git a/apps/web/lib/analytics.ts b/apps/web/lib/analytics.ts index 28f1d4fe..282c75b4 100644 --- a/apps/web/lib/analytics.ts +++ b/apps/web/lib/analytics.ts @@ -1,4 +1,4 @@ -import posthog from "posthog-js" +import posthog from "posthog-js"; export const analytics = { userSignedOut: () => posthog.capture("user_signed_out"), @@ -7,11 +7,11 @@ export const analytics = { tourSkipped: () => posthog.capture("tour_skipped"), memoryAdded: (props: { - type: "note" | "link" | "file" - project_id?: string - content_length?: number - file_size?: number - file_type?: string + type: "note" | "link" | "file"; + project_id?: string; + content_length?: number; + file_size?: number; + file_type?: string; }) => posthog.capture("memory_added", props), memoryDetailOpened: () => posthog.capture("memory_detail_opened"), @@ -43,4 +43,4 @@ export const analytics = { mcpInstallCmdCopied: () => posthog.capture("mcp_install_cmd_copied"), extensionInstallClicked: () => posthog.capture("extension_install_clicked"), -} +}; diff --git a/apps/web/lib/document-icon.tsx b/apps/web/lib/document-icon.tsx index 3a80b2e0..d901e2e0 100644 --- a/apps/web/lib/document-icon.tsx +++ b/apps/web/lib/document-icon.tsx @@ -1,54 +1,54 @@ -import { colors } from '@repo/ui/memory-graph/constants'; +import { colors } from "@repo/ui/memory-graph/constants"; import { - GoogleDocs, - MicrosoftWord, - NotionDoc, - GoogleDrive, - GoogleSheets, - GoogleSlides, - PDF, - OneDrive, - MicrosoftOneNote, - MicrosoftPowerpoint, - MicrosoftExcel, -} from '@ui/assets/icons'; -import { FileText } from 'lucide-react'; + GoogleDocs, + GoogleDrive, + GoogleSheets, + GoogleSlides, + MicrosoftExcel, + MicrosoftOneNote, + MicrosoftPowerpoint, + MicrosoftWord, + NotionDoc, + OneDrive, + PDF, +} from "@ui/assets/icons"; +import { FileText } from "lucide-react"; export const getDocumentIcon = (type: string, className: string) => { - const iconProps = { - className, - style: { color: colors.text.muted }, - }; + const iconProps = { + className, + style: { color: colors.text.muted }, + }; - switch (type) { - case 'google_doc': - return <GoogleDocs {...iconProps} />; - case 'google_sheet': - return <GoogleSheets {...iconProps} />; - case 'google_slide': - return <GoogleSlides {...iconProps} />; - case 'google_drive': - return <GoogleDrive {...iconProps} />; - case 'notion': - case 'notion_doc': - return <NotionDoc {...iconProps} />; - case 'word': - case 'microsoft_word': - return <MicrosoftWord {...iconProps} />; - case 'excel': - case 'microsoft_excel': - return <MicrosoftExcel {...iconProps} />; - case 'powerpoint': - case 'microsoft_powerpoint': - return <MicrosoftPowerpoint {...iconProps} />; - case 'onenote': - case 'microsoft_onenote': - return <MicrosoftOneNote {...iconProps} />; - case 'onedrive': - return <OneDrive {...iconProps} />; - case 'pdf': - return <PDF {...iconProps} />; - default: - return <FileText {...iconProps} />; - } + switch (type) { + case "google_doc": + return <GoogleDocs {...iconProps} />; + case "google_sheet": + return <GoogleSheets {...iconProps} />; + case "google_slide": + return <GoogleSlides {...iconProps} />; + case "google_drive": + return <GoogleDrive {...iconProps} />; + case "notion": + case "notion_doc": + return <NotionDoc {...iconProps} />; + case "word": + case "microsoft_word": + return <MicrosoftWord {...iconProps} />; + case "excel": + case "microsoft_excel": + return <MicrosoftExcel {...iconProps} />; + case "powerpoint": + case "microsoft_powerpoint": + return <MicrosoftPowerpoint {...iconProps} />; + case "onenote": + case "microsoft_onenote": + return <MicrosoftOneNote {...iconProps} />; + case "onedrive": + return <OneDrive {...iconProps} />; + case "pdf": + return <PDF {...iconProps} />; + default: + return <FileText {...iconProps} />; + } }; diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index f9d24cd8..efd8f64b 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -1,30 +1,30 @@ -import { getSessionCookie } from "better-auth/cookies" -import { NextResponse } from "next/server" +import { getSessionCookie } from "better-auth/cookies"; +import { NextResponse } from "next/server"; export default async function middleware(request: Request) { - console.debug("[MIDDLEWARE] === MIDDLEWARE START ===") - const url = new URL(request.url) - console.debug("[MIDDLEWARE] Path:", url.pathname) - console.debug("[MIDDLEWARE] Method:", request.method) + console.debug("[MIDDLEWARE] === MIDDLEWARE START ==="); + const url = new URL(request.url); + console.debug("[MIDDLEWARE] Path:", url.pathname); + console.debug("[MIDDLEWARE] Method:", request.method); - const sessionCookie = getSessionCookie(request) - console.debug("[MIDDLEWARE] Session cookie exists:", !!sessionCookie) + const sessionCookie = getSessionCookie(request); + console.debug("[MIDDLEWARE] Session cookie exists:", !!sessionCookie); // Always allow access to login and waitlist pages - const publicPaths = ["/login"] + const publicPaths = ["/login"]; if (publicPaths.includes(url.pathname)) { - console.debug("[MIDDLEWARE] Public path, allowing access") - return NextResponse.next() + console.debug("[MIDDLEWARE] Public path, allowing access"); + return NextResponse.next(); } // If no session cookie and not on a public path, redirect to login if (!sessionCookie) { console.debug( "[MIDDLEWARE] No session cookie and not on public path, redirecting to /login", - ) - const url = new URL("/login", request.url) - url.searchParams.set("redirect", request.url) - return NextResponse.redirect(url) + ); + const url = new URL("/login", request.url); + url.searchParams.set("redirect", request.url); + return NextResponse.redirect(url); } // TEMPORARILY DISABLED: Waitlist check @@ -40,19 +40,19 @@ export default async function middleware(request: Request) { // } // } - console.debug("[MIDDLEWARE] Passing through to next handler") - console.debug("[MIDDLEWARE] === MIDDLEWARE END ===") - const response = NextResponse.next() + console.debug("[MIDDLEWARE] Passing through to next handler"); + console.debug("[MIDDLEWARE] === MIDDLEWARE END ==="); + const response = NextResponse.next(); response.cookies.set({ name: "last-site-visited", value: "https://app.supermemory.ai", domain: "supermemory.ai", - }) - return response + }); + return response; } export const config = { matcher: [ "/((?!_next/static|_next/image|images|icon.png|monitoring|opengraph-image.png|ingest|api|login|api/emails).*)", ], -} +}; diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index a6ee01f3..427faa0a 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -2,56 +2,56 @@ import { withSentryConfig } from "@sentry/nextjs"; import type { NextConfig } from "next"; const nextConfig: NextConfig = { - experimental: { - reactCompiler: true, - viewTransition: true, - }, - eslint: { - ignoreDuringBuilds: true, - }, - poweredByHeader: false, - async rewrites() { - return [ - { - source: "/ingest/static/:path*", - destination: "https://us-assets.i.posthog.com/static/:path*", - }, - { - source: "/ingest/:path*", - destination: "https://us.i.posthog.com/:path*", - }, - ]; - }, - skipTrailingSlashRedirect: true, + experimental: { + reactCompiler: true, + viewTransition: true, + }, + eslint: { + ignoreDuringBuilds: true, + }, + poweredByHeader: false, + async rewrites() { + return [ + { + source: "/ingest/static/:path*", + destination: "https://us-assets.i.posthog.com/static/:path*", + }, + { + source: "/ingest/:path*", + destination: "https://us.i.posthog.com/:path*", + }, + ]; + }, + skipTrailingSlashRedirect: true, }; export default withSentryConfig(nextConfig, { - // For all available options, see: + // For all available options, see: // https://www.npmjs.com/package/@sentry/webpack-plugin#options org: "supermemory", - project: "consumer-app", + project: "consumer-app", - // Only print logs for uploading source maps in CI + // Only print logs for uploading source maps in CI silent: !process.env.CI, - // For all available options, see: + // For all available options, see: // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ // Upload a larger set of source maps for prettier stack traces (increases build time) widenClientFileUpload: true, - // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. + // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. // This can increase your server load as well as your hosting bill. // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- // side errors will fail. tunnelRoute: "/monitoring", - // Automatically tree-shake Sentry logger statements to reduce bundle size + // Automatically tree-shake Sentry logger statements to reduce bundle size disableLogger: true, - // Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.) + // Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.) // See the following for more information: // https://docs.sentry.io/product/crons/ // https://vercel.com/docs/cron-jobs @@ -60,4 +60,4 @@ export default withSentryConfig(nextConfig, { import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare"; -initOpenNextCloudflareForDev();
\ No newline at end of file +initOpenNextCloudflareForDev(); diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 67626bbc..bdd64662 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -1,20 +1,20 @@ { - "compilerOptions": { - "incremental": true, - "jsx": "preserve", - "paths": { - "@/*": ["./*"], - "@ui/*": ["../../packages/ui/*"], - "@lib/*": ["../../packages/lib/*"], - "@hooks/*": ["../../packages/hooks/*"] - }, - "plugins": [ - { - "name": "next" - } - ] - }, - "exclude": ["node_modules"], - "extends": "@total-typescript/tsconfig/bundler/dom/app", - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"] + "compilerOptions": { + "incremental": true, + "jsx": "preserve", + "paths": { + "@/*": ["./*"], + "@ui/*": ["../../packages/ui/*"], + "@lib/*": ["../../packages/lib/*"], + "@hooks/*": ["../../packages/hooks/*"] + }, + "plugins": [ + { + "name": "next" + } + ] + }, + "exclude": ["node_modules"], + "extends": "@total-typescript/tsconfig/bundler/dom/app", + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"] } diff --git a/apps/web/wrangler.jsonc b/apps/web/wrangler.jsonc index dadfdf63..674f6270 100644 --- a/apps/web/wrangler.jsonc +++ b/apps/web/wrangler.jsonc @@ -1,31 +1,31 @@ { - "$schema": "node_modules/wrangler/config-schema.json", - "main": ".open-next/worker.js", - "name": "supermemory-app", - "compatibility_date": "2024-12-30", - "compatibility_flags": [ - // Enable Node.js API - // see https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag - "nodejs_compat", - // Allow to fetch URLs in your app - // see https://developers.cloudflare.com/workers/configuration/compatibility-flags/#global-fetch-strictly-public - "global_fetch_strictly_public", - ], - "assets": { - "directory": ".open-next/assets", - "binding": "ASSETS", - }, - "services": [ - { - "binding": "WORKER_SELF_REFERENCE", - // The service should match the "name" of your worker - "service": "supermemory-app", - }, - ], - "r2_buckets": [ - { - "binding": "NEXT_INC_CACHE_R2_BUCKET", - "bucket_name": "supermemory-console-cache", - }, - ], + "$schema": "node_modules/wrangler/config-schema.json", + "main": ".open-next/worker.js", + "name": "supermemory-app", + "compatibility_date": "2024-12-30", + "compatibility_flags": [ + // Enable Node.js API + // see https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag + "nodejs_compat", + // Allow to fetch URLs in your app + // see https://developers.cloudflare.com/workers/configuration/compatibility-flags/#global-fetch-strictly-public + "global_fetch_strictly_public" + ], + "assets": { + "directory": ".open-next/assets", + "binding": "ASSETS" + }, + "services": [ + { + "binding": "WORKER_SELF_REFERENCE", + // The service should match the "name" of your worker + "service": "supermemory-app" + } + ], + "r2_buckets": [ + { + "binding": "NEXT_INC_CACHE_R2_BUCKET", + "bucket_name": "supermemory-console-cache" + } + ] } |