aboutsummaryrefslogtreecommitdiff
path: root/apps/web
diff options
context:
space:
mode:
authorDhravya Shah <[email protected]>2025-09-18 20:34:18 -0700
committerDhravya Shah <[email protected]>2025-09-18 21:03:49 -0700
commit1fcb56908920da386900abb4ce2383374a625c72 (patch)
tree0f9d7f695d4c9b1b85be3950fc869e0061dff3ed /apps/web
parentrefetching logic change (diff)
downloadsupermemory-09-18-formatting.tar.xz
supermemory-09-18-formatting.zip
Diffstat (limited to 'apps/web')
-rw-r--r--apps/web/app/api/emails/welcome/route.tsx2
-rw-r--r--apps/web/app/global-error.tsx30
-rw-r--r--apps/web/app/layout.tsx9
-rw-r--r--apps/web/app/page.tsx326
-rw-r--r--apps/web/app/ref/[code]/page.tsx9
-rw-r--r--apps/web/app/upgrade-mcp/page.tsx104
-rw-r--r--apps/web/biome.json2
-rw-r--r--apps/web/components/connect-ai-modal.tsx6
-rw-r--r--apps/web/components/create-project-dialog.tsx20
-rw-r--r--apps/web/components/get-started.tsx58
-rw-r--r--apps/web/components/install-prompt.tsx16
-rw-r--r--apps/web/components/memories/index.tsx2
-rw-r--r--apps/web/components/memories/memory-detail.tsx775
-rw-r--r--apps/web/components/memory-list-view.tsx216
-rw-r--r--apps/web/components/menu.tsx211
-rw-r--r--apps/web/components/project-selector.tsx20
-rw-r--r--apps/web/components/referral-upgrade-modal.tsx14
-rw-r--r--apps/web/components/text-shimmer.tsx15
-rw-r--r--apps/web/components/views/add-memory/action-buttons.tsx118
-rw-r--r--apps/web/components/views/add-memory/fixed-mutation.tsx244
-rw-r--r--apps/web/components/views/add-memory/index.tsx417
-rw-r--r--apps/web/components/views/add-memory/memory-usage-ring.tsx23
-rw-r--r--apps/web/components/views/add-memory/project-selection.tsx164
-rw-r--r--apps/web/components/views/add-memory/tab-button.tsx42
-rw-r--r--apps/web/components/views/add-memory/text-editor.tsx22
-rw-r--r--apps/web/components/views/chat/index.tsx40
-rw-r--r--apps/web/components/views/connections-tab-content.tsx130
-rw-r--r--apps/web/components/views/integrations.tsx264
-rw-r--r--apps/web/components/views/mcp/index.tsx116
-rw-r--r--apps/web/components/views/mcp/installation-dialog-content.tsx29
-rw-r--r--apps/web/components/views/profile.tsx86
-rw-r--r--apps/web/instrumentation-client.ts32
-rw-r--r--apps/web/lib/analytics.ts14
-rw-r--r--apps/web/lib/document-icon.tsx98
-rw-r--r--apps/web/middleware.ts42
-rw-r--r--apps/web/next.config.ts58
-rw-r--r--apps/web/tsconfig.json36
-rw-r--r--apps/web/wrangler.jsonc58
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"
+ }
+ ]
}