aboutsummaryrefslogtreecommitdiff
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
parentrefetching logic change (diff)
downloadsupermemory-09-18-formatting.tar.xz
supermemory-09-18-formatting.zip
-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
-rw-r--r--bun.lock3
-rw-r--r--package.json2
-rw-r--r--packages/ai-sdk/src/tools.test.ts124
-rw-r--r--packages/ai-sdk/src/tools.ts48
-rw-r--r--packages/ai-sdk/tsdown.config.ts4
-rw-r--r--packages/hooks/use-keypress.ts16
-rw-r--r--packages/hooks/use-mobile.ts24
-rw-r--r--packages/lib/api.ts18
-rw-r--r--packages/lib/auth-context.tsx77
-rw-r--r--packages/lib/auth.middleware.ts6
-rw-r--r--packages/lib/auth.ts14
-rw-r--r--packages/lib/constants.ts10
-rw-r--r--packages/lib/error-tracking.tsx103
-rw-r--r--packages/lib/generate-id.ts4
-rw-r--r--packages/lib/glass-effect-manager.ts196
-rw-r--r--packages/lib/posthog.tsx46
-rw-r--r--packages/lib/queries.ts94
-rw-r--r--packages/lib/query-client.tsx14
-rw-r--r--packages/lib/similarity.ts50
-rw-r--r--packages/lib/utils.ts8
-rw-r--r--packages/tools/src/ai-sdk.ts44
-rw-r--r--packages/tools/src/index.ts2
-rw-r--r--packages/tools/src/openai.ts104
-rw-r--r--packages/tools/src/shared.ts16
-rw-r--r--packages/tools/src/tools.test.ts232
-rw-r--r--packages/tools/src/types.ts6
-rw-r--r--packages/tools/tsdown.config.ts4
-rw-r--r--packages/ui/memory-graph/hooks/use-graph-data.ts18
-rw-r--r--packages/ui/memory-graph/memory-graph.tsx64
-rw-r--r--packages/ui/memory-graph/navigation-controls.tsx97
-rw-r--r--packages/ui/pages/login.tsx4
-rw-r--r--packages/validation/api.ts104
-rw-r--r--packages/validation/connection.ts68
71 files changed, 2659 insertions, 2833 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"
+ }
+ ]
}
diff --git a/bun.lock b/bun.lock
index 94a534f2..1fe84b89 100644
--- a/bun.lock
+++ b/bun.lock
@@ -26,6 +26,7 @@
"drizzle-zod": "~0.7.1",
"file-type": "^21.0.0",
"hono-openapi": "^0.4.8",
+ "iconsax-reactjs": "^0.0.8",
"nanoid": "^5.1.5",
"neverthrow": "^8.2.0",
"pg": "^8.16.3",
@@ -2488,6 +2489,8 @@
"humanize-ms": ["[email protected]", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="],
+ "iconsax-reactjs": ["[email protected]", "", { "peerDependencies": { "react": "*" } }, "sha512-cb+uTMxbkSFNbu8ZclX7BWQVfOWQt8+m/PsDjnsm/H+mcYrnfTYMjHxiof1FB43k7UAgt1ds+0oFeMVKdqyslw=="],
+
"iconv-lite": ["[email protected]", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
"ieee754": ["[email protected]", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
diff --git a/package.json b/package.json
index bb88f692..feb45bd6 100644
--- a/package.json
+++ b/package.json
@@ -40,7 +40,7 @@
"drizzle-zod": "~0.7.1",
"file-type": "^21.0.0",
"hono-openapi": "^0.4.8",
-
+ "iconsax-reactjs": "^0.0.8",
"nanoid": "^5.1.5",
"neverthrow": "^8.2.0",
"pg": "^8.16.3",
diff --git a/packages/ai-sdk/src/tools.test.ts b/packages/ai-sdk/src/tools.test.ts
index ebbe2235..aab4d451 100644
--- a/packages/ai-sdk/src/tools.test.ts
+++ b/packages/ai-sdk/src/tools.test.ts
@@ -1,27 +1,29 @@
-import { createOpenAI } from "@ai-sdk/openai"
-import { generateText } from "ai"
-import { describe, expect, it } from "vitest"
-import { type SupermemoryToolsConfig, supermemoryTools } from "./tools"
+import { createOpenAI } from "@ai-sdk/openai";
+import { generateText } from "ai";
+import { describe, expect, it } from "vitest";
+import { type SupermemoryToolsConfig, supermemoryTools } from "./tools";
-import "dotenv/config"
+import "dotenv/config";
describe("supermemoryTools", () => {
// Required API keys - tests will fail if not provided
- const testApiKey = process.env.SUPERMEMORY_API_KEY
- const testOpenAIKey = process.env.OPENAI_API_KEY
+ const testApiKey = process.env.SUPERMEMORY_API_KEY;
+ const testOpenAIKey = process.env.OPENAI_API_KEY;
if (!testApiKey) {
throw new Error(
"SUPERMEMORY_API_KEY environment variable is required for tests",
- )
+ );
}
if (!testOpenAIKey) {
- throw new Error("OPENAI_API_KEY environment variable is required for tests")
+ throw new Error(
+ "OPENAI_API_KEY environment variable is required for tests",
+ );
}
// Optional configuration with defaults
- const testBaseUrl = process.env.SUPERMEMORY_BASE_URL ?? undefined
- const testModelName = process.env.MODEL_NAME || "gpt-5-mini"
+ const testBaseUrl = process.env.SUPERMEMORY_BASE_URL ?? undefined;
+ const testModelName = process.env.MODEL_NAME || "gpt-5-mini";
const testPrompts = [
"What do you remember about my preferences?",
@@ -29,57 +31,57 @@ describe("supermemoryTools", () => {
"What are my current projects?",
"Remind me of my interests and hobbies",
"What should I focus on today?",
- ]
+ ];
describe("client initialization", () => {
it("should create tools with default configuration", () => {
- const config: SupermemoryToolsConfig = {}
- const tools = supermemoryTools(testApiKey, config)
+ const config: SupermemoryToolsConfig = {};
+ const tools = supermemoryTools(testApiKey, config);
- expect(tools).toBeDefined()
- expect(tools.searchMemories).toBeDefined()
- expect(tools.addMemory).toBeDefined()
- })
+ expect(tools).toBeDefined();
+ expect(tools.searchMemories).toBeDefined();
+ expect(tools.addMemory).toBeDefined();
+ });
it("should create tools with custom baseUrl", () => {
const config: SupermemoryToolsConfig = {
baseUrl: testBaseUrl,
- }
- const tools = supermemoryTools(testApiKey, config)
+ };
+ const tools = supermemoryTools(testApiKey, config);
- expect(tools).toBeDefined()
- expect(tools.searchMemories).toBeDefined()
- expect(tools.addMemory).toBeDefined()
- })
+ expect(tools).toBeDefined();
+ expect(tools.searchMemories).toBeDefined();
+ expect(tools.addMemory).toBeDefined();
+ });
it("should create tools with projectId configuration", () => {
const config: SupermemoryToolsConfig = {
projectId: "test-project-123",
- }
- const tools = supermemoryTools(testApiKey, config)
+ };
+ const tools = supermemoryTools(testApiKey, config);
- expect(tools).toBeDefined()
- expect(tools.searchMemories).toBeDefined()
- expect(tools.addMemory).toBeDefined()
- })
+ expect(tools).toBeDefined();
+ expect(tools.searchMemories).toBeDefined();
+ expect(tools.addMemory).toBeDefined();
+ });
it("should create tools with custom container tags", () => {
const config: SupermemoryToolsConfig = {
containerTags: ["custom-tag-1", "custom-tag-2"],
- }
- const tools = supermemoryTools(testApiKey, config)
+ };
+ const tools = supermemoryTools(testApiKey, config);
- expect(tools).toBeDefined()
- expect(tools.searchMemories).toBeDefined()
- expect(tools.addMemory).toBeDefined()
- })
- })
+ expect(tools).toBeDefined();
+ expect(tools.searchMemories).toBeDefined();
+ expect(tools.addMemory).toBeDefined();
+ });
+ });
describe("AI SDK integration", () => {
it("should work with AI SDK generateText", async () => {
const openai = createOpenAI({
apiKey: testOpenAIKey,
- })
+ });
const result = await generateText({
model: openai(testModelName),
@@ -100,22 +102,22 @@ describe("supermemoryTools", () => {
baseUrl: testBaseUrl,
}),
},
- })
+ });
- expect(result).toBeDefined()
- expect(result.text).toBeDefined()
- expect(typeof result.text).toBe("string")
- })
+ expect(result).toBeDefined();
+ expect(result.text).toBeDefined();
+ expect(typeof result.text).toBe("string");
+ });
it("should use tools when prompted", async () => {
const openai = createOpenAI({
apiKey: testOpenAIKey,
- })
+ });
const tools = supermemoryTools(testApiKey, {
projectId: "test-tool-usage",
baseUrl: testBaseUrl,
- })
+ });
const result = await generateText({
model: openai(testModelName),
@@ -133,24 +135,24 @@ describe("supermemoryTools", () => {
tools: {
addMemory: tools.addMemory,
},
- })
+ });
- expect(result).toBeDefined()
- expect(result.text).toBeDefined()
- expect(result.toolCalls).toBeDefined()
+ expect(result).toBeDefined();
+ expect(result.text).toBeDefined();
+ expect(result.toolCalls).toBeDefined();
if (result.toolCalls && result.toolCalls.length > 0) {
const addMemoryCall = result.toolCalls.find(
(call) => call.toolName === "addMemory",
- )
- expect(addMemoryCall).toBeDefined()
+ );
+ expect(addMemoryCall).toBeDefined();
}
- })
+ });
it("should handle multiple tool types", async () => {
const openai = createOpenAI({
apiKey: testOpenAIKey,
- })
+ });
const result = await generateText({
model: openai(testModelName),
@@ -171,11 +173,11 @@ describe("supermemoryTools", () => {
containerTags: ["test-multi-tools"],
}),
},
- })
-
- expect(result).toBeDefined()
- expect(result.text).toBeDefined()
- expect(typeof result.text).toBe("string")
- })
- })
-})
+ });
+
+ expect(result).toBeDefined();
+ expect(result.text).toBeDefined();
+ expect(typeof result.text).toBe("string");
+ });
+ });
+});
diff --git a/packages/ai-sdk/src/tools.ts b/packages/ai-sdk/src/tools.ts
index d0461924..97f54791 100644
--- a/packages/ai-sdk/src/tools.ts
+++ b/packages/ai-sdk/src/tools.ts
@@ -1,15 +1,15 @@
-import { tool } from "ai"
-import Supermemory from "supermemory"
-import { z } from "zod"
+import { tool } from "ai";
+import Supermemory from "supermemory";
+import { z } from "zod";
/**
* Supermemory configuration
* Only one of `projectId` or `containerTags` can be provided.
*/
export interface SupermemoryToolsConfig {
- baseUrl?: string
- containerTags?: string[]
- projectId?: string
+ baseUrl?: string;
+ containerTags?: string[];
+ projectId?: string;
}
/**
@@ -22,11 +22,11 @@ export function supermemoryTools(
const client = new Supermemory({
apiKey,
...(config?.baseUrl ? { baseURL: config.baseUrl } : {}),
- })
+ });
const containerTags = config?.projectId
? [`sm_project_${config?.projectId}`]
- : (config?.containerTags ?? ["sm_project_default"])
+ : (config?.containerTags ?? ["sm_project_default"]);
const searchMemories = tool({
description:
@@ -60,21 +60,21 @@ export function supermemoryTools(
limit,
chunkThreshold: 0.6,
includeFullDocs,
- })
+ });
return {
success: true,
results: response.results,
count: response.results?.length || 0,
- }
+ };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error",
- }
+ };
}
},
- })
+ });
const addMemory = tool({
description:
@@ -88,31 +88,31 @@ export function supermemoryTools(
}),
execute: async ({ memory }) => {
try {
- const metadata: Record<string, string | number | boolean> = {}
+ const metadata: Record<string, string | number | boolean> = {};
const response = await client.memories.add({
content: memory,
containerTags,
...(Object.keys(metadata).length > 0 && { metadata }),
- })
+ });
return {
success: true,
memory: response,
- }
+ };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error",
- }
+ };
}
},
- })
+ });
return {
searchMemories,
addMemory,
- }
+ };
}
// Export individual tool creators for more flexibility
@@ -120,14 +120,14 @@ export const searchMemoriesTool = (
apiKey: string,
config?: SupermemoryToolsConfig,
) => {
- const { searchMemories } = supermemoryTools(apiKey, config)
- return searchMemories
-}
+ const { searchMemories } = supermemoryTools(apiKey, config);
+ return searchMemories;
+};
export const addMemoryTool = (
apiKey: string,
config?: SupermemoryToolsConfig,
) => {
- const { addMemory } = supermemoryTools(apiKey, config)
- return addMemory
-}
+ const { addMemory } = supermemoryTools(apiKey, config);
+ return addMemory;
+};
diff --git a/packages/ai-sdk/tsdown.config.ts b/packages/ai-sdk/tsdown.config.ts
index f587b211..53839a00 100644
--- a/packages/ai-sdk/tsdown.config.ts
+++ b/packages/ai-sdk/tsdown.config.ts
@@ -1,4 +1,4 @@
-import { defineConfig } from "tsdown"
+import { defineConfig } from "tsdown";
export default defineConfig({
entry: ["src/index.ts"],
@@ -12,4 +12,4 @@ export default defineConfig({
sourcemap: true,
},
exports: true,
-})
+});
diff --git a/packages/hooks/use-keypress.ts b/packages/hooks/use-keypress.ts
index 42906660..eee23acb 100644
--- a/packages/hooks/use-keypress.ts
+++ b/packages/hooks/use-keypress.ts
@@ -1,15 +1,15 @@
-import { useEffect } from "react"
+import { useEffect } from "react";
export const useKeyPress = (key: string, callback: () => void) => {
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === key && e.altKey) {
- callback()
+ callback();
}
- }
- window.addEventListener("keydown", handler)
+ };
+ window.addEventListener("keydown", handler);
return () => {
- window.removeEventListener("keydown", handler)
- }
- }, [key, callback])
-}
+ window.removeEventListener("keydown", handler);
+ };
+ }, [key, callback]);
+};
diff --git a/packages/hooks/use-mobile.ts b/packages/hooks/use-mobile.ts
index 283bbb4c..0a892310 100644
--- a/packages/hooks/use-mobile.ts
+++ b/packages/hooks/use-mobile.ts
@@ -1,19 +1,21 @@
-import * as React from "react"
+import * as React from "react";
-const MOBILE_BREAKPOINT = 768
+const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
- const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
+ undefined,
+ );
React.useEffect(() => {
- const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
- setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
- }
- mql.addEventListener("change", onChange)
- setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
- return () => mql.removeEventListener("change", onChange)
- }, [])
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
+ };
+ mql.addEventListener("change", onChange);
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
+ return () => mql.removeEventListener("change", onChange);
+ }, []);
- return !!isMobile
+ return !!isMobile;
}
diff --git a/packages/lib/api.ts b/packages/lib/api.ts
index ad343050..82c0b8ef 100644
--- a/packages/lib/api.ts
+++ b/packages/lib/api.ts
@@ -10,6 +10,7 @@ import {
DeleteProjectSchema,
DocumentsWithMemoriesQuerySchema,
DocumentsWithMemoriesResponseSchema,
+ GetMemoryResponseSchema,
ListMemoriesResponseSchema,
ListProjectsResponseSchema,
MemoryAddSchema,
@@ -126,7 +127,24 @@ export const apiSchema = createSchema({
input: SettingsRequestSchema,
output: SettingsResponseSchema,
},
+
+ "@get/documents/:id": {
+ output: GetMemoryResponseSchema,
+ params: z.object({ id: z.string() }),
+ },
+
// Memory operations
+ "@get/documents": {
+ output: ListMemoriesResponseSchema,
+ query: z
+ .object({
+ limit: z.number().optional(),
+ page: z.number().optional(),
+ status: z.string().optional(),
+ containerTags: z.array(z.string()).optional(),
+ })
+ .optional(),
+ },
"@post/documents": {
input: MemoryAddSchema,
output: MemoryResponseSchema,
diff --git a/packages/lib/auth-context.tsx b/packages/lib/auth-context.tsx
index 66ff84bc..a88c73c8 100644
--- a/packages/lib/auth-context.tsx
+++ b/packages/lib/auth-context.tsx
@@ -1,4 +1,4 @@
-"use client"
+"use client";
import {
createContext,
@@ -6,75 +6,74 @@ import {
useContext,
useEffect,
useState,
-} from "react"
-import { authClient, useSession } from "./auth"
+} from "react";
+import { authClient, useSession } from "./auth";
-type Organization = typeof authClient.$Infer.ActiveOrganization
-type SessionData = NonNullable<ReturnType<typeof useSession>["data"]>
+type Organization = typeof authClient.$Infer.ActiveOrganization;
+type SessionData = NonNullable<ReturnType<typeof useSession>["data"]>;
interface AuthContextType {
- session: SessionData["session"] | null
- user: SessionData["user"] | null
- org: Organization | null
- setActiveOrg: (orgSlug: string) => Promise<void>
+ session: SessionData["session"] | null;
+ user: SessionData["user"] | null;
+ org: Organization | null;
+ setActiveOrg: (orgSlug: string) => Promise<void>;
}
-const AuthContext = createContext<AuthContextType | undefined>(undefined)
+const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
- const { data: session } = useSession()
- const [org, setOrg] = useState<Organization | null>(null)
+ const { data: session } = useSession();
+ const [org, setOrg] = useState<Organization | null>(null);
useEffect(() => {
if (session?.session.activeOrganizationId) {
authClient.organization.getFullOrganization().then((org) => {
- setOrg(org)
- })
+ setOrg(org);
+ });
}
- }, [session?.session.activeOrganizationId])
+ }, [session?.session.activeOrganizationId]);
// When a session exists and there is a pending login method recorded,
// promote it to the last-used method (successful login) and clear pending.
useEffect(() => {
- if (typeof window === "undefined") return
- if (!session?.session) return
+ if (typeof window === "undefined") return;
+ if (!session?.session) return;
try {
const pendingMethod = localStorage.getItem(
"supermemory-pending-login-method",
- )
+ );
const pendingTsRaw = localStorage.getItem(
"supermemory-pending-login-timestamp",
- )
+ );
if (pendingMethod) {
- const now = Date.now()
- const ts = pendingTsRaw ? Number.parseInt(pendingTsRaw, 10) : NaN
- const isFresh = Number.isFinite(ts) && now - ts < 10 * 60 * 1000 // 10 minutes TTL
+ const now = Date.now();
+ const ts = pendingTsRaw
+ ? Number.parseInt(pendingTsRaw, 10)
+ : Number.NaN;
+ const isFresh = Number.isFinite(ts) && now - ts < 10 * 60 * 1000; // 10 minutes TTL
if (isFresh) {
- localStorage.setItem(
- "supermemory-last-login-method",
- pendingMethod,
- )
+ localStorage.setItem("supermemory-last-login-method", pendingMethod);
}
}
- } catch { }
+ } catch {}
// Always clear pending markers once a session is present
try {
- localStorage.removeItem("supermemory-pending-login-method")
- localStorage.removeItem("supermemory-pending-login-timestamp")
- } catch { }
- }, [session?.session])
+ localStorage.removeItem("supermemory-pending-login-method");
+ localStorage.removeItem("supermemory-pending-login-timestamp");
+ } catch {}
+ }, [session?.session]);
const setActiveOrg = async (slug: string) => {
- if (!slug) return
+ if (!slug) return;
const activeOrg = await authClient.organization.setActive({
organizationSlug: slug,
- })
- setOrg(activeOrg)
- }
+ });
+ setOrg(activeOrg);
+ };
return (
<AuthContext.Provider
@@ -87,13 +86,13 @@ export function AuthProvider({ children }: { children: ReactNode }) {
>
{children}
</AuthContext.Provider>
- )
+ );
}
export function useAuth() {
- const context = useContext(AuthContext)
+ const context = useContext(AuthContext);
if (context === undefined) {
- throw new Error("useAuth must be used within an AuthProvider")
+ throw new Error("useAuth must be used within an AuthProvider");
}
- return context
+ return context;
}
diff --git a/packages/lib/auth.middleware.ts b/packages/lib/auth.middleware.ts
index 3b1f1f40..884f14b8 100644
--- a/packages/lib/auth.middleware.ts
+++ b/packages/lib/auth.middleware.ts
@@ -1,4 +1,4 @@
-import { createAuthClient } from "better-auth/client"
+import { createAuthClient } from "better-auth/client";
import {
adminClient,
anonymousClient,
@@ -7,7 +7,7 @@ import {
magicLinkClient,
organizationClient,
usernameClient,
-} from "better-auth/client/plugins"
+} from "better-auth/client/plugins";
export const middlewareAuthClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai",
@@ -23,4 +23,4 @@ export const middlewareAuthClient = createAuthClient({
organizationClient(),
anonymousClient(),
],
-})
+});
diff --git a/packages/lib/auth.ts b/packages/lib/auth.ts
index 4369bef1..02709e2c 100644
--- a/packages/lib/auth.ts
+++ b/packages/lib/auth.ts
@@ -6,8 +6,8 @@ import {
magicLinkClient,
organizationClient,
usernameClient,
-} from "better-auth/client/plugins"
-import { createAuthClient } from "better-auth/react"
+} from "better-auth/client/plugins";
+import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai",
@@ -24,9 +24,9 @@ export const authClient = createAuthClient({
organizationClient(),
anonymousClient(),
],
-})
+});
-export const signIn = authClient.signIn
-export const signOut = authClient.signOut
-export const useSession = authClient.useSession
-export const getSession = authClient.getSession
+export const signIn = authClient.signIn;
+export const signOut = authClient.signOut;
+export const useSession = authClient.useSession;
+export const getSession = authClient.getSession;
diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts
index fde5bce1..74eb27ec 100644
--- a/packages/lib/constants.ts
+++ b/packages/lib/constants.ts
@@ -1,13 +1,13 @@
-const BIG_DIMENSIONS_NEW = 1536
-const DEFAULT_PROJECT_ID = "sm_project_default"
+const BIG_DIMENSIONS_NEW = 1536;
+const DEFAULT_PROJECT_ID = "sm_project_default";
const SEARCH_MEMORY_SHORTCUT_URL =
- "https://www.icloud.com/shortcuts/f2b5c544372844a38ab4c6900e2a88de"
+ "https://www.icloud.com/shortcuts/f2b5c544372844a38ab4c6900e2a88de";
const ADD_MEMORY_SHORTCUT_URL =
- "https://www.icloud.com/shortcuts/ec33b029b2c7481d89eda7640dbb7688"
+ "https://www.icloud.com/shortcuts/ec33b029b2c7481d89eda7640dbb7688";
export {
BIG_DIMENSIONS_NEW,
DEFAULT_PROJECT_ID,
SEARCH_MEMORY_SHORTCUT_URL,
ADD_MEMORY_SHORTCUT_URL,
-}
+};
diff --git a/packages/lib/error-tracking.tsx b/packages/lib/error-tracking.tsx
index bf320271..cc3a9b3c 100644
--- a/packages/lib/error-tracking.tsx
+++ b/packages/lib/error-tracking.tsx
@@ -1,14 +1,14 @@
-"use client"
+"use client";
-import { usePathname } from "next/navigation"
-import { useEffect } from "react"
-import { useSession } from "./auth"
-import { usePostHog } from "./posthog"
+import { usePathname } from "next/navigation";
+import { useEffect } from "react";
+import { useSession } from "./auth";
+import { usePostHog } from "./posthog";
export function useErrorTracking() {
- const posthog = usePostHog()
- const { data: session } = useSession()
- const pathname = usePathname()
+ const posthog = usePostHog();
+ const { data: session } = useSession();
+ const pathname = usePathname();
const trackError = (
error: Error | unknown,
@@ -23,10 +23,10 @@ export function useErrorTracking() {
user_email: session?.user?.email,
timestamp: new Date().toISOString(),
...context,
- }
+ };
- posthog.capture("error_occurred", errorDetails)
- }
+ posthog.capture("error_occurred", errorDetails);
+ };
const trackApiError = (
error: Error | unknown,
@@ -37,8 +37,8 @@ export function useErrorTracking() {
error_type: "api_error",
api_endpoint: endpoint,
api_method: method,
- })
- }
+ });
+ };
const trackComponentError = (
error: Error | unknown,
@@ -47,8 +47,8 @@ export function useErrorTracking() {
trackError(error, {
error_type: "component_error",
component_name: componentName,
- })
- }
+ });
+ };
const trackValidationError = (
error: Error | unknown,
@@ -59,24 +59,24 @@ export function useErrorTracking() {
error_type: "validation_error",
form_name: formName,
field_name: field,
- })
- }
+ });
+ };
return {
trackError,
trackApiError,
trackComponentError,
trackValidationError,
- }
+ };
}
// Global error boundary component
export function ErrorTrackingProvider({
children,
}: {
- children: React.ReactNode
+ children: React.ReactNode;
}) {
- const { trackError } = useErrorTracking()
+ const { trackError } = useErrorTracking();
useEffect(() => {
// Global error handler for unhandled errors
@@ -87,34 +87,37 @@ export function ErrorTrackingProvider({
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
- })
- }
+ });
+ };
// Global handler for unhandled promise rejections
const handleUnhandledRejection = (event: PromiseRejectionEvent) => {
trackError(event.reason, {
error_type: "unhandled_promise_rejection",
source: "promise_rejection",
- })
- }
+ });
+ };
- window.addEventListener("error", handleError)
- window.addEventListener("unhandledrejection", handleUnhandledRejection)
+ window.addEventListener("error", handleError);
+ window.addEventListener("unhandledrejection", handleUnhandledRejection);
return () => {
- window.removeEventListener("error", handleError)
- window.removeEventListener("unhandledrejection", handleUnhandledRejection)
- }
- }, [trackError])
-
- return <>{children}</>
+ window.removeEventListener("error", handleError);
+ window.removeEventListener(
+ "unhandledrejection",
+ handleUnhandledRejection,
+ );
+ };
+ }, [trackError]);
+
+ return <>{children}</>;
}
// Hook for tracking user interactions
export function useInteractionTracking() {
- const posthog = usePostHog()
- const { data: session } = useSession()
- const pathname = usePathname()
+ const posthog = usePostHog();
+ const { data: session } = useSession();
+ const pathname = usePathname();
const trackInteraction = (action: string, details?: Record<string, any>) => {
posthog.capture("user_interaction", {
@@ -123,8 +126,8 @@ export function useInteractionTracking() {
user_id: session?.user?.id,
timestamp: new Date().toISOString(),
...details,
- })
- }
+ });
+ };
const trackFormSubmission = (
formName: string,
@@ -138,15 +141,15 @@ export function useInteractionTracking() {
user_id: session?.user?.id,
timestamp: new Date().toISOString(),
...details,
- })
- }
+ });
+ };
const trackButtonClick = (buttonName: string, context?: string) => {
trackInteraction("button_click", {
button_name: buttonName,
context,
- })
- }
+ });
+ };
const trackLinkClick = (
url: string,
@@ -157,27 +160,27 @@ export function useInteractionTracking() {
url,
link_text: linkText,
external,
- })
- }
+ });
+ };
const trackModalOpen = (modalName: string) => {
trackInteraction("modal_open", {
modal_name: modalName,
- })
- }
+ });
+ };
const trackModalClose = (modalName: string) => {
trackInteraction("modal_close", {
modal_name: modalName,
- })
- }
+ });
+ };
const trackTabChange = (fromTab: string, toTab: string) => {
trackInteraction("tab_change", {
from_tab: fromTab,
to_tab: toTab,
- })
- }
+ });
+ };
return {
trackInteraction,
@@ -187,5 +190,5 @@ export function useInteractionTracking() {
trackModalOpen,
trackModalClose,
trackTabChange,
- }
+ };
}
diff --git a/packages/lib/generate-id.ts b/packages/lib/generate-id.ts
index bbe201fd..6007027e 100644
--- a/packages/lib/generate-id.ts
+++ b/packages/lib/generate-id.ts
@@ -1,6 +1,6 @@
-import { customAlphabet } from "nanoid"
+import { customAlphabet } from "nanoid";
export const generateId = () =>
customAlphabet("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz")(
22,
- )
+ );
diff --git a/packages/lib/glass-effect-manager.ts b/packages/lib/glass-effect-manager.ts
index 291a30aa..03a79734 100644
--- a/packages/lib/glass-effect-manager.ts
+++ b/packages/lib/glass-effect-manager.ts
@@ -1,56 +1,56 @@
// Singleton WebGL context manager for glass effects
class GlassEffectManager {
- private static instance: GlassEffectManager | null = null
- private canvas: HTMLCanvasElement | null = null
- private gl: WebGLRenderingContext | null = null
- private program: WebGLProgram | null = null
- private uniforms: Record<string, WebGLUniformLocation | null> = {}
- private effects: Map<string, EffectInstance> = new Map()
- private animationFrame: number | null = null
- private startTime: number = performance.now()
- private mousePositions: Map<string, { x: number; y: number }> = new Map()
+ private static instance: GlassEffectManager | null = null;
+ private canvas: HTMLCanvasElement | null = null;
+ private gl: WebGLRenderingContext | null = null;
+ private program: WebGLProgram | null = null;
+ private uniforms: Record<string, WebGLUniformLocation | null> = {};
+ private effects: Map<string, EffectInstance> = new Map();
+ private animationFrame: number | null = null;
+ private startTime: number = performance.now();
+ private mousePositions: Map<string, { x: number; y: number }> = new Map();
static getInstance(): GlassEffectManager {
if (!GlassEffectManager.instance) {
- GlassEffectManager.instance = new GlassEffectManager()
+ GlassEffectManager.instance = new GlassEffectManager();
}
- return GlassEffectManager.instance
+ return GlassEffectManager.instance;
}
private constructor() {
- this.initializeContext()
+ this.initializeContext();
}
private initializeContext() {
// Create offscreen canvas
- this.canvas = document.createElement("canvas")
- this.canvas.width = 1024 // Default size, will be adjusted
- this.canvas.height = 1024
+ this.canvas = document.createElement("canvas");
+ this.canvas.width = 1024; // Default size, will be adjusted
+ this.canvas.height = 1024;
this.gl = this.canvas.getContext("webgl", {
alpha: true,
premultipliedAlpha: false,
preserveDrawingBuffer: true,
- })
+ });
if (!this.gl) {
- console.error("WebGL not supported")
- return
+ console.error("WebGL not supported");
+ return;
}
- this.setupShaders()
- this.startRenderLoop()
+ this.setupShaders();
+ this.startRenderLoop();
}
private setupShaders() {
- if (!this.gl) return
+ if (!this.gl) return;
const vsSource = `
attribute vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
- `
+ `;
const fsSource = `
precision mediump float;
@@ -106,48 +106,48 @@ class GlassEffectManager {
gl_FragColor = vec4(glassColor, alpha);
}
- `
+ `;
const createShader = (type: number, source: string) => {
- const shader = this.gl!.createShader(type)
- if (!shader) return null
+ const shader = this.gl!.createShader(type);
+ if (!shader) return null;
- this.gl!.shaderSource(shader, source)
- this.gl!.compileShader(shader)
+ this.gl!.shaderSource(shader, source);
+ this.gl!.compileShader(shader);
if (!this.gl!.getShaderParameter(shader, this.gl!.COMPILE_STATUS)) {
- console.error("Shader error:", this.gl!.getShaderInfoLog(shader))
- this.gl!.deleteShader(shader)
- return null
+ console.error("Shader error:", this.gl!.getShaderInfoLog(shader));
+ this.gl!.deleteShader(shader);
+ return null;
}
- return shader
- }
+ return shader;
+ };
- const vs = createShader(this.gl.VERTEX_SHADER, vsSource)
- const fs = createShader(this.gl.FRAGMENT_SHADER, fsSource)
- if (!vs || !fs) return
+ const vs = createShader(this.gl.VERTEX_SHADER, vsSource);
+ const fs = createShader(this.gl.FRAGMENT_SHADER, fsSource);
+ if (!vs || !fs) return;
- this.program = this.gl.createProgram()
- if (!this.program) return
+ this.program = this.gl.createProgram();
+ if (!this.program) return;
- this.gl.attachShader(this.program, vs)
- this.gl.attachShader(this.program, fs)
- this.gl.linkProgram(this.program)
+ this.gl.attachShader(this.program, vs);
+ this.gl.attachShader(this.program, fs);
+ this.gl.linkProgram(this.program);
// biome-ignore lint/correctness/useHookAtTopLevel: Well, not a hook
- this.gl.useProgram(this.program)
+ this.gl.useProgram(this.program);
// Buffer setup
- const buffer = this.gl.createBuffer()
- this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer)
+ const buffer = this.gl.createBuffer();
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer);
this.gl.bufferData(
this.gl.ARRAY_BUFFER,
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
this.gl.STATIC_DRAW,
- )
+ );
- const position = this.gl.getAttribLocation(this.program, "position")
- this.gl.enableVertexAttribArray(position)
- this.gl.vertexAttribPointer(position, 2, this.gl.FLOAT, false, 0, 0)
+ const position = this.gl.getAttribLocation(this.program, "position");
+ this.gl.enableVertexAttribArray(position);
+ this.gl.vertexAttribPointer(position, 2, this.gl.FLOAT, false, 0, 0);
// Store uniform locations
this.uniforms = {
@@ -155,11 +155,11 @@ class GlassEffectManager {
time: this.gl.getUniformLocation(this.program, "iTime"),
mouse: this.gl.getUniformLocation(this.program, "iMouse"),
expanded: this.gl.getUniformLocation(this.program, "iExpanded"),
- }
+ };
// Enable blending
- this.gl.enable(this.gl.BLEND)
- this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA)
+ this.gl.enable(this.gl.BLEND);
+ this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
}
registerEffect(
@@ -168,8 +168,8 @@ class GlassEffectManager {
isExpanded: boolean,
): () => void {
// Ensure minimum dimensions
- const width = Math.max(1, targetCanvas.width)
- const height = Math.max(1, targetCanvas.height)
+ const width = Math.max(1, targetCanvas.width);
+ const height = Math.max(1, targetCanvas.height);
const effect: EffectInstance = {
id,
@@ -177,58 +177,58 @@ class GlassEffectManager {
isExpanded,
width,
height,
- }
+ };
- this.effects.set(id, effect)
- this.mousePositions.set(id, { x: 0, y: 0 })
+ this.effects.set(id, effect);
+ this.mousePositions.set(id, { x: 0, y: 0 });
// Return cleanup function
return () => {
- this.effects.delete(id)
- this.mousePositions.delete(id)
+ this.effects.delete(id);
+ this.mousePositions.delete(id);
if (this.effects.size === 0 && this.animationFrame) {
- cancelAnimationFrame(this.animationFrame)
- this.animationFrame = null
+ cancelAnimationFrame(this.animationFrame);
+ this.animationFrame = null;
}
- }
+ };
}
updateMousePosition(id: string, x: number, y: number) {
- this.mousePositions.set(id, { x, y })
+ this.mousePositions.set(id, { x, y });
}
updateExpanded(id: string, isExpanded: boolean) {
- const effect = this.effects.get(id)
+ const effect = this.effects.get(id);
if (effect) {
- effect.isExpanded = isExpanded
+ effect.isExpanded = isExpanded;
}
}
updateSize(id: string, width: number, height: number) {
- const effect = this.effects.get(id)
+ const effect = this.effects.get(id);
if (effect) {
// Ensure minimum dimensions
- effect.width = Math.max(1, width)
- effect.height = Math.max(1, height)
+ effect.width = Math.max(1, width);
+ effect.height = Math.max(1, height);
}
}
private startRenderLoop() {
const render = () => {
if (!this.gl || !this.program || this.effects.size === 0) {
- this.animationFrame = requestAnimationFrame(render)
- return
+ this.animationFrame = requestAnimationFrame(render);
+ return;
}
- const currentTime = (performance.now() - this.startTime) / 1000
+ const currentTime = (performance.now() - this.startTime) / 1000;
// Render each effect
for (const [id, effect] of Array.from(this.effects)) {
- const mousePos = this.mousePositions.get(id) || { x: 0, y: 0 }
+ const mousePos = this.mousePositions.get(id) || { x: 0, y: 0 };
// Skip rendering if dimensions are invalid
if (effect.width <= 0 || effect.height <= 0) {
- continue
+ continue;
}
// Set canvas size if needed
@@ -236,14 +236,14 @@ class GlassEffectManager {
this.canvas!.width !== effect.width ||
this.canvas!.height !== effect.height
) {
- this.canvas!.width = effect.width
- this.canvas!.height = effect.height
- this.gl.viewport(0, 0, effect.width, effect.height)
+ this.canvas!.width = effect.width;
+ this.canvas!.height = effect.height;
+ this.gl.viewport(0, 0, effect.width, effect.height);
}
// Clear and render
- this.gl.clearColor(0, 0, 0, 0)
- this.gl.clear(this.gl.COLOR_BUFFER_BIT)
+ this.gl.clearColor(0, 0, 0, 0);
+ this.gl.clear(this.gl.COLOR_BUFFER_BIT);
// Set uniforms
if (this.uniforms.resolution) {
@@ -251,58 +251,58 @@ class GlassEffectManager {
this.uniforms.resolution,
effect.width,
effect.height,
- )
+ );
}
if (this.uniforms.time) {
- this.gl.uniform1f(this.uniforms.time, currentTime)
+ this.gl.uniform1f(this.uniforms.time, currentTime);
}
if (this.uniforms.mouse) {
- this.gl.uniform2f(this.uniforms.mouse, mousePos.x, mousePos.y)
+ this.gl.uniform2f(this.uniforms.mouse, mousePos.x, mousePos.y);
}
if (this.uniforms.expanded) {
this.gl.uniform1f(
this.uniforms.expanded,
effect.isExpanded ? 1.0 : 0.0,
- )
+ );
}
// Draw
- this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4)
+ this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
// Copy to target canvas
- const targetCtx = effect.targetCanvas.getContext("2d")
+ const targetCtx = effect.targetCanvas.getContext("2d");
if (targetCtx) {
- targetCtx.clearRect(0, 0, effect.width, effect.height)
- targetCtx.drawImage(this.canvas!, 0, 0)
+ targetCtx.clearRect(0, 0, effect.width, effect.height);
+ targetCtx.drawImage(this.canvas!, 0, 0);
}
}
- this.animationFrame = requestAnimationFrame(render)
- }
+ this.animationFrame = requestAnimationFrame(render);
+ };
- render()
+ render();
}
// Clean up method (optional, for when the app unmounts)
destroy() {
if (this.animationFrame) {
- cancelAnimationFrame(this.animationFrame)
+ cancelAnimationFrame(this.animationFrame);
}
if (this.gl && this.program) {
- this.gl.deleteProgram(this.program)
+ this.gl.deleteProgram(this.program);
}
- this.effects.clear()
- this.mousePositions.clear()
- GlassEffectManager.instance = null
+ this.effects.clear();
+ this.mousePositions.clear();
+ GlassEffectManager.instance = null;
}
}
interface EffectInstance {
- id: string
- targetCanvas: HTMLCanvasElement
- isExpanded: boolean
- width: number
- height: number
+ id: string;
+ targetCanvas: HTMLCanvasElement;
+ isExpanded: boolean;
+ width: number;
+ height: number;
}
-export default GlassEffectManager
+export default GlassEffectManager;
diff --git a/packages/lib/posthog.tsx b/packages/lib/posthog.tsx
index ac563aae..d1105cbc 100644
--- a/packages/lib/posthog.tsx
+++ b/packages/lib/posthog.tsx
@@ -1,20 +1,20 @@
-"use client"
+"use client";
-import { usePathname, useSearchParams } from "next/navigation"
-import posthog from "posthog-js"
-import { Suspense, useEffect } from "react"
-import { useSession } from "./auth"
+import { usePathname, useSearchParams } from "next/navigation";
+import posthog from "posthog-js";
+import { Suspense, useEffect } from "react";
+import { useSession } from "./auth";
function PostHogPageTracking() {
- const pathname = usePathname()
- const searchParams = useSearchParams()
+ const pathname = usePathname();
+ const searchParams = useSearchParams();
// Page tracking
useEffect(() => {
if (pathname) {
- let url = window.origin + pathname
+ let url = window.origin + pathname;
if (searchParams.toString()) {
- url = `${url}?${searchParams.toString()}`
+ url = `${url}?${searchParams.toString()}`;
}
// Extract page context for better tracking
@@ -24,17 +24,17 @@ function PostHogPageTracking() {
search_params: searchParams.toString(),
page_type: getPageType(pathname),
org_slug: getOrgSlug(pathname),
- }
+ };
- posthog.capture("$pageview", pageContext)
+ posthog.capture("$pageview", pageContext);
}
- }, [pathname, searchParams])
+ }, [pathname, searchParams]);
- return null
+ return null;
}
export function PostHogProvider({ children }: { children: React.ReactNode }) {
- const { data: session } = useSession()
+ const { data: session } = useSession();
useEffect(() => {
if (typeof window !== "undefined") {
@@ -44,9 +44,9 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) {
person_profiles: "identified_only",
capture_pageview: false,
capture_pageleave: true,
- })
+ });
}
- }, [])
+ }, []);
// User identification
useEffect(() => {
@@ -56,9 +56,9 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) {
name: session.user.name,
userId: session.user.id,
createdAt: session.user.createdAt,
- })
+ });
}
- }, [session?.user])
+ }, [session?.user]);
return (
<>
@@ -67,18 +67,18 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) {
</Suspense>
{children}
</>
- )
+ );
}
function getPageType(pathname: string): string {
- return "other"
+ return "other";
}
function getOrgSlug(pathname: string): string | null {
- const match = pathname.match(/^\/([^/]+)\//)
- return match ? (match[1] ?? null) : null
+ const match = pathname.match(/^\/([^/]+)\//);
+ return match ? (match[1] ?? null) : null;
}
export function usePostHog() {
- return posthog
+ return posthog;
}
diff --git a/packages/lib/queries.ts b/packages/lib/queries.ts
index 3e9e1ab9..d0f93915 100644
--- a/packages/lib/queries.ts
+++ b/packages/lib/queries.ts
@@ -1,12 +1,12 @@
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
-import type { useCustomer } from "autumn-js/react"
-import { toast } from "sonner"
-import type { z } from "zod"
-import type { DocumentsWithMemoriesResponseSchema } from "../validation/api"
-import { $fetch } from "./api"
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import type { useCustomer } from "autumn-js/react";
+import { toast } from "sonner";
+import type { z } from "zod";
+import type { DocumentsWithMemoriesResponseSchema } from "../validation/api";
+import { $fetch } from "./api";
-type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
-type DocumentWithMemories = DocumentsResponse["documents"][0]
+type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>;
+type DocumentWithMemories = DocumentsResponse["documents"][0];
export const fetchSubscriptionStatus = (
autumn: ReturnType<typeof useCustomer>,
@@ -18,54 +18,54 @@ export const fetchSubscriptionStatus = (
"memory_starter",
"memory_growth",
"consumer_pro",
- ]
- const statusMap: Record<string, boolean | null> = {}
+ ];
+ const statusMap: Record<string, boolean | null> = {};
await Promise.all(
allPlans.map(async (plan) => {
try {
const res = await autumn.check({
productId: plan,
- })
- statusMap[plan] = res.data?.allowed ?? false
+ });
+ statusMap[plan] = res.data?.allowed ?? false;
} catch (error) {
- console.error(`Error checking status for ${plan}:`, error)
- statusMap[plan] = false
+ console.error(`Error checking status for ${plan}:`, error);
+ statusMap[plan] = false;
}
}),
- )
+ );
- return statusMap
+ return statusMap;
},
queryKey: ["subscription-status"],
refetchInterval: 5000, // Refetch every 5 seconds
staleTime: 4000, // Consider data stale after 4 seconds
- })
+ });
// Feature checks
export const fetchMemoriesFeature = (autumn: ReturnType<typeof useCustomer>) =>
useQuery({
queryFn: async () => {
- const res = await autumn.check({ featureId: "memories" })
- return res.data
+ const res = await autumn.check({ featureId: "memories" });
+ return res.data;
},
queryKey: ["autumn-feature", "memories"],
staleTime: 30 * 1000, // 30 seconds
gcTime: 5 * 60 * 1000, // 5 minutes
- })
+ });
export const fetchConnectionsFeature = (
autumn: ReturnType<typeof useCustomer>,
) =>
useQuery({
queryFn: async () => {
- const res = await autumn.check({ featureId: "connections" })
- return res.data
+ const res = await autumn.check({ featureId: "connections" });
+ return res.data;
},
queryKey: ["autumn-feature", "connections"],
staleTime: 30 * 1000, // 30 seconds
gcTime: 5 * 60 * 1000, // 5 minutes
- })
+ });
// Product checks
export const fetchConsumerProProduct = (
@@ -73,54 +73,54 @@ export const fetchConsumerProProduct = (
) =>
useQuery({
queryFn: async () => {
- const res = await autumn.check({ productId: "consumer_pro" })
- return res.data
+ const res = await autumn.check({ productId: "consumer_pro" });
+ return res.data;
},
queryKey: ["autumn-product", "consumer_pro"],
staleTime: 30 * 1000, // 30 seconds
gcTime: 5 * 60 * 1000, // 5 minutes
- })
+ });
export const fetchProProduct = (autumn: ReturnType<typeof useCustomer>) =>
useQuery({
queryFn: async () => {
- const res = await autumn.check({ productId: "pro" })
- return res.data
+ const res = await autumn.check({ productId: "pro" });
+ return res.data;
},
queryKey: ["autumn-product", "pro"],
staleTime: 30 * 1000, // 30 seconds
gcTime: 5 * 60 * 1000, // 5 minutes
- })
+ });
export const useDeleteDocument = (selectedProject: string) => {
- const queryClient = useQueryClient()
+ const queryClient = useQueryClient();
return useMutation({
mutationFn: async (documentId: string) => {
// context for LLM: delete/memories/:documentId is documents delete endpoint not memories delete endpoint
- const response = await $fetch(`@delete/documents/${documentId}`)
+ const response = await $fetch(`@delete/documents/${documentId}`);
if (response.error) {
- throw new Error(response.error?.message || "Failed to delete document")
+ throw new Error(response.error?.message || "Failed to delete document");
}
- return response.data
+ return response.data;
},
onMutate: async (documentId: string) => {
await queryClient.cancelQueries({
queryKey: ["documents-with-memories", selectedProject],
- })
+ });
const previousData = queryClient.getQueryData([
"documents-with-memories",
selectedProject,
- ])
+ ]);
queryClient.setQueryData(
["documents-with-memories", selectedProject],
(old: unknown) => {
- if (!old || typeof old !== "object") return old
+ if (!old || typeof old !== "object") return old;
const typedOld = old as {
- pages?: Array<{ documents?: DocumentWithMemories[] }>
- }
+ pages?: Array<{ documents?: DocumentWithMemories[] }>;
+ };
return {
...typedOld,
pages: typedOld.pages?.map((page) => ({
@@ -129,30 +129,30 @@ export const useDeleteDocument = (selectedProject: string) => {
(doc: DocumentWithMemories) => doc.id !== documentId,
),
})),
- }
+ };
},
- )
+ );
- return { previousData }
+ return { previousData };
},
onSuccess: () => {
- toast.success("Memory deleted successfully")
+ toast.success("Memory deleted successfully");
},
onError: (error, _documentId, context) => {
if (context?.previousData) {
queryClient.setQueryData(
["documents-with-memories", selectedProject],
context.previousData,
- )
+ );
}
toast.error("Failed to delete memory", {
description: error instanceof Error ? error.message : "Unknown error",
- })
+ });
},
onSettled: () => {
queryClient.invalidateQueries({
queryKey: ["documents-with-memories", selectedProject],
- })
+ });
},
- })
-}
+ });
+};
diff --git a/packages/lib/query-client.tsx b/packages/lib/query-client.tsx
index eb9c5a21..322e9f89 100644
--- a/packages/lib/query-client.tsx
+++ b/packages/lib/query-client.tsx
@@ -1,8 +1,8 @@
-"use client"
+"use client";
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
-import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
-import { useState } from "react"
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
+import { useState } from "react";
export const QueryProvider = ({ children }: { children: React.ReactNode }) => {
const [queryClient] = useState(
@@ -16,12 +16,12 @@ export const QueryProvider = ({ children }: { children: React.ReactNode }) => {
},
},
}),
- )
+ );
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
- )
-}
+ );
+};
diff --git a/packages/lib/similarity.ts b/packages/lib/similarity.ts
index 09d3a2cc..0d43710d 100644
--- a/packages/lib/similarity.ts
+++ b/packages/lib/similarity.ts
@@ -10,27 +10,27 @@ export const cosineSimilarity = (
vectorB: number[],
): number => {
if (vectorA.length !== vectorB.length) {
- throw new Error("Vectors must have the same length")
+ throw new Error("Vectors must have the same length");
}
- let dotProduct = 0
+ let dotProduct = 0;
for (let i = 0; i < vectorA.length; i++) {
- const vectorAi = vectorA[i]
- const vectorBi = vectorB[i]
+ const vectorAi = vectorA[i];
+ const vectorBi = vectorB[i];
if (
typeof vectorAi !== "number" ||
typeof vectorBi !== "number" ||
isNaN(vectorAi) ||
isNaN(vectorBi)
) {
- throw new Error("Vectors must contain only numbers")
+ throw new Error("Vectors must contain only numbers");
}
- dotProduct += vectorAi * vectorBi
+ dotProduct += vectorAi * vectorBi;
}
- return dotProduct
-}
+ return dotProduct;
+};
/**
* Calculate semantic similarity between two documents
@@ -47,13 +47,13 @@ export const calculateSemanticSimilarity = (
document1Embedding.length > 0 &&
document2Embedding.length > 0
) {
- const similarity = cosineSimilarity(document1Embedding, document2Embedding)
+ const similarity = cosineSimilarity(document1Embedding, document2Embedding);
// Convert from [-1, 1] to [0, 1] range
- return similarity >= 0 ? similarity : 0
+ return similarity >= 0 ? similarity : 0;
}
- return 0
-}
+ return 0;
+};
/**
* Calculate semantic similarity between a document and memory entry
@@ -71,34 +71,34 @@ export const calculateDocumentMemorySimilarity = (
documentEmbedding.length > 0 &&
memoryEmbedding.length > 0
) {
- const similarity = cosineSimilarity(documentEmbedding, memoryEmbedding)
+ const similarity = cosineSimilarity(documentEmbedding, memoryEmbedding);
// Convert from [-1, 1] to [0, 1] range
- return similarity >= 0 ? similarity : 0
+ return similarity >= 0 ? similarity : 0;
}
// Fall back to relevance score from database (0-100 scale)
if (relevanceScore !== null && relevanceScore !== undefined) {
- return Math.max(0, Math.min(1, relevanceScore / 100))
+ return Math.max(0, Math.min(1, relevanceScore / 100));
}
// Default similarity for connections without embeddings or relevance scores
- return 0.5
-}
+ return 0.5;
+};
/**
* Get visual properties for connection based on similarity
*/
export const getConnectionVisualProps = (similarity: number) => {
// Ensure similarity is between 0 and 1
- const normalizedSimilarity = Math.max(0, Math.min(1, similarity))
+ const normalizedSimilarity = Math.max(0, Math.min(1, similarity));
return {
opacity: Math.max(0, normalizedSimilarity), // 0 to 1 range
thickness: Math.max(1, normalizedSimilarity * 4), // 1 to 4 pixels
glow: normalizedSimilarity * 0.6, // Glow intensity
pulseDuration: 2000 + (1 - normalizedSimilarity) * 3000, // Faster pulse for higher similarity
- }
-}
+ };
+};
/**
* Generate magical color based on similarity and connection type
@@ -107,9 +107,9 @@ export const getMagicalConnectionColor = (
similarity: number,
hue = 220,
): string => {
- const normalizedSimilarity = Math.max(0, Math.min(1, similarity))
- const saturation = 60 + normalizedSimilarity * 40 // 60% to 100%
- const lightness = 40 + normalizedSimilarity * 30 // 40% to 70%
+ const normalizedSimilarity = Math.max(0, Math.min(1, similarity));
+ const saturation = 60 + normalizedSimilarity * 40; // 60% to 100%
+ const lightness = 40 + normalizedSimilarity * 30; // 40% to 70%
- return `hsl(${hue}, ${saturation}%, ${lightness}%)`
-}
+ return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
+};
diff --git a/packages/lib/utils.ts b/packages/lib/utils.ts
index 02a2ddaf..59e86ae1 100644
--- a/packages/lib/utils.ts
+++ b/packages/lib/utils.ts
@@ -1,8 +1,8 @@
-import { type ClassValue, clsx } from "clsx"
-import { twMerge } from "tailwind-merge"
+import { type ClassValue, clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs))
+ return twMerge(clsx(inputs));
}
-export const isSelfHosted = process.env.NEXT_PUBLIC_HOST_ID !== "supermemory"
+export const isSelfHosted = process.env.NEXT_PUBLIC_HOST_ID !== "supermemory";
diff --git a/packages/tools/src/ai-sdk.ts b/packages/tools/src/ai-sdk.ts
index 703175e7..37f346c2 100644
--- a/packages/tools/src/ai-sdk.ts
+++ b/packages/tools/src/ai-sdk.ts
@@ -1,13 +1,13 @@
-import Supermemory from "supermemory"
-import { tool } from "ai"
-import { z } from "zod"
+import { tool } from "ai";
+import Supermemory from "supermemory";
+import { z } from "zod";
import {
DEFAULT_VALUES,
+ getContainerTags,
PARAMETER_DESCRIPTIONS,
TOOL_DESCRIPTIONS,
- getContainerTags,
-} from "./shared"
-import type { SupermemoryToolsConfig } from "./types"
+} from "./shared";
+import type { SupermemoryToolsConfig } from "./types";
// Export individual tool creators
export const searchMemoriesTool = (
@@ -17,9 +17,9 @@ export const searchMemoriesTool = (
const client = new Supermemory({
apiKey,
...(config?.baseUrl ? { baseURL: config.baseUrl } : {}),
- })
+ });
- const containerTags = getContainerTags(config)
+ const containerTags = getContainerTags(config);
return tool({
description: TOOL_DESCRIPTIONS.searchMemories,
@@ -50,22 +50,22 @@ export const searchMemoriesTool = (
limit,
chunkThreshold: DEFAULT_VALUES.chunkThreshold,
includeFullDocs,
- })
+ });
return {
success: true,
results: response.results,
count: response.results?.length || 0,
- }
+ };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error",
- }
+ };
}
},
- })
-}
+ });
+};
export const addMemoryTool = (
apiKey: string,
@@ -74,9 +74,9 @@ export const addMemoryTool = (
const client = new Supermemory({
apiKey,
...(config?.baseUrl ? { baseURL: config.baseUrl } : {}),
- })
+ });
- const containerTags = getContainerTags(config)
+ const containerTags = getContainerTags(config);
return tool({
description: TOOL_DESCRIPTIONS.addMemory,
@@ -85,27 +85,27 @@ export const addMemoryTool = (
}),
execute: async ({ memory }) => {
try {
- const metadata: Record<string, string | number | boolean> = {}
+ const metadata: Record<string, string | number | boolean> = {};
const response = await client.memories.add({
content: memory,
containerTags,
...(Object.keys(metadata).length > 0 && { metadata }),
- })
+ });
return {
success: true,
memory: response,
- }
+ };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error",
- }
+ };
}
},
- })
-}
+ });
+};
/**
* Create Supermemory tools for AI SDK
@@ -117,5 +117,5 @@ export function supermemoryTools(
return {
searchMemories: searchMemoriesTool(apiKey, config),
addMemory: addMemoryTool(apiKey, config),
- }
+ };
}
diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts
index 4f21246e..588a4fb3 100644
--- a/packages/tools/src/index.ts
+++ b/packages/tools/src/index.ts
@@ -1,2 +1,2 @@
// Export shared types and utilities
-export type { SupermemoryToolsConfig } from "./types"
+export type { SupermemoryToolsConfig } from "./types";
diff --git a/packages/tools/src/openai.ts b/packages/tools/src/openai.ts
index 5c79a9c1..197a5829 100644
--- a/packages/tools/src/openai.ts
+++ b/packages/tools/src/openai.ts
@@ -1,27 +1,27 @@
-import type OpenAI from "openai"
-import Supermemory from "supermemory"
+import type OpenAI from "openai";
+import Supermemory from "supermemory";
import {
DEFAULT_VALUES,
+ getContainerTags,
PARAMETER_DESCRIPTIONS,
TOOL_DESCRIPTIONS,
- getContainerTags,
-} from "./shared"
-import type { SupermemoryToolsConfig } from "./types"
+} from "./shared";
+import type { SupermemoryToolsConfig } from "./types";
/**
* Result types for memory operations
*/
export interface MemorySearchResult {
- success: boolean
- results?: Awaited<ReturnType<Supermemory["search"]["execute"]>>["results"]
- count?: number
- error?: string
+ success: boolean;
+ results?: Awaited<ReturnType<Supermemory["search"]["execute"]>>["results"];
+ count?: number;
+ error?: string;
}
export interface MemoryAddResult {
- success: boolean
- memory?: Awaited<ReturnType<Supermemory["memories"]["add"]>>
- error?: string
+ success: boolean;
+ memory?: Awaited<ReturnType<Supermemory["memories"]["add"]>>;
+ error?: string;
}
/**
@@ -67,7 +67,7 @@ export const memoryToolSchemas = {
required: ["memory"],
},
} satisfies OpenAI.FunctionDefinition,
-} as const
+} as const;
/**
* Create a Supermemory client with configuration
@@ -76,11 +76,11 @@ function createClient(apiKey: string, config?: SupermemoryToolsConfig) {
const client = new Supermemory({
apiKey,
...(config?.baseUrl && { baseURL: config.baseUrl }),
- })
+ });
- const containerTags = getContainerTags(config)
+ const containerTags = getContainerTags(config);
- return { client, containerTags }
+ return { client, containerTags };
}
/**
@@ -90,16 +90,16 @@ export function createSearchMemoriesFunction(
apiKey: string,
config?: SupermemoryToolsConfig,
) {
- const { client, containerTags } = createClient(apiKey, config)
+ const { client, containerTags } = createClient(apiKey, config);
return async function searchMemories({
informationToGet,
includeFullDocs = DEFAULT_VALUES.includeFullDocs,
limit = DEFAULT_VALUES.limit,
}: {
- informationToGet: string
- includeFullDocs?: boolean
- limit?: number
+ informationToGet: string;
+ includeFullDocs?: boolean;
+ limit?: number;
}): Promise<MemorySearchResult> {
try {
const response = await client.search.execute({
@@ -108,20 +108,20 @@ export function createSearchMemoriesFunction(
limit,
chunkThreshold: DEFAULT_VALUES.chunkThreshold,
includeFullDocs,
- })
+ });
return {
success: true,
results: response.results,
count: response.results?.length || 0,
- }
+ };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error",
- }
+ };
}
- }
+ };
}
/**
@@ -131,33 +131,33 @@ export function createAddMemoryFunction(
apiKey: string,
config?: SupermemoryToolsConfig,
) {
- const { client, containerTags } = createClient(apiKey, config)
+ const { client, containerTags } = createClient(apiKey, config);
return async function addMemory({
memory,
}: {
- memory: string
+ memory: string;
}): Promise<MemoryAddResult> {
try {
- const metadata: Record<string, string | number | boolean> = {}
+ const metadata: Record<string, string | number | boolean> = {};
const response = await client.memories.add({
content: memory,
containerTags,
...(Object.keys(metadata).length > 0 && { metadata }),
- })
+ });
return {
success: true,
memory: response,
- }
+ };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error",
- }
+ };
}
- }
+ };
}
/**
@@ -167,13 +167,13 @@ export function supermemoryTools(
apiKey: string,
config?: SupermemoryToolsConfig,
) {
- const searchMemories = createSearchMemoriesFunction(apiKey, config)
- const addMemory = createAddMemoryFunction(apiKey, config)
+ const searchMemories = createSearchMemoriesFunction(apiKey, config);
+ const addMemory = createAddMemoryFunction(apiKey, config);
return {
searchMemories,
addMemory,
- }
+ };
}
/**
@@ -183,7 +183,7 @@ export function getToolDefinitions(): OpenAI.Chat.Completions.ChatCompletionTool
return [
{ type: "function", function: memoryToolSchemas.searchMemories },
{ type: "function", function: memoryToolSchemas.addMemory },
- ]
+ ];
}
/**
@@ -193,26 +193,26 @@ export function createToolCallExecutor(
apiKey: string,
config?: SupermemoryToolsConfig,
) {
- const tools = supermemoryTools(apiKey, config)
+ const tools = supermemoryTools(apiKey, config);
return async function executeToolCall(
toolCall: OpenAI.Chat.Completions.ChatCompletionMessageToolCall,
): Promise<string> {
- const functionName = toolCall.function.name
- const args = JSON.parse(toolCall.function.arguments)
+ const functionName = toolCall.function.name;
+ const args = JSON.parse(toolCall.function.arguments);
switch (functionName) {
case "searchMemories":
- return JSON.stringify(await tools.searchMemories(args))
+ return JSON.stringify(await tools.searchMemories(args));
case "addMemory":
- return JSON.stringify(await tools.addMemory(args))
+ return JSON.stringify(await tools.addMemory(args));
default:
return JSON.stringify({
success: false,
error: `Unknown function: ${functionName}`,
- })
+ });
}
- }
+ };
}
/**
@@ -222,24 +222,24 @@ export function createToolCallsExecutor(
apiKey: string,
config?: SupermemoryToolsConfig,
) {
- const executeToolCall = createToolCallExecutor(apiKey, config)
+ const executeToolCall = createToolCallExecutor(apiKey, config);
return async function executeToolCalls(
toolCalls: OpenAI.Chat.Completions.ChatCompletionMessageToolCall[],
): Promise<OpenAI.Chat.Completions.ChatCompletionToolMessageParam[]> {
const results = await Promise.all(
toolCalls.map(async (toolCall) => {
- const result = await executeToolCall(toolCall)
+ const result = await executeToolCall(toolCall);
return {
tool_call_id: toolCall.id,
role: "tool" as const,
content: result,
- }
+ };
}),
- )
+ );
- return results
- }
+ return results;
+ };
}
/**
@@ -249,7 +249,7 @@ export function createSearchMemoriesTool(
apiKey: string,
config?: SupermemoryToolsConfig,
) {
- const searchMemories = createSearchMemoriesFunction(apiKey, config)
+ const searchMemories = createSearchMemoriesFunction(apiKey, config);
return {
definition: {
@@ -257,14 +257,14 @@ export function createSearchMemoriesTool(
function: memoryToolSchemas.searchMemories,
},
execute: searchMemories,
- }
+ };
}
export function createAddMemoryTool(
apiKey: string,
config?: SupermemoryToolsConfig,
) {
- const addMemory = createAddMemoryFunction(apiKey, config)
+ const addMemory = createAddMemoryFunction(apiKey, config);
return {
definition: {
@@ -272,5 +272,5 @@ export function createAddMemoryTool(
function: memoryToolSchemas.addMemory,
},
execute: addMemory,
- }
+ };
}
diff --git a/packages/tools/src/shared.ts b/packages/tools/src/shared.ts
index 0ff14e86..ffe07a11 100644
--- a/packages/tools/src/shared.ts
+++ b/packages/tools/src/shared.ts
@@ -8,7 +8,7 @@ export const TOOL_DESCRIPTIONS = {
"Search (recall) memories/details/information about the user or other facts or entities. Run when explicitly asked or when context about user's past choices would be helpful.",
addMemory:
"Add (remember) memories/details/information about the user or other facts or entities. Run when explicitly asked or when the user mentions any information generalizable beyond the context of the current conversation.",
-} as const
+} as const;
// Parameter descriptions
export const PARAMETER_DESCRIPTIONS = {
@@ -18,30 +18,30 @@ export const PARAMETER_DESCRIPTIONS = {
limit: "Maximum number of results to return",
memory:
"The text content of the memory to add. This should be a single sentence or a short paragraph.",
-} as const
+} as const;
// Default values
export const DEFAULT_VALUES = {
includeFullDocs: true,
limit: 10,
chunkThreshold: 0.6,
-} as const
+} as const;
// Container tag constants
export const CONTAINER_TAG_CONSTANTS = {
projectPrefix: "sm_project_",
defaultTags: ["sm_project_default"] as string[],
-} as const
+} as const;
/**
* Helper function to generate container tags based on config
*/
export function getContainerTags(config?: {
- projectId?: string
- containerTags?: string[]
+ projectId?: string;
+ containerTags?: string[];
}): string[] {
if (config?.projectId) {
- return [`${CONTAINER_TAG_CONSTANTS.projectPrefix}${config.projectId}`]
+ return [`${CONTAINER_TAG_CONSTANTS.projectPrefix}${config.projectId}`];
}
- return config?.containerTags ?? CONTAINER_TAG_CONSTANTS.defaultTags
+ return config?.containerTags ?? CONTAINER_TAG_CONSTANTS.defaultTags;
}
diff --git a/packages/tools/src/tools.test.ts b/packages/tools/src/tools.test.ts
index 5cde729d..bcff0f35 100644
--- a/packages/tools/src/tools.test.ts
+++ b/packages/tools/src/tools.test.ts
@@ -1,70 +1,72 @@
-import { createOpenAI } from "@ai-sdk/openai"
-import { generateText } from "ai"
-import { describe, expect, it } from "vitest"
-import * as aiSdk from "./ai-sdk"
-import * as openAi from "./openai"
-import type { SupermemoryToolsConfig } from "./types"
+import { createOpenAI } from "@ai-sdk/openai";
+import { generateText } from "ai";
+import { describe, expect, it } from "vitest";
+import * as aiSdk from "./ai-sdk";
+import * as openAi from "./openai";
+import type { SupermemoryToolsConfig } from "./types";
-import "dotenv/config"
+import "dotenv/config";
describe("@supermemory/tools", () => {
// Required API keys - tests will fail if not provided
- const testApiKey = process.env.SUPERMEMORY_API_KEY
- const testOpenAIKey = process.env.OPENAI_API_KEY
+ const testApiKey = process.env.SUPERMEMORY_API_KEY;
+ const testOpenAIKey = process.env.OPENAI_API_KEY;
if (!testApiKey) {
throw new Error(
"SUPERMEMORY_API_KEY environment variable is required for tests",
- )
+ );
}
if (!testOpenAIKey) {
- throw new Error("OPENAI_API_KEY environment variable is required for tests")
+ throw new Error(
+ "OPENAI_API_KEY environment variable is required for tests",
+ );
}
// Optional configuration with defaults
- const testBaseUrl = process.env.SUPERMEMORY_BASE_URL ?? undefined
- const testModelName = process.env.MODEL_NAME || "gpt-4o-mini"
+ const testBaseUrl = process.env.SUPERMEMORY_BASE_URL ?? undefined;
+ const testModelName = process.env.MODEL_NAME || "gpt-4o-mini";
describe("aiSdk module", () => {
describe("client initialization", () => {
it("should create tools with default configuration", () => {
- const config: SupermemoryToolsConfig = {}
- const tools = aiSdk.supermemoryTools(testApiKey, config)
+ const config: SupermemoryToolsConfig = {};
+ const tools = aiSdk.supermemoryTools(testApiKey, config);
- expect(tools).toBeDefined()
- expect(tools.searchMemories).toBeDefined()
- expect(tools.addMemory).toBeDefined()
- })
+ expect(tools).toBeDefined();
+ expect(tools.searchMemories).toBeDefined();
+ expect(tools.addMemory).toBeDefined();
+ });
it("should create tools with custom baseUrl", () => {
const config: SupermemoryToolsConfig = {
baseUrl: testBaseUrl,
- }
- const tools = aiSdk.supermemoryTools(testApiKey, config)
+ };
+ const tools = aiSdk.supermemoryTools(testApiKey, config);
- expect(tools).toBeDefined()
- expect(tools.searchMemories).toBeDefined()
- expect(tools.addMemory).toBeDefined()
- })
+ expect(tools).toBeDefined();
+ expect(tools.searchMemories).toBeDefined();
+ expect(tools.addMemory).toBeDefined();
+ });
it("should create individual tools", () => {
const searchTool = aiSdk.searchMemoriesTool(testApiKey, {
projectId: "test-project-123",
- })
+ });
const addTool = aiSdk.addMemoryTool(testApiKey, {
projectId: "test-project-123",
- })
+ });
- expect(searchTool).toBeDefined()
- expect(addTool).toBeDefined()
- })
- })
+ expect(searchTool).toBeDefined();
+ expect(addTool).toBeDefined();
+ });
+ });
describe("AI SDK integration", () => {
it("should work with AI SDK generateText", async () => {
const openai = createOpenAI({
apiKey: testOpenAIKey,
- })
+ });
const result = await generateText({
model: openai(testModelName),
@@ -85,22 +87,22 @@ describe("@supermemory/tools", () => {
baseUrl: testBaseUrl,
}),
},
- })
+ });
- expect(result).toBeDefined()
- expect(result.text).toBeDefined()
- expect(typeof result.text).toBe("string")
- })
+ expect(result).toBeDefined();
+ expect(result.text).toBeDefined();
+ expect(typeof result.text).toBe("string");
+ });
it("should use tools when prompted", async () => {
const openai = createOpenAI({
apiKey: testOpenAIKey,
- })
+ });
const tools = aiSdk.supermemoryTools(testApiKey, {
projectId: "test-tool-usage",
baseUrl: testBaseUrl,
- })
+ });
const result = await generateText({
model: openai(testModelName),
@@ -118,157 +120,159 @@ describe("@supermemory/tools", () => {
tools: {
addMemory: tools.addMemory,
},
- })
+ });
- expect(result).toBeDefined()
- expect(result.text).toBeDefined()
- })
- })
- })
+ expect(result).toBeDefined();
+ expect(result.text).toBeDefined();
+ });
+ });
+ });
describe("openAi module", () => {
describe("function-based tools", () => {
it("should create function-based tools", () => {
const tools = openAi.supermemoryTools(testApiKey, {
projectId: "test-openai-functions",
- })
+ });
- expect(tools).toBeDefined()
- expect(tools.searchMemories).toBeDefined()
- expect(tools.addMemory).toBeDefined()
- })
+ expect(tools).toBeDefined();
+ expect(tools.searchMemories).toBeDefined();
+ expect(tools.addMemory).toBeDefined();
+ });
it("should create individual tool functions", () => {
const searchFunction = openAi.createSearchMemoriesFunction(testApiKey, {
projectId: "test-individual",
- })
+ });
const addFunction = openAi.createAddMemoryFunction(testApiKey, {
projectId: "test-individual",
- })
+ });
- expect(searchFunction).toBeDefined()
- expect(addFunction).toBeDefined()
- expect(typeof searchFunction).toBe("function")
- expect(typeof addFunction).toBe("function")
- })
- })
+ expect(searchFunction).toBeDefined();
+ expect(addFunction).toBeDefined();
+ expect(typeof searchFunction).toBe("function");
+ expect(typeof addFunction).toBe("function");
+ });
+ });
describe("tool definitions", () => {
it("should return proper OpenAI function definitions", () => {
- const definitions = openAi.getToolDefinitions()
+ const definitions = openAi.getToolDefinitions();
- expect(definitions).toBeDefined()
- expect(definitions.length).toBe(2)
+ expect(definitions).toBeDefined();
+ expect(definitions.length).toBe(2);
// Check searchMemories
const searchTool = definitions.find(
(d) => d.function.name === "searchMemories",
- )
- expect(searchTool).toBeDefined()
- expect(searchTool!.type).toBe("function")
+ );
+ expect(searchTool).toBeDefined();
+ expect(searchTool!.type).toBe("function");
expect(searchTool!.function.parameters?.required).toContain(
"informationToGet",
- )
+ );
// Check addMemory
- const addTool = definitions.find((d) => d.function.name === "addMemory")
- expect(addTool).toBeDefined()
- expect(addTool!.type).toBe("function")
- expect(addTool!.function.parameters?.required).toContain("memory")
- })
- })
+ const addTool = definitions.find(
+ (d) => d.function.name === "addMemory",
+ );
+ expect(addTool).toBeDefined();
+ expect(addTool!.type).toBe("function");
+ expect(addTool!.function.parameters?.required).toContain("memory");
+ });
+ });
describe("tool execution", () => {
it("should create tool call executor", () => {
const executor = openAi.createToolCallExecutor(testApiKey, {
containerTags: ["test-executor"],
baseUrl: testBaseUrl,
- })
+ });
- expect(executor).toBeDefined()
- expect(typeof executor).toBe("function")
- })
+ expect(executor).toBeDefined();
+ expect(typeof executor).toBe("function");
+ });
it("should create tool calls executor", () => {
const executor = openAi.createToolCallsExecutor(testApiKey, {
containerTags: ["test-executors"],
baseUrl: testBaseUrl,
- })
+ });
- expect(executor).toBeDefined()
- expect(typeof executor).toBe("function")
- })
- })
+ expect(executor).toBeDefined();
+ expect(typeof executor).toBe("function");
+ });
+ });
describe("individual tool creators", () => {
it("should create individual search tool", () => {
const searchTool = openAi.createSearchMemoriesTool(testApiKey, {
projectId: "test-individual",
- })
+ });
- expect(searchTool).toBeDefined()
- expect(searchTool.definition).toBeDefined()
- expect(searchTool.execute).toBeDefined()
- expect(searchTool.definition.function.name).toBe("searchMemories")
- })
+ expect(searchTool).toBeDefined();
+ expect(searchTool.definition).toBeDefined();
+ expect(searchTool.execute).toBeDefined();
+ expect(searchTool.definition.function.name).toBe("searchMemories");
+ });
it("should create individual add tool", () => {
const addTool = openAi.createAddMemoryTool(testApiKey, {
projectId: "test-individual",
- })
+ });
- expect(addTool).toBeDefined()
- expect(addTool.definition).toBeDefined()
- expect(addTool.execute).toBeDefined()
- expect(addTool.definition.function.name).toBe("addMemory")
- })
- })
+ expect(addTool).toBeDefined();
+ expect(addTool.definition).toBeDefined();
+ expect(addTool.execute).toBeDefined();
+ expect(addTool.definition.function.name).toBe("addMemory");
+ });
+ });
describe("memory operations", () => {
it("should search memories", async () => {
const searchFunction = openAi.createSearchMemoriesFunction(testApiKey, {
projectId: "test-search",
baseUrl: testBaseUrl,
- })
+ });
const result = await searchFunction({
informationToGet: "test preferences",
limit: 5,
- })
+ });
- expect(result).toBeDefined()
- expect(result.success).toBeDefined()
- expect(typeof result.success).toBe("boolean")
+ expect(result).toBeDefined();
+ expect(result.success).toBeDefined();
+ expect(typeof result.success).toBe("boolean");
if (result.success) {
- expect(result.results).toBeDefined()
- expect(result.count).toBeDefined()
- expect(typeof result.count).toBe("number")
+ expect(result.results).toBeDefined();
+ expect(result.count).toBeDefined();
+ expect(typeof result.count).toBe("number");
} else {
- expect(result.error).toBeDefined()
+ expect(result.error).toBeDefined();
}
- })
+ });
it("should add memory", async () => {
const addFunction = openAi.createAddMemoryFunction(testApiKey, {
containerTags: ["test-add-memory"],
baseUrl: testBaseUrl,
- })
+ });
const result = await addFunction({
memory: "User prefers dark roast coffee in the morning - test memory",
- })
+ });
- expect(result).toBeDefined()
- expect(result.success).toBeDefined()
- expect(typeof result.success).toBe("boolean")
+ expect(result).toBeDefined();
+ expect(result.success).toBeDefined();
+ expect(typeof result.success).toBe("boolean");
if (result.success) {
- expect(result.memory).toBeDefined()
+ expect(result.memory).toBeDefined();
} else {
- expect(result.error).toBeDefined()
+ expect(result.error).toBeDefined();
}
- })
- })
- })
-})
+ });
+ });
+ });
+});
diff --git a/packages/tools/src/types.ts b/packages/tools/src/types.ts
index dfff0f00..86c7fc16 100644
--- a/packages/tools/src/types.ts
+++ b/packages/tools/src/types.ts
@@ -3,7 +3,7 @@
* Only one of `projectId` or `containerTags` can be provided.
*/
export interface SupermemoryToolsConfig {
- baseUrl?: string
- containerTags?: string[]
- projectId?: string
+ baseUrl?: string;
+ containerTags?: string[];
+ projectId?: string;
}
diff --git a/packages/tools/tsdown.config.ts b/packages/tools/tsdown.config.ts
index 59be1b93..60005c69 100644
--- a/packages/tools/tsdown.config.ts
+++ b/packages/tools/tsdown.config.ts
@@ -1,4 +1,4 @@
-import { defineConfig } from "tsdown"
+import { defineConfig } from "tsdown";
export default defineConfig({
entry: ["src/index.ts", "src/ai-sdk.ts", "src/openai.ts"],
@@ -12,4 +12,4 @@ export default defineConfig({
sourcemap: false,
},
exports: true,
-})
+});
diff --git a/packages/ui/memory-graph/hooks/use-graph-data.ts b/packages/ui/memory-graph/hooks/use-graph-data.ts
index 3e9fa5cc..ec17a756 100644
--- a/packages/ui/memory-graph/hooks/use-graph-data.ts
+++ b/packages/ui/memory-graph/hooks/use-graph-data.ts
@@ -29,26 +29,26 @@ export function useGraphData(
const allEdges: GraphEdge[] = [];
// Filter documents that have memories in selected space
- const filteredDocuments = data.documents
+ const filteredDocuments = (data.documents || [])
.map((doc) => ({
...doc,
memoryEntries:
selectedSpace === "all"
- ? doc.memoryEntries
- : doc.memoryEntries.filter(
+ ? doc.memoryEntries || []
+ : (doc.memoryEntries || []).filter(
(memory) =>
(memory.spaceContainerTag ?? memory.spaceId ?? "default") ===
selectedSpace,
),
}))
- .filter((doc) => doc.memoryEntries.length > 0);
+ .filter((doc) => (doc.memoryEntries || []).length > 0);
// Group documents by space for better clustering
const documentsBySpace = new Map<string, typeof filteredDocuments>();
filteredDocuments.forEach((doc) => {
const docSpace =
- doc.memoryEntries[0]?.spaceContainerTag ??
- doc.memoryEntries[0]?.spaceId ??
+ (doc.memoryEntries || [])[0]?.spaceContainerTag ??
+ (doc.memoryEntries || [])[0]?.spaceId ??
"default";
if (!documentsBySpace.has(docSpace)) {
documentsBySpace.set(docSpace, []);
@@ -171,7 +171,7 @@ export function useGraphData(
const memoryNodeMap = new Map<string, GraphNode>();
const doc = docNode.data as DocumentWithMemories;
- doc.memoryEntries.forEach((memory, memIndex) => {
+ (doc.memoryEntries || []).forEach((memory, memIndex) => {
const memoryId = `${memory.id}`;
const customMemPos = nodePositions.get(memoryId);
@@ -231,8 +231,8 @@ export function useGraphData(
});
// Add version-chain edges (old -> new)
- data.documents.forEach((doc) => {
- doc.memoryEntries.forEach((mem: MemoryEntry) => {
+ (data.documents || []).forEach((doc) => {
+ (doc.memoryEntries || []).forEach((mem: MemoryEntry) => {
// Support both new object structure and legacy array/single parent fields
let parentRelations: Record<string, MemoryRelation> = {};
diff --git a/packages/ui/memory-graph/memory-graph.tsx b/packages/ui/memory-graph/memory-graph.tsx
index 912a741a..72ddf090 100644
--- a/packages/ui/memory-graph/memory-graph.tsx
+++ b/packages/ui/memory-graph/memory-graph.tsx
@@ -157,8 +157,8 @@ export const MemoryGraph = ({
const spaceSet = new Set<string>();
const counts: Record<string, number> = {};
- data.documents.forEach((doc) => {
- doc.memoryEntries.forEach((memory) => {
+ (data.documents || []).forEach((doc) => {
+ (doc.memoryEntries || []).forEach((memory) => {
const spaceId = memory.spaceContainerTag || memory.spaceId || "default";
spaceSet.add(spaceId);
counts[spaceId] = (counts[spaceId] || 0) + 1;
@@ -199,32 +199,47 @@ export const MemoryGraph = ({
const handleCenter = useCallback(() => {
if (nodes.length > 0) {
// Calculate center of all nodes
- let sumX = 0
- let sumY = 0
- let count = 0
-
+ let sumX = 0;
+ let sumY = 0;
+ let count = 0;
+
nodes.forEach((node) => {
- sumX += node.x
- sumY += node.y
- count++
- })
-
+ sumX += node.x;
+ sumY += node.y;
+ count++;
+ });
+
if (count > 0) {
- const centerX = sumX / count
- const centerY = sumY / count
- centerViewportOn(centerX, centerY, containerSize.width, containerSize.height)
+ const centerX = sumX / count;
+ const centerY = sumY / count;
+ centerViewportOn(
+ centerX,
+ centerY,
+ containerSize.width,
+ containerSize.height,
+ );
}
}
- }, [nodes, centerViewportOn, containerSize.width, containerSize.height])
+ }, [nodes, centerViewportOn, containerSize.width, containerSize.height]);
const handleAutoFit = useCallback(() => {
- if (nodes.length > 0 && containerSize.width > 0 && containerSize.height > 0) {
+ if (
+ nodes.length > 0 &&
+ containerSize.width > 0 &&
+ containerSize.height > 0
+ ) {
autoFitToViewport(nodes, containerSize.width, containerSize.height, {
occludedRightPx,
animate: true,
- })
+ });
}
- }, [nodes, containerSize.width, containerSize.height, occludedRightPx, autoFitToViewport])
+ }, [
+ nodes,
+ containerSize.width,
+ containerSize.height,
+ occludedRightPx,
+ autoFitToViewport,
+ ]);
// Get selected node data
const selectedNodeData = useMemo(() => {
@@ -251,7 +266,7 @@ export const MemoryGraph = ({
};
// Count visible documents
- const visibleDocuments = data.documents.filter((doc) => {
+ const visibleDocuments = (data.documents || []).filter((doc) => {
const docNodes = nodes.filter(
(node) => node.type === "document" && node.data.id === doc.id,
);
@@ -265,7 +280,8 @@ export const MemoryGraph = ({
});
// If 80% or more of documents are visible, load more
- const visibilityRatio = visibleDocuments.length / data.documents.length;
+ const visibilityRatio =
+ visibleDocuments.length / (data.documents || []).length;
if (visibilityRatio >= 0.8) {
loadMoreDocuments();
}
@@ -421,8 +437,12 @@ export const MemoryGraph = ({
{containerSize.width > 0 && (
<NavigationControls
onCenter={handleCenter}
- onZoomIn={() => zoomIn(containerSize.width / 2, containerSize.height / 2)}
- onZoomOut={() => zoomOut(containerSize.width / 2, containerSize.height / 2)}
+ onZoomIn={() =>
+ zoomIn(containerSize.width / 2, containerSize.height / 2)
+ }
+ onZoomOut={() =>
+ zoomOut(containerSize.width / 2, containerSize.height / 2)
+ }
onAutoFit={handleAutoFit}
nodes={nodes}
className="absolute bottom-4 left-4"
diff --git a/packages/ui/memory-graph/navigation-controls.tsx b/packages/ui/memory-graph/navigation-controls.tsx
index b2abd67f..afd98a0f 100644
--- a/packages/ui/memory-graph/navigation-controls.tsx
+++ b/packages/ui/memory-graph/navigation-controls.tsx
@@ -1,67 +1,62 @@
-"use client"
+"use client";
-import { memo } from "react"
-import type { GraphNode } from "./types"
+import { memo } from "react";
+import type { GraphNode } from "./types";
interface NavigationControlsProps {
- onCenter: () => void
- onZoomIn: () => void
- onZoomOut: () => void
- onAutoFit: () => void
- nodes: GraphNode[]
- className?: string
+ onCenter: () => void;
+ onZoomIn: () => void;
+ onZoomOut: () => void;
+ onAutoFit: () => void;
+ nodes: GraphNode[];
+ className?: string;
}
-export const NavigationControls = memo<NavigationControlsProps>(({
- onCenter,
- onZoomIn,
- onZoomOut,
- onAutoFit,
- nodes,
- className = "",
-}) => {
- if (nodes.length === 0) {
- return null
- }
+export const NavigationControls = memo<NavigationControlsProps>(
+ ({ onCenter, onZoomIn, onZoomOut, onAutoFit, nodes, className = "" }) => {
+ if (nodes.length === 0) {
+ return null;
+ }
- return (
- <div className={`flex flex-col gap-1 ${className}`}>
- <button
- type="button"
- onClick={onAutoFit}
- className="bg-black/20 backdrop-blur-sm hover:bg-black/30 border border-white/10 hover:border-white/20 rounded-lg p-2 text-white/70 hover:text-white transition-colors text-xs font-medium min-w-16"
- title="Auto-fit graph to viewport"
- >
- Fit
- </button>
- <button
- type="button"
- onClick={onCenter}
- className="bg-black/20 backdrop-blur-sm hover:bg-black/30 border border-white/10 hover:border-white/20 rounded-lg p-2 text-white/70 hover:text-white transition-colors text-xs font-medium min-w-16"
- title="Center view on graph"
- >
- Center
- </button>
- <div className="flex flex-col">
+ return (
+ <div className={`flex flex-col gap-1 ${className}`}>
<button
type="button"
- onClick={onZoomIn}
- className="bg-black/20 backdrop-blur-sm hover:bg-black/30 border border-white/10 hover:border-white/20 rounded-t-lg p-2 text-white/70 hover:text-white transition-colors text-xs font-medium min-w-16 border-b-0"
- title="Zoom in"
+ onClick={onAutoFit}
+ className="bg-black/20 backdrop-blur-sm hover:bg-black/30 border border-white/10 hover:border-white/20 rounded-lg p-2 text-white/70 hover:text-white transition-colors text-xs font-medium min-w-16"
+ title="Auto-fit graph to viewport"
>
- +
+ Fit
</button>
<button
type="button"
- onClick={onZoomOut}
- className="bg-black/20 backdrop-blur-sm hover:bg-black/30 border border-white/10 hover:border-white/20 rounded-b-lg p-2 text-white/70 hover:text-white transition-colors text-xs font-medium min-w-16"
- title="Zoom out"
+ onClick={onCenter}
+ className="bg-black/20 backdrop-blur-sm hover:bg-black/30 border border-white/10 hover:border-white/20 rounded-lg p-2 text-white/70 hover:text-white transition-colors text-xs font-medium min-w-16"
+ title="Center view on graph"
>
- −
+ Center
</button>
+ <div className="flex flex-col">
+ <button
+ type="button"
+ onClick={onZoomIn}
+ className="bg-black/20 backdrop-blur-sm hover:bg-black/30 border border-white/10 hover:border-white/20 rounded-t-lg p-2 text-white/70 hover:text-white transition-colors text-xs font-medium min-w-16 border-b-0"
+ title="Zoom in"
+ >
+ +
+ </button>
+ <button
+ type="button"
+ onClick={onZoomOut}
+ className="bg-black/20 backdrop-blur-sm hover:bg-black/30 border border-white/10 hover:border-white/20 rounded-b-lg p-2 text-white/70 hover:text-white transition-colors text-xs font-medium min-w-16"
+ title="Zoom out"
+ >
+ −
+ </button>
+ </div>
</div>
- </div>
- )
-})
+ );
+ },
+);
-NavigationControls.displayName = "NavigationControls" \ No newline at end of file
+NavigationControls.displayName = "NavigationControls";
diff --git a/packages/ui/pages/login.tsx b/packages/ui/pages/login.tsx
index 74765da1..387309cb 100644
--- a/packages/ui/pages/login.tsx
+++ b/packages/ui/pages/login.tsx
@@ -5,8 +5,8 @@ import { usePostHog } from "@lib/posthog";
import { LogoFull } from "@repo/ui/assets/Logo";
import { TextSeparator } from "@repo/ui/components/text-separator";
import { ExternalAuthButton } from "@ui/button/external-auth";
-import { Button } from "@ui/components/button";
import { Badge } from "@ui/components/badge";
+import { Button } from "@ui/components/button";
import {
Carousel,
CarouselContent,
@@ -20,7 +20,7 @@ import { Title1Bold } from "@ui/text/title/title-1-bold";
import Autoplay from "embla-carousel-autoplay";
import Image from "next/image";
import { useRouter, useSearchParams } from "next/navigation";
-import { useState, useEffect } from "react";
+import { useEffect, useState } from "react";
export function LoginPage({
heroText = "The unified memory API for the AI era.",
diff --git a/packages/validation/api.ts b/packages/validation/api.ts
index 91ac5e09..0363cf2c 100644
--- a/packages/validation/api.ts
+++ b/packages/validation/api.ts
@@ -1,21 +1,21 @@
-import { z } from "zod"
-import "zod-openapi/extend"
+import { z } from "zod";
+import "zod-openapi/extend";
import {
MetadataSchema as BaseMetadataSchema,
DocumentSchema,
MemoryEntrySchema,
OrganizationSettingsSchema,
RequestTypeEnum,
-} from "./schemas"
+} from "./schemas";
-export const MetadataSchema = BaseMetadataSchema
+export const MetadataSchema = BaseMetadataSchema;
export const SearchFiltersSchema = z
.object({
AND: z.array(z.unknown()).optional(),
OR: z.array(z.unknown()).optional(),
})
- .or(z.record(z.unknown()))
+ .or(z.record(z.unknown()));
const exampleMetadata: Record<string, string | number | boolean> = {
category: "technology",
@@ -24,7 +24,7 @@ const exampleMetadata: Record<string, string | number | boolean> = {
source: "web",
tag_1: "ai",
tag_2: "machine-learning",
-} as const
+} as const;
const exampleMemory = {
connectionId: "conn_123",
@@ -45,7 +45,7 @@ const exampleMemory = {
type: "text",
updatedAt: new Date().toISOString(),
url: "https://example.com/article",
-} as const
+} as const;
export const MemorySchema = z
.object({
@@ -136,7 +136,7 @@ export const MemorySchema = z
.openapi({
description: "Memory object",
example: exampleMemory,
- })
+ });
export const MemoryUpdateSchema = z.object({
containerTags: z
@@ -162,9 +162,9 @@ export const MemoryUpdateSchema = z.object({
"Optional metadata for the memory. This is used to store additional information about the memory. You can use this to store any additional information you need about the memory. Metadata can be filtered through. Keys must be strings and are case sensitive. Values can be strings, numbers, or booleans. You cannot nest objects.",
example: exampleMetadata,
}),
-})
+});
-export const MemoryAddSchema = MemoryUpdateSchema
+export const MemoryAddSchema = MemoryUpdateSchema;
export const PaginationSchema = z
.object({
@@ -181,9 +181,9 @@ export const PaginationSchema = z
totalItems: 100,
totalPages: 10,
},
- })
+ });
-export const GetMemoryResponseSchema = MemorySchema
+export const GetMemoryResponseSchema = MemorySchema;
export const ListMemoriesResponseSchema = z
.object({
@@ -229,7 +229,7 @@ export const ListMemoriesResponseSchema = z
totalPages: 10,
},
},
- })
+ });
export const ListMemoriesQuerySchema = z
.object({
@@ -317,12 +317,12 @@ export const ListMemoriesQuerySchema = z
page: 1,
sort: "createdAt",
},
- })
+ });
export const MemoryResponseSchema = z.object({
id: z.string(),
status: z.string(),
-})
+});
export const SearchRequestSchema = z.object({
categoriesFilter: z
@@ -455,7 +455,7 @@ export const SearchRequestSchema = z.object({
"If true, rewrites the query to make it easier to find documents. This increases the latency by about 400ms",
example: false,
}),
-})
+});
export const Searchv4RequestSchema = z.object({
containerTag: z.string().optional().openapi({
@@ -546,7 +546,7 @@ export const Searchv4RequestSchema = z.object({
"If true, rewrites the query to make it easier to find documents. This increases the latency by about 400ms",
example: false,
}),
-})
+});
export const SearchResultSchema = z.object({
chunks: z
@@ -632,13 +632,13 @@ export const SearchResultSchema = z.object({
description: "Document type",
example: "web",
}),
-})
+});
export const SearchResponseSchema = z.object({
results: z.array(SearchResultSchema),
timing: z.number(),
total: z.number(),
-})
+});
// V4 Memory Search Schemas
export const MemorySearchDocumentSchema = z.object({
@@ -666,7 +666,7 @@ export const MemorySearchDocumentSchema = z.object({
description: "Document last update date",
format: "date-time",
}),
-})
+});
export const MemorySearchResult = z.object({
id: z.string().openapi({
@@ -765,7 +765,7 @@ export const MemorySearchResult = z.object({
documents: z.array(MemorySearchDocumentSchema).optional().openapi({
description: "Associated documents for this memory entry",
}),
-})
+});
export const MemorySearchResponseSchema = z.object({
results: z.array(MemorySearchResult).openapi({
@@ -779,7 +779,7 @@ export const MemorySearchResponseSchema = z.object({
description: "Total number of results returned",
example: 5,
}),
-})
+});
export const ErrorResponseSchema = z.object({
details: z.string().optional().openapi({
@@ -790,15 +790,15 @@ export const ErrorResponseSchema = z.object({
description: "Error message",
example: "Invalid request parameters",
}),
-})
+});
-export type SearchResult = z.infer<typeof SearchResultSchema>
+export type SearchResult = z.infer<typeof SearchResultSchema>;
export const SettingsRequestSchema = OrganizationSettingsSchema.omit({
id: true,
orgId: true,
updatedAt: true,
-})
+});
export const ConnectionResponseSchema = z.object({
createdAt: z.string().datetime(),
@@ -808,21 +808,21 @@ export const ConnectionResponseSchema = z.object({
id: z.string(),
metadata: z.record(z.any()).optional(),
provider: z.string(),
-})
+});
-export const RequestTypeSchema = RequestTypeEnum
+export const RequestTypeSchema = RequestTypeEnum;
export const HourlyAnalyticsSchema = z.object({
count: z.number(),
hour: z.union([z.date(), z.string()]),
-})
+});
export const ApiKeyAnalyticsBaseSchema = z.object({
count: z.number(),
keyId: z.string(),
keyName: z.string().nullable(),
lastUsed: z.union([z.date(), z.string()]).nullable(),
-})
+});
export const AnalyticsUsageResponseSchema = z.object({
byKey: z.array(
@@ -845,7 +845,7 @@ export const AnalyticsUsageResponseSchema = z.object({
type: RequestTypeSchema,
}),
),
-})
+});
export const AnalyticsErrorResponseSchema = z.object({
byKey: z.array(
@@ -878,7 +878,7 @@ export const AnalyticsErrorResponseSchema = z.object({
type: RequestTypeSchema,
}),
),
-})
+});
export const AnalyticsLogSchema = z.object({
createdAt: z.date(),
@@ -917,12 +917,12 @@ export const AnalyticsLogSchema = z.object({
]),
statusCode: z.number(),
type: RequestTypeSchema,
-})
+});
export const AnalyticsLogsResponseSchema = z.object({
logs: z.array(z.unknown()),
pagination: PaginationSchema,
-})
+});
export const AnalyticsChatResponseSchema = z.object({
analytics: z.object({
@@ -1022,7 +1022,7 @@ export const AnalyticsChatResponseSchema = z.object({
}),
}),
}),
-})
+});
export const AnalyticsMemoryResponseSchema = z.object({
connectionsGrowth: z.number(),
@@ -1033,7 +1033,7 @@ export const AnalyticsMemoryResponseSchema = z.object({
tokensProcessed: z.number(),
totalConnections: z.number(),
totalMemories: z.number(),
-})
+});
export const MemoryEntryAPISchema = MemoryEntrySchema.extend({
sourceAddedAt: z.date().nullable(), // From join relationship
@@ -1042,7 +1042,7 @@ export const MemoryEntryAPISchema = MemoryEntrySchema.extend({
spaceContainerTag: z.string().nullable(), // From join relationship
}).openapi({
description: "Memory entry with source relationship data",
-})
+});
// Extended document schema with memory entries
export const DocumentWithMemoriesSchema = z
@@ -1075,7 +1075,7 @@ export const DocumentWithMemoriesSchema = z
})
.openapi({
description: "Document with associated memory entries",
- })
+ });
export const DocumentsWithMemoriesResponseSchema = z
.object({
@@ -1084,7 +1084,7 @@ export const DocumentsWithMemoriesResponseSchema = z
})
.openapi({
description: "List of documents with their memory entries",
- })
+ });
export const DocumentsWithMemoriesQuerySchema = z
.object({
@@ -1114,7 +1114,7 @@ export const DocumentsWithMemoriesQuerySchema = z
})
.openapi({
description: "Query parameters for listing documents with memory entries",
- })
+ });
export const MigrateMCPRequestSchema = z
.object({
@@ -1129,7 +1129,7 @@ export const MigrateMCPRequestSchema = z
})
.openapi({
description: "Request body for migrating MCP documents",
- })
+ });
export const MigrateMCPResponseSchema = z
.object({
@@ -1155,7 +1155,7 @@ export const MigrateMCPResponseSchema = z
})
.openapi({
description: "Response for MCP document migration",
- })
+ });
// Processing documents schema
export const ProcessingDocumentsResponseSchema = z
@@ -1196,7 +1196,7 @@ export const ProcessingDocumentsResponseSchema = z
],
totalCount: 5,
},
- })
+ });
// Project schemas
export const ProjectSchema = z
@@ -1235,7 +1235,7 @@ export const ProjectSchema = z
})
.openapi({
description: "Project object for organizing memories",
- })
+ });
export const CreateProjectSchema = z
.object({
@@ -1248,7 +1248,7 @@ export const CreateProjectSchema = z
})
.openapi({
description: "Request body for creating a new project",
- })
+ });
export const ListProjectsResponseSchema = z
.object({
@@ -1258,7 +1258,7 @@ export const ListProjectsResponseSchema = z
})
.openapi({
description: "Response containing list of projects",
- })
+ });
export const DeleteProjectSchema = z
.object({
@@ -1275,9 +1275,9 @@ export const DeleteProjectSchema = z
(data) => {
// If action is "move", targetProjectId is required
if (data.action === "move") {
- return !!data.targetProjectId
+ return !!data.targetProjectId;
}
- return true
+ return true;
},
{
message: "targetProjectId is required when action is 'move'",
@@ -1286,7 +1286,7 @@ export const DeleteProjectSchema = z
)
.openapi({
description: "Request body for deleting a project",
- })
+ });
export const DeleteProjectResponseSchema = z
.object({
@@ -1309,7 +1309,7 @@ export const DeleteProjectResponseSchema = z
})
.openapi({
description: "Response for project deletion",
- })
+ });
// Bulk delete schema - supports both IDs and container tags
export const BulkDeleteMemoriesSchema = z
@@ -1336,7 +1336,7 @@ export const BulkDeleteMemoriesSchema = z
.refine(
(data) => {
// At least one of ids or containerTags must be provided
- return !!data.ids?.length || !!data.containerTags?.length
+ return !!data.ids?.length || !!data.containerTags?.length;
},
{
message: "Either 'ids' or 'containerTags' must be provided",
@@ -1348,7 +1348,7 @@ export const BulkDeleteMemoriesSchema = z
example: {
ids: ["acxV5LHMEsG2hMSNb4umbn", "bxcV5LHMEsG2hMSNb4umbn"],
},
- })
+ });
export const BulkDeleteMemoriesResponseSchema = z
.object({
@@ -1383,4 +1383,4 @@ export const BulkDeleteMemoriesResponseSchema = z
})
.openapi({
description: "Response for bulk memory deletion",
- })
+ });
diff --git a/packages/validation/connection.ts b/packages/validation/connection.ts
index e7bc8352..e4baade6 100644
--- a/packages/validation/connection.ts
+++ b/packages/validation/connection.ts
@@ -1,13 +1,13 @@
-import { z } from "zod"
-import { ConnectionProviderEnum } from "./schemas"
+import { z } from "zod";
+import { ConnectionProviderEnum } from "./schemas";
-export const providers = ConnectionProviderEnum
-export type Provider = z.infer<typeof providers>
+export const providers = ConnectionProviderEnum;
+export type Provider = z.infer<typeof providers>;
const BaseMetadataSchema = <T extends z.ZodTypeAny>(provider: T) =>
z.object({
provider,
- })
+ });
export const NotionMetadataSchema = BaseMetadataSchema(
z.literal("notion"),
@@ -17,8 +17,8 @@ export const NotionMetadataSchema = BaseMetadataSchema(
workspaceIcon: z.string().optional(),
workspaceId: z.string(),
workspaceName: z.string(),
-})
-export type NotionMetadata = z.infer<typeof NotionMetadataSchema>
+});
+export type NotionMetadata = z.infer<typeof NotionMetadataSchema>;
export const GoogleDriveMetadataSchema = BaseMetadataSchema(
z.literal("google-drive"),
@@ -27,8 +27,8 @@ export const GoogleDriveMetadataSchema = BaseMetadataSchema(
webhookChannelId: z.string().optional(),
webhookExpiration: z.number().optional(),
webhookResourceId: z.string().optional(),
-})
-export type GoogleDriveMetadata = z.infer<typeof GoogleDriveMetadataSchema>
+});
+export type GoogleDriveMetadata = z.infer<typeof GoogleDriveMetadataSchema>;
export const OneDriveMetadataSchema = BaseMetadataSchema(
z.literal("onedrive"),
@@ -38,38 +38,38 @@ export const OneDriveMetadataSchema = BaseMetadataSchema(
webhookClientState: z.string().optional(),
webhookExpiration: z.number().optional(),
webhookSubscriptionId: z.string().optional(),
-})
-export type OneDriveMetadata = z.infer<typeof OneDriveMetadataSchema>
+});
+export type OneDriveMetadata = z.infer<typeof OneDriveMetadataSchema>;
export const ConnectionMetadataSchema = z.discriminatedUnion("provider", [
NotionMetadataSchema,
GoogleDriveMetadataSchema,
OneDriveMetadataSchema,
-])
+]);
export type ConnectionMetadata<T extends Provider> = T extends "notion"
? NotionMetadata
: T extends "google-drive"
? GoogleDriveMetadata
: T extends "onedrive"
? OneDriveMetadata
- : never
+ : never;
export function isNotionMetadata(
metadata: unknown,
): metadata is NotionMetadata {
- return NotionMetadataSchema.safeParse(metadata).success
+ return NotionMetadataSchema.safeParse(metadata).success;
}
export function isGoogleDriveMetadata(
metadata: unknown,
): metadata is GoogleDriveMetadata {
- return GoogleDriveMetadataSchema.safeParse(metadata).success
+ return GoogleDriveMetadataSchema.safeParse(metadata).success;
}
export function isOneDriveMetadata(
metadata: unknown,
): metadata is OneDriveMetadata {
- return OneDriveMetadataSchema.safeParse(metadata).success
+ return OneDriveMetadataSchema.safeParse(metadata).success;
}
export const ConnectionStateSchema = z.object({
@@ -77,8 +77,8 @@ export const ConnectionStateSchema = z.object({
org: z.string(),
provider: providers,
userId: z.string(),
-})
-export type ConnectionState = z.infer<typeof ConnectionStateSchema>
+});
+export type ConnectionState = z.infer<typeof ConnectionStateSchema>;
export const TokenDataSchema = z.object({
// Only used for Notion connections since they don't support refresh tokens
@@ -90,8 +90,8 @@ export const TokenDataSchema = z.object({
metadata: ConnectionMetadataSchema.optional(),
refreshToken: z.string().optional(),
userId: z.string().optional(),
-})
-export type TokenData = z.infer<typeof TokenDataSchema>
+});
+export type TokenData = z.infer<typeof TokenDataSchema>;
export const NotionTokenResponseSchema = z.object({
access_token: z.string(),
@@ -123,8 +123,8 @@ export const NotionTokenResponseSchema = z.object({
workspace_icon: z.string().optional(),
workspace_id: z.string(),
workspace_name: z.string(),
-})
-export type NotionTokenResponse = z.infer<typeof NotionTokenResponseSchema>
+});
+export type NotionTokenResponse = z.infer<typeof NotionTokenResponseSchema>;
export const GoogleDriveTokenResponseSchema = z.object({
access_token: z.string(),
@@ -132,10 +132,10 @@ export const GoogleDriveTokenResponseSchema = z.object({
refresh_token: z.string().optional(),
scope: z.string(),
token_type: z.literal("Bearer"),
-})
+});
export type GoogleDriveTokenResponse = z.infer<
typeof GoogleDriveTokenResponseSchema
->
+>;
export const OneDriveTokenResponseSchema = z.object({
access_token: z.string(),
@@ -143,8 +143,8 @@ export const OneDriveTokenResponseSchema = z.object({
refresh_token: z.string().optional(),
scope: z.string(),
token_type: z.literal("Bearer"),
-})
-export type OneDriveTokenResponse = z.infer<typeof OneDriveTokenResponseSchema>
+});
+export type OneDriveTokenResponse = z.infer<typeof OneDriveTokenResponseSchema>;
export const NotionConfigSchema = z.object({
clientId: z.string(),
@@ -154,23 +154,23 @@ export const NotionConfigSchema = z.object({
token: z.string().url(),
}),
scopes: z.array(z.string()),
-})
-export type NotionConfig = z.infer<typeof NotionConfigSchema>
+});
+export type NotionConfig = z.infer<typeof NotionConfigSchema>;
export const ConnectionQuerySchema = z.object({
id: z.string(),
redirectUrl: z.string().optional(),
-})
+});
export const GoogleDrivePageTokenResponseSchema = z.object({
startPageToken: z.union([z.string(), z.number()]),
-})
+});
export const GoogleDriveWatchResponseSchema = z.object({
expiration: z.string(),
id: z.string(),
resourceId: z.string(),
-})
+});
export const OneDriveSubscriptionResponseSchema = z.object({
changeType: z.string(),
@@ -179,13 +179,13 @@ export const OneDriveSubscriptionResponseSchema = z.object({
id: z.string(),
notificationUrl: z.string(),
resource: z.string(),
-})
+});
export const GoogleUserInfoResponseSchema = z.object({
email: z.string().email(),
-})
+});
export const MicrosoftUserInfoResponseSchema = z.object({
mail: z.string().optional(),
userPrincipalName: z.string().optional(),
-})
+});