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(
Google Logo
, 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 ( - - - {/* `NextError` is the default Next.js error page component. Its type + return ( + + + {/* `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. */} - - - - ); -} \ No newline at end of file + + + + ); +} 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 ( -type DocumentWithMemories = DocumentsResponse["documents"][0] +type DocumentsResponse = z.infer; +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([]) - 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([]); + 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() - 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(); + 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 => { 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() + if (!isOpen) return; + if (!allHighlightDocumentIds || allHighlightDocumentIds.length === 0) + return; + const present = new Set(); 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([ ...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 (
@@ -526,9 +528,9 @@ const MemoryGraphPage = () => { -
- - - - + )} @@ -735,35 +707,35 @@ const MemoryGraphPage = () => { onClose={() => setShowReferralModal(false)} /> - ) -} + ); +}; // 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() {

Loading...

- ) + ); } // If we have a user and they have access, show the main component @@ -783,5 +755,5 @@ export default function Page() { - ) + ); } 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() {

-
Learn more about supermemory @@ -178,10 +175,10 @@ export default function ReferralPage() {

@@ -103,12 +103,12 @@ export function CreateProjectDialog({ whileTap={{ scale: 0.95 }} > @@ -107,19 +107,19 @@ export function InstallPrompt() { 2. Select "Add to Home Screen" ➕

) : ( - ); + return ( + + ); }); 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; - }) => ( -
-
-
- {getDocumentIcon(document.type, 'w-5 h-5')} -
-
- - {document.title || 'Untitled Document'} - -
- {formatDocumentType(document.type)} - - {formatDate(document.createdAt)} - {document.url && ( - <> - - - - )} -
-
-
-
- ); + const HeaderContent = ({ + TitleComponent, + }: { + TitleComponent: typeof SheetTitle | typeof DrawerTitle; + }) => ( +
+
+
+ {getDocumentIcon(document.type, "w-5 h-5")} +
+
+ + {document.title || "Untitled Document"} + +
+ {formatDocumentType(document.type)} + + {formatDate(document.createdAt)} + {document.url && ( + <> + + + + )} +
+
+
+
+ ); - 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; + const ContentAndSummarySection = () => { + const hasContent = document.content && document.content.trim().length > 0; + const hasSummary = document.summary && document.summary.trim().length > 0; - const defaultTab = hasContent ? 'content' : 'summary'; + if (!hasContent && !hasSummary) return null; - return ( -
- - - {hasContent && ( - - - Original Content - - )} - {hasSummary && ( - - - Summary - - )} - + const defaultTab = hasContent ? "content" : "summary"; - {hasContent && ( - -
-

- {document.content} -

-
-
- )} + return ( +
+ + + {hasContent && ( + + + Original Content + + )} + {hasSummary && ( + + + Summary + + )} + - {hasSummary && ( - -
-

- {document.summary} -

-
-
- )} -
-
- ); - }; + {hasContent && ( + +
+

+ {document.content} +

+
+
+ )} - const MemoryContent = () => ( -
- {activeMemories.length > 0 && ( -
-
- Active Memories ({activeMemories.length}) -
-
- {activeMemories.map((memory) => ( -
- -
- ))} -
-
- )} + {hasSummary && ( + +
+

+ {document.summary} +

+
+
+ )} + +
+ ); + }; - {forgottenMemories.length > 0 && ( -
-
- Forgotten Memories ({forgottenMemories.length}) -
-
- {forgottenMemories.map((memory) => ( - - ))} -
-
- )} + const MemoryContent = () => ( +
+ {activeMemories.length > 0 && ( +
+
+ Active Memories ({activeMemories.length}) +
+
+ {activeMemories.map((memory) => ( +
+ +
+ ))} +
+
+ )} - {activeMemories.length === 0 && forgottenMemories.length === 0 && ( -
- -

- No memories found for this document -

-
- )} -
- ); + {forgottenMemories.length > 0 && ( +
+
+ Forgotten Memories ({forgottenMemories.length}) +
+
+ {forgottenMemories.map((memory) => ( + + ))} +
+
+ )} - if (isMobile) { - return ( - - - {/* Header section with glass effect */} -
- - - + {activeMemories.length === 0 && forgottenMemories.length === 0 && ( +
+ +

+ No memories found for this document +

+
+ )} +
+ ); - -
+ if (isMobile) { + return ( + + + {/* Header section with glass effect */} +
+ + + -
- -
- - - ); - } + +
- return ( - - -
- - - +
+ +
+ + + ); + } - -
+ return ( + + +
+ + + -
- -
- - - ); - } + +
+ +
+ +
+
+
+ ); + }, ); 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 -type DocumentWithMemories = DocumentsResponse["documents"][0] +type DocumentsResponse = z.infer; +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 + children?: React.ReactNode; + documents: DocumentWithMemories[]; + isLoading: boolean; + isLoadingMore: boolean; + error: Error | null; + totalLoaded: number; + hasMore: boolean; + loadMoreDocuments: () => Promise; } 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 (
@@ -66,8 +65,8 @@ const GreetingMessage = memo(() => {

- ) -}) + ); +}); 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 ( { - analytics.documentCardClicked() - onOpenDetails(document) + analytics.documentCardClicked(); + onOpenDetails(document); }} style={{ backgroundColor: colors.document.primary, @@ -112,9 +113,9 @@ const DocumentCard = memo( - ) + ); } -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 && ( <> setIsOpen(false)} />
@@ -187,14 +187,14 @@ export function ProjectSelector() { .filter((p: Project) => p.containerTag !== DEFAULT_PROJECT_ID) .map((project: Project, index: number) => (
@@ -297,8 +297,8 @@ export function ProjectSelector() { {/* 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 ( - + + {/* Tabs */} - + - + Refer Friends {!isPro && ( Upgrade Plan @@ -194,7 +194,7 @@ export function ReferralUpgradeModal({ )} - +
@@ -241,7 +241,7 @@ export function ReferralUpgradeModal({ {!isPro && ( - +
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 ( {children} 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 ( -
- + return ( +
+ - - - -
- ); + + + +
+ ); } 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([]); + const queryClient = useQueryClient() + const { selectedProject, setSelectedProject } = useProject() + const [showAddDialog, setShowAddDialog] = useState(true) + const [selectedFiles, setSelectedFiles] = useState([]) 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>( + `@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 ( @@ -591,8 +602,8 @@ export function AddMemoryView({ { - setShowAddDialog(open); - if (!open) onClose?.(); + setShowAddDialog(open) + if (!open) onClose?.() }} open={showAddDialog} > @@ -651,9 +662,9 @@ export function AddMemoryView({
{ - e.preventDefault(); - e.stopPropagation(); - addContentForm.handleSubmit(); + e.preventDefault() + e.stopPropagation() + addContentForm.handleSubmit() }} >
@@ -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({
{ - e.preventDefault(); - e.stopPropagation(); - addContentForm.handleSubmit(); + e.preventDefault() + e.stopPropagation() + addContentForm.handleSubmit() }} >
@@ -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({
{ - e.preventDefault(); - e.stopPropagation(); - fileUploadForm.handleSubmit(); + e.preventDefault() + e.stopPropagation() + fileUploadForm.handleSubmit() }} >
@@ -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({
)}
- ); + ) } 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 (
+ Memory Usage {/* Background circle */} {/* Progress circle */} @@ -49,5 +50,5 @@ export function MemoryUsageRing({ {memoriesUsed} / {memoriesLimit}
- ); + ) } 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 ( - - ); + return ( + + ); } 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 ( - - ); + return ( + + ); } 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; }) => ( @@ -77,19 +77,19 @@ export function ChatRewrite() { const isActive = c.id === currentChatId; return (
{ - 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} >
@@ -107,14 +107,14 @@ export function ChatRewrite() {
@@ -129,22 +129,22 @@ export function ChatRewrite() {
- 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 +type Connection = z.infer; // 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 + const Icon = connector.icon; + return ; } - return 📎 - } + return 📎; + }; return (
@@ -310,7 +312,7 @@ export function ConnectionsTabContent() {
{Object.entries(CONNECTORS).map(([provider, config], index) => { - const Icon = config.icon + const Icon = config.icon; return ( { - addConnectionMutation.mutate(provider as ConnectorProvider) + addConnectionMutation.mutate(provider as ConnectorProvider); }} variant="outline" > @@ -337,10 +339,10 @@ export function ConnectionsTabContent() {
- ) + ); })}
- ) + ); } 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" +} 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 +type Connection = z.infer; 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 }) => ( Google Chrome Icon -) +); export function IntegrationsView() { - const { org } = useAuth() - const queryClient = useQueryClient() - const { selectedProject } = useProject() - const autumn = useCustomer() - const [showApiKeyModal, setShowApiKeyModal] = useState(false) - const [apiKey, setApiKey] = useState("") - 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(""); + 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 (
@@ -298,32 +300,32 @@ export function IntegrationsView() {
- ) + ); })} )} @@ -587,10 +589,10 @@ export function IntegrationsView() { More integrations are coming soon! Have a suggestion? Share it with us on{" "} X @@ -599,7 +601,7 @@ export function IntegrationsView() { {/* API Key Modal */} - + @@ -618,24 +620,24 @@ export function IntegrationsView() { {/* API Key Section */}
@@ -700,5 +702,5 @@ export function IntegrationsView() {
- ) + ); } 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() - const [isInstallDialogOpen, setIsInstallDialogOpen] = useState(false) + const [isMigrateDialogOpen, setIsMigrateDialogOpen] = useState(false); + const projectId = localStorage.getItem("selectedProject") ?? "default"; + const { org } = useAuth(); + const [apiKey, setApiKey] = useState(); + 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 (
@@ -155,9 +155,9 @@ export function MCPView() {
- ) + ); } 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() { Install the supermemory MCP Server - 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: @@ -91,7 +91,7 @@ export function InstallationDialogContent() { onValueChange={(value) => setClient(value as keyof typeof clients)} value={client} > - + @@ -107,27 +107,30 @@ export function InstallationDialogContent() {
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() {
- ) + ); } return ( @@ -337,5 +337,5 @@ export function ProfileView() { Sign Out - ) + ); } 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 ; - case 'google_sheet': - return ; - case 'google_slide': - return ; - case 'google_drive': - return ; - case 'notion': - case 'notion_doc': - return ; - case 'word': - case 'microsoft_word': - return ; - case 'excel': - case 'microsoft_excel': - return ; - case 'powerpoint': - case 'microsoft_powerpoint': - return ; - case 'onenote': - case 'microsoft_onenote': - return ; - case 'onedrive': - return ; - case 'pdf': - return ; - default: - return ; - } + switch (type) { + case "google_doc": + return ; + case "google_sheet": + return ; + case "google_slide": + return ; + case "google_drive": + return ; + case "notion": + case "notion_doc": + return ; + case "word": + case "microsoft_word": + return ; + case "excel": + case "microsoft_excel": + return ; + case "powerpoint": + case "microsoft_powerpoint": + return ; + case "onenote": + case "microsoft_onenote": + return ; + case "onedrive": + return ; + case "pdf": + return ; + default: + return ; + } }; 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": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], + "iconsax-reactjs": ["iconsax-reactjs@0.0.8", "", { "peerDependencies": { "react": "*" } }, "sha512-cb+uTMxbkSFNbu8ZclX7BWQVfOWQt8+m/PsDjnsm/H+mcYrnfTYMjHxiof1FB43k7UAgt1ds+0oFeMVKdqyslw=="], + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], "ieee754": ["ieee754@1.2.1", "", {}, "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 = {} + const metadata: Record = {}; 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(undefined) + const [isMobile, setIsMobile] = React.useState( + 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["data"]> +type Organization = typeof authClient.$Infer.ActiveOrganization; +type SessionData = NonNullable["data"]>; interface AuthContextType { - session: SessionData["session"] | null - user: SessionData["user"] | null - org: Organization | null - setActiveOrg: (orgSlug: string) => Promise + session: SessionData["session"] | null; + user: SessionData["user"] | null; + org: Organization | null; + setActiveOrg: (orgSlug: string) => Promise; } -const AuthContext = createContext(undefined) +const AuthContext = createContext(undefined); export function AuthProvider({ children }: { children: ReactNode }) { - const { data: session } = useSession() - const [org, setOrg] = useState(null) + const { data: session } = useSession(); + const [org, setOrg] = useState(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 ( {children} - ) + ); } 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]) + window.removeEventListener("error", handleError); + window.removeEventListener( + "unhandledrejection", + handleUnhandledRejection, + ); + }; + }, [trackError]); - return <>{children} + 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) => { 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 = {} - private effects: Map = new Map() - private animationFrame: number | null = null - private startTime: number = performance.now() - private mousePositions: Map = 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 = {}; + private effects: Map = new Map(); + private animationFrame: number | null = null; + private startTime: number = performance.now(); + private mousePositions: Map = 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 }) { {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 -type DocumentWithMemories = DocumentsResponse["documents"][0] +type DocumentsResponse = z.infer; +type DocumentWithMemories = DocumentsResponse["documents"][0]; export const fetchSubscriptionStatus = ( autumn: ReturnType, @@ -18,54 +18,54 @@ export const fetchSubscriptionStatus = ( "memory_starter", "memory_growth", "consumer_pro", - ] - const statusMap: Record = {} + ]; + const statusMap: Record = {}; 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) => 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, ) => 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) => 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 ( {children} - ) -} + ); +}; 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 = {} + const metadata: Record = {}; 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>["results"] - count?: number - error?: string + success: boolean; + results?: Awaited>["results"]; + count?: number; + error?: string; } export interface MemoryAddResult { - success: boolean - memory?: Awaited> - error?: string + success: boolean; + memory?: Awaited>; + 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 { 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 { try { - const metadata: Record = {} + const metadata: Record = {}; 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 { - 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 { 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(); 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(); 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 = {}; 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(); const counts: Record = {}; - 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 && ( 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(({ - onCenter, - onZoomIn, - onZoomOut, - onAutoFit, - nodes, - className = "", -}) => { - if (nodes.length === 0) { - return null - } +export const NavigationControls = memo( + ({ onCenter, onZoomIn, onZoomOut, onAutoFit, nodes, className = "" }) => { + if (nodes.length === 0) { + return null; + } - return ( -
- - -
+ return ( +
+
+ + +
-
- ) -}) + ); + }, +); -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 = { category: "technology", @@ -24,7 +24,7 @@ const exampleMetadata: Record = { 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 +export type SearchResult = z.infer; 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 +export const providers = ConnectionProviderEnum; +export type Provider = z.infer; const BaseMetadataSchema = (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 +}); +export type NotionMetadata = z.infer; 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 +}); +export type GoogleDriveMetadata = z.infer; 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 +}); +export type OneDriveMetadata = z.infer; export const ConnectionMetadataSchema = z.discriminatedUnion("provider", [ NotionMetadataSchema, GoogleDriveMetadataSchema, OneDriveMetadataSchema, -]) +]); export type ConnectionMetadata = 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 +}); +export type ConnectionState = z.infer; 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 +}); +export type TokenData = z.infer; 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 +}); +export type NotionTokenResponse = z.infer; 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 +}); +export type OneDriveTokenResponse = z.infer; 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 +}); +export type NotionConfig = z.infer; 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(), -}) +});