mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-10 22:41:17 +00:00
formatting
This commit is contained in:
parent
ea17bc4451
commit
1fcb569089
71 changed files with 2657 additions and 2831 deletions
|
|
@ -5,9 +5,9 @@ export async function GET() {
|
|||
return new ImageResponse(
|
||||
<div tw="w-full h-full flex flex-col justify-center items-center">
|
||||
<img
|
||||
src="https://pub-1be2b1df2c7e456f8e21149e972f4caf.r2.dev/bust.png"
|
||||
alt="Google Logo"
|
||||
height={367}
|
||||
src="https://pub-1be2b1df2c7e456f8e21149e972f4caf.r2.dev/bust.png"
|
||||
width={369}
|
||||
/>
|
||||
</div>,
|
||||
|
|
|
|||
|
|
@ -4,20 +4,24 @@ import * as Sentry from "@sentry/nextjs";
|
|||
import NextError from "next/error";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function GlobalError({ error }: { error: Error & { digest?: string } }) {
|
||||
useEffect(() => {
|
||||
Sentry.captureException(error);
|
||||
}, [error]);
|
||||
export default function GlobalError({
|
||||
error,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
}) {
|
||||
useEffect(() => {
|
||||
Sentry.captureException(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
{/* `NextError` is the default Next.js error page component. Its type
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
{/* `NextError` is the default Next.js error page component. Its type
|
||||
definition requires a `statusCode` prop. However, since the App Router
|
||||
does not expose status codes for errors, we simply pass 0 to render a
|
||||
generic error message. */}
|
||||
<NextError statusCode={0} />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
<NextError statusCode={0} />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Metadata } from "next";
|
||||
import { Inter, JetBrains_Mono } from "next/font/google";
|
||||
import { Inter, JetBrains_Mono, Space_Grotesk } from "next/font/google";
|
||||
import "../globals.css";
|
||||
import "@ui/globals.css";
|
||||
import { AuthProvider } from "@lib/auth-context";
|
||||
|
|
@ -24,6 +24,11 @@ const mono = JetBrains_Mono({
|
|||
variable: "--font-mono",
|
||||
});
|
||||
|
||||
const spaceGrotesk = Space_Grotesk({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-space-grotesk",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://app.supermemory.ai"),
|
||||
description: "Your memories, wherever you are",
|
||||
|
|
@ -38,7 +43,7 @@ export default function RootLayout({
|
|||
return (
|
||||
<html className="dark bg-sm-black" lang="en">
|
||||
<body
|
||||
className={`${sans.variable} ${mono.variable} antialiased bg-[#0f1419]`}
|
||||
className={`${sans.variable} ${mono.variable} ${spaceGrotesk.variable} antialiased bg-[#0f1419]`}
|
||||
>
|
||||
<AutumnProvider
|
||||
backendUrl={
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useIsMobile } from "@hooks/use-mobile"
|
||||
import { useAuth } from "@lib/auth-context"
|
||||
import { $fetch } from "@repo/lib/api"
|
||||
import { MemoryGraph } from "@repo/ui/memory-graph"
|
||||
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
|
||||
import { useInfiniteQuery, useQuery } from "@tanstack/react-query"
|
||||
import { Logo, LogoFull } from "@ui/assets/Logo"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { GlassMenuEffect } from "@ui/other/glass-effect"
|
||||
import { useIsMobile } from "@hooks/use-mobile";
|
||||
import { useAuth } from "@lib/auth-context";
|
||||
import { $fetch } from "@repo/lib/api";
|
||||
import { MemoryGraph } from "@repo/ui/memory-graph";
|
||||
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api";
|
||||
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
||||
import { Logo, LogoFull } from "@ui/assets/Logo";
|
||||
import { Button } from "@ui/components/button";
|
||||
import { GlassMenuEffect } from "@ui/other/glass-effect";
|
||||
import {
|
||||
HelpCircle,
|
||||
LayoutGrid,
|
||||
|
|
@ -16,58 +16,59 @@ import {
|
|||
LoaderIcon,
|
||||
MessageSquare,
|
||||
Unplug,
|
||||
} from "lucide-react"
|
||||
import { AnimatePresence, motion } from "motion/react"
|
||||
import Link from "next/link"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import type { z } from "zod"
|
||||
import { ConnectAIModal } from "@/components/connect-ai-modal"
|
||||
import { InstallPrompt } from "@/components/install-prompt"
|
||||
import { MemoryListView } from "@/components/memory-list-view"
|
||||
import Menu from "@/components/menu"
|
||||
import { ProjectSelector } from "@/components/project-selector"
|
||||
import { ReferralUpgradeModal } from "@/components/referral-upgrade-modal"
|
||||
import type { TourStep } from "@/components/tour"
|
||||
import { TourAlertDialog, useTour } from "@/components/tour"
|
||||
import { AddMemoryView } from "@/components/views/add-memory"
|
||||
import { ChatRewrite } from "@/components/views/chat"
|
||||
import { TOUR_STEP_IDS, TOUR_STORAGE_KEY } from "@/lib/tour-constants"
|
||||
import { useViewMode } from "@/lib/view-mode-context"
|
||||
import { useChatOpen, useProject } from "@/stores"
|
||||
import { useGraphHighlights } from "@/stores/highlights"
|
||||
} from "lucide-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import type { z } from "zod";
|
||||
import { ConnectAIModal } from "@/components/connect-ai-modal";
|
||||
import { GetStarted } from "@/components/get-started";
|
||||
import { InstallPrompt } from "@/components/install-prompt";
|
||||
import { MemoryListView } from "@/components/memory-list-view";
|
||||
import Menu from "@/components/menu";
|
||||
import { ProjectSelector } from "@/components/project-selector";
|
||||
import { ReferralUpgradeModal } from "@/components/referral-upgrade-modal";
|
||||
import type { TourStep } from "@/components/tour";
|
||||
import { TourAlertDialog, useTour } from "@/components/tour";
|
||||
import { AddMemoryView } from "@/components/views/add-memory";
|
||||
import { ChatRewrite } from "@/components/views/chat";
|
||||
import { TOUR_STEP_IDS, TOUR_STORAGE_KEY } from "@/lib/tour-constants";
|
||||
import { useViewMode } from "@/lib/view-mode-context";
|
||||
import { useChatOpen, useProject } from "@/stores";
|
||||
import { useGraphHighlights } from "@/stores/highlights";
|
||||
|
||||
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
|
||||
type DocumentWithMemories = DocumentsResponse["documents"][0]
|
||||
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>;
|
||||
type DocumentWithMemories = DocumentsResponse["documents"][0];
|
||||
|
||||
const MemoryGraphPage = () => {
|
||||
const { documentIds: allHighlightDocumentIds } = useGraphHighlights()
|
||||
const isMobile = useIsMobile()
|
||||
const { viewMode, setViewMode, isInitialized } = useViewMode()
|
||||
const { selectedProject } = useProject()
|
||||
const { setSteps, isTourCompleted } = useTour()
|
||||
const { isOpen, setIsOpen } = useChatOpen()
|
||||
const [injectedDocs, setInjectedDocs] = useState<DocumentWithMemories[]>([])
|
||||
const [showAddMemoryView, setShowAddMemoryView] = useState(false)
|
||||
const [showReferralModal, setShowReferralModal] = useState(false)
|
||||
const [showConnectAIModal, setShowConnectAIModal] = useState(false)
|
||||
const [isHelpHovered, setIsHelpHovered] = useState(false)
|
||||
const { documentIds: allHighlightDocumentIds } = useGraphHighlights();
|
||||
const isMobile = useIsMobile();
|
||||
const { viewMode, setViewMode, isInitialized } = useViewMode();
|
||||
const { selectedProject } = useProject();
|
||||
const { setSteps, isTourCompleted } = useTour();
|
||||
const { isOpen, setIsOpen } = useChatOpen();
|
||||
const [injectedDocs, setInjectedDocs] = useState<DocumentWithMemories[]>([]);
|
||||
const [showAddMemoryView, setShowAddMemoryView] = useState(false);
|
||||
const [showReferralModal, setShowReferralModal] = useState(false);
|
||||
const [showConnectAIModal, setShowConnectAIModal] = useState(false);
|
||||
const [isHelpHovered, setIsHelpHovered] = useState(false);
|
||||
|
||||
// Fetch projects meta to detect experimental flag
|
||||
const { data: projectsMeta = [] } = useQuery({
|
||||
queryKey: ["projects"],
|
||||
queryFn: async () => {
|
||||
const response = await $fetch("@get/projects")
|
||||
return response.data?.projects ?? []
|
||||
const response = await $fetch("@get/projects");
|
||||
return response.data?.projects ?? [];
|
||||
},
|
||||
staleTime: 5 * 60 * 1000,
|
||||
})
|
||||
});
|
||||
|
||||
const isCurrentProjectExperimental = !!projectsMeta.find(
|
||||
(p: any) => p.containerTag === selectedProject,
|
||||
)?.isExperimental
|
||||
)?.isExperimental;
|
||||
|
||||
// Tour state
|
||||
const [showTourDialog, setShowTourDialog] = useState(false)
|
||||
const [showTourDialog, setShowTourDialog] = useState(false);
|
||||
|
||||
// Define tour steps with useMemo to prevent recreation
|
||||
const tourSteps: TourStep[] = useMemo(() => {
|
||||
|
|
@ -200,37 +201,37 @@ const MemoryGraphPage = () => {
|
|||
selectorId: TOUR_STEP_IDS.FLOATING_CHAT,
|
||||
position: "left",
|
||||
},
|
||||
]
|
||||
}, [])
|
||||
];
|
||||
}, []);
|
||||
|
||||
// Check if tour has been completed before
|
||||
useEffect(() => {
|
||||
const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === "true"
|
||||
const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === "true";
|
||||
if (!hasCompletedTour && !isTourCompleted) {
|
||||
const timer = setTimeout(() => {
|
||||
setShowTourDialog(true)
|
||||
setShowConnectAIModal(false)
|
||||
}, 1000) // Show after 1 second
|
||||
return () => clearTimeout(timer)
|
||||
setShowTourDialog(true);
|
||||
setShowConnectAIModal(false);
|
||||
}, 1000); // Show after 1 second
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isTourCompleted])
|
||||
}, [isTourCompleted]);
|
||||
|
||||
// Set up tour steps
|
||||
useEffect(() => {
|
||||
setSteps(tourSteps)
|
||||
}, [setSteps, tourSteps])
|
||||
setSteps(tourSteps);
|
||||
}, [setSteps, tourSteps]);
|
||||
|
||||
// Save tour completion to localStorage
|
||||
useEffect(() => {
|
||||
if (isTourCompleted) {
|
||||
localStorage.setItem(TOUR_STORAGE_KEY, "true")
|
||||
localStorage.setItem(TOUR_STORAGE_KEY, "true");
|
||||
}
|
||||
}, [isTourCompleted])
|
||||
}, [isTourCompleted]);
|
||||
|
||||
// Progressive loading via useInfiniteQuery
|
||||
const IS_DEV = process.env.NODE_ENV === "development"
|
||||
const PAGE_SIZE = IS_DEV ? 100 : 100
|
||||
const MAX_TOTAL = 1000
|
||||
const IS_DEV = process.env.NODE_ENV === "development";
|
||||
const PAGE_SIZE = IS_DEV ? 100 : 100;
|
||||
const MAX_TOTAL = 1000;
|
||||
|
||||
const {
|
||||
data,
|
||||
|
|
@ -252,75 +253,76 @@ const MemoryGraphPage = () => {
|
|||
containerTags: selectedProject ? [selectedProject] : undefined,
|
||||
},
|
||||
disableValidation: true,
|
||||
})
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error?.message || "Failed to fetch documents")
|
||||
throw new Error(response.error?.message || "Failed to fetch documents");
|
||||
}
|
||||
|
||||
return response.data
|
||||
return response.data;
|
||||
},
|
||||
getNextPageParam: (lastPage, allPages) => {
|
||||
const loaded = allPages.reduce(
|
||||
(acc, p) => acc + (p.documents?.length ?? 0),
|
||||
0,
|
||||
)
|
||||
if (loaded >= MAX_TOTAL) return undefined
|
||||
);
|
||||
if (loaded >= MAX_TOTAL) return undefined;
|
||||
|
||||
const { currentPage, totalPages } = lastPage.pagination
|
||||
const { currentPage, totalPages } = lastPage.pagination;
|
||||
if (currentPage < totalPages) {
|
||||
return currentPage + 1
|
||||
return currentPage + 1;
|
||||
}
|
||||
return undefined
|
||||
return undefined;
|
||||
},
|
||||
staleTime: 5 * 60 * 1000,
|
||||
})
|
||||
});
|
||||
|
||||
const baseDocuments = useMemo(() => {
|
||||
return (
|
||||
data?.pages.flatMap((p: DocumentsResponse) => p.documents ?? []) ?? []
|
||||
)
|
||||
}, [data])
|
||||
);
|
||||
}, [data]);
|
||||
|
||||
const allDocuments = useMemo(() => {
|
||||
if (injectedDocs.length === 0) return baseDocuments
|
||||
const byId = new Map<string, DocumentWithMemories>()
|
||||
for (const d of injectedDocs) byId.set(d.id, d)
|
||||
for (const d of baseDocuments) if (!byId.has(d.id)) byId.set(d.id, d)
|
||||
return Array.from(byId.values())
|
||||
}, [baseDocuments, injectedDocs])
|
||||
if (injectedDocs.length === 0) return baseDocuments;
|
||||
const byId = new Map<string, DocumentWithMemories>();
|
||||
for (const d of injectedDocs) byId.set(d.id, d);
|
||||
for (const d of baseDocuments) if (!byId.has(d.id)) byId.set(d.id, d);
|
||||
return Array.from(byId.values());
|
||||
}, [baseDocuments, injectedDocs]);
|
||||
|
||||
const totalLoaded = allDocuments.length
|
||||
const hasMore = hasNextPage
|
||||
const isLoadingMore = isFetchingNextPage
|
||||
const totalLoaded = allDocuments.length;
|
||||
const hasMore = hasNextPage;
|
||||
const isLoadingMore = isFetchingNextPage;
|
||||
|
||||
const loadMoreDocuments = useCallback(async (): Promise<void> => {
|
||||
if (hasNextPage && !isFetchingNextPage) {
|
||||
await fetchNextPage()
|
||||
return
|
||||
await fetchNextPage();
|
||||
return;
|
||||
}
|
||||
return
|
||||
}, [hasNextPage, isFetchingNextPage, fetchNextPage])
|
||||
return;
|
||||
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
|
||||
|
||||
// Reset injected docs when project changes
|
||||
useEffect(() => {
|
||||
setInjectedDocs([])
|
||||
}, [selectedProject])
|
||||
setInjectedDocs([]);
|
||||
}, [selectedProject]);
|
||||
|
||||
// Surgical fetch of missing highlighted documents (customId-based IDs from search)
|
||||
useEffect(() => {
|
||||
if (!isOpen) return
|
||||
if (!allHighlightDocumentIds || allHighlightDocumentIds.length === 0) return
|
||||
const present = new Set<string>()
|
||||
if (!isOpen) return;
|
||||
if (!allHighlightDocumentIds || allHighlightDocumentIds.length === 0)
|
||||
return;
|
||||
const present = new Set<string>();
|
||||
for (const d of [...baseDocuments, ...injectedDocs]) {
|
||||
if (d.id) present.add(d.id)
|
||||
if ((d as any).customId) present.add((d as any).customId as string)
|
||||
if (d.id) present.add(d.id);
|
||||
if ((d as any).customId) present.add((d as any).customId as string);
|
||||
}
|
||||
const missing = allHighlightDocumentIds.filter(
|
||||
(id: string) => !present.has(id),
|
||||
)
|
||||
if (missing.length === 0) return
|
||||
let cancelled = false
|
||||
);
|
||||
if (missing.length === 0) return;
|
||||
let cancelled = false;
|
||||
const run = async () => {
|
||||
try {
|
||||
const resp = await $fetch("@post/memories/documents/by-ids", {
|
||||
|
|
@ -330,32 +332,32 @@ const MemoryGraphPage = () => {
|
|||
containerTags: selectedProject ? [selectedProject] : undefined,
|
||||
},
|
||||
disableValidation: true,
|
||||
})
|
||||
if (cancelled || (resp as any)?.error) return
|
||||
});
|
||||
if (cancelled || (resp as any)?.error) return;
|
||||
const extraDocs = (resp as any)?.data?.documents as
|
||||
| DocumentWithMemories[]
|
||||
| undefined
|
||||
if (!extraDocs || extraDocs.length === 0) return
|
||||
| undefined;
|
||||
if (!extraDocs || extraDocs.length === 0) return;
|
||||
setInjectedDocs((prev) => {
|
||||
const seen = new Set<string>([
|
||||
...prev.map((d) => d.id),
|
||||
...baseDocuments.map((d) => d.id),
|
||||
])
|
||||
const merged = [...prev]
|
||||
]);
|
||||
const merged = [...prev];
|
||||
for (const doc of extraDocs) {
|
||||
if (!seen.has(doc.id)) {
|
||||
merged.push(doc)
|
||||
seen.add(doc.id)
|
||||
merged.push(doc);
|
||||
seen.add(doc.id);
|
||||
}
|
||||
}
|
||||
return merged
|
||||
})
|
||||
return merged;
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
void run()
|
||||
};
|
||||
void run();
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
cancelled = true;
|
||||
};
|
||||
}, [
|
||||
isOpen,
|
||||
allHighlightDocumentIds.join("|"),
|
||||
|
|
@ -363,39 +365,39 @@ const MemoryGraphPage = () => {
|
|||
injectedDocs,
|
||||
selectedProject,
|
||||
$fetch,
|
||||
])
|
||||
]);
|
||||
|
||||
// Handle view mode change
|
||||
const handleViewModeChange = useCallback(
|
||||
(mode: "graph" | "list") => {
|
||||
setViewMode(mode)
|
||||
setViewMode(mode);
|
||||
},
|
||||
[setViewMode],
|
||||
)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === "true"
|
||||
const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === "true";
|
||||
if (hasCompletedTour && allDocuments.length === 0 && !showTourDialog) {
|
||||
setShowConnectAIModal(true)
|
||||
setShowConnectAIModal(true);
|
||||
} else if (showTourDialog) {
|
||||
setShowConnectAIModal(false)
|
||||
setShowConnectAIModal(false);
|
||||
}
|
||||
}, [allDocuments.length, showTourDialog])
|
||||
}, [allDocuments.length, showTourDialog]);
|
||||
|
||||
// Prevent body scrolling
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = "hidden"
|
||||
document.body.style.height = "100vh"
|
||||
document.documentElement.style.overflow = "hidden"
|
||||
document.documentElement.style.height = "100vh"
|
||||
document.body.style.overflow = "hidden";
|
||||
document.body.style.height = "100vh";
|
||||
document.documentElement.style.overflow = "hidden";
|
||||
document.documentElement.style.height = "100vh";
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = ""
|
||||
document.body.style.height = ""
|
||||
document.documentElement.style.overflow = ""
|
||||
document.documentElement.style.height = ""
|
||||
}
|
||||
}, [])
|
||||
document.body.style.overflow = "";
|
||||
document.body.style.height = "";
|
||||
document.documentElement.style.overflow = "";
|
||||
document.documentElement.style.height = "";
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative h-screen bg-[#0f1419] overflow-hidden touch-none">
|
||||
|
|
@ -526,9 +528,9 @@ const MemoryGraphPage = () => {
|
|||
<button
|
||||
className="text-sm text-blue-400 hover:text-blue-300 transition-colors underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setShowAddMemoryView(true)
|
||||
setShowConnectAIModal(false)
|
||||
e.stopPropagation();
|
||||
setShowAddMemoryView(true);
|
||||
setShowConnectAIModal(false);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
|
|
@ -564,37 +566,7 @@ const MemoryGraphPage = () => {
|
|||
loadMoreDocuments={loadMoreDocuments}
|
||||
totalLoaded={totalLoaded}
|
||||
>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<ConnectAIModal
|
||||
onOpenChange={setShowConnectAIModal}
|
||||
open={showConnectAIModal}
|
||||
>
|
||||
<div className="rounded-xl overflow-hidden cursor-pointer hover:bg-white/5 transition-colors p-6">
|
||||
<div className="relative z-10 text-slate-200 text-center">
|
||||
<p className="text-lg font-medium mb-4">
|
||||
Get Started with supermemory
|
||||
</p>
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="text-sm text-blue-400 hover:text-blue-300 transition-colors">
|
||||
Click here to set up your AI connection
|
||||
</p>
|
||||
<p className="text-xs text-white/60">or</p>
|
||||
<button
|
||||
className="text-sm text-blue-400 hover:text-blue-300 transition-colors underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setShowAddMemoryView(true)
|
||||
setShowConnectAIModal(false)
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Add your first memory
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ConnectAIModal>
|
||||
</div>
|
||||
<GetStarted />
|
||||
</MemoryListView>
|
||||
</motion.div>
|
||||
)}
|
||||
|
|
@ -735,35 +707,35 @@ const MemoryGraphPage = () => {
|
|||
onClose={() => setShowReferralModal(false)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Wrapper component to handle auth and waitlist checks
|
||||
export default function Page() {
|
||||
const { user, session } = useAuth()
|
||||
const { user, session } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
const url = new URL(window.location.href)
|
||||
const url = new URL(window.location.href);
|
||||
const authenticateChromeExtension = url.searchParams.get(
|
||||
"extension-auth-success",
|
||||
)
|
||||
);
|
||||
|
||||
if (authenticateChromeExtension) {
|
||||
const sessionToken = session?.token
|
||||
const sessionToken = session?.token;
|
||||
const userData = {
|
||||
email: user?.email,
|
||||
name: user?.name,
|
||||
userId: user?.id,
|
||||
}
|
||||
};
|
||||
|
||||
if (sessionToken && userData?.email) {
|
||||
const encodedToken = encodeURIComponent(sessionToken)
|
||||
window.postMessage({ token: encodedToken, userData }, "*")
|
||||
url.searchParams.delete("extension-auth-success")
|
||||
window.history.replaceState({}, "", url.toString())
|
||||
const encodedToken = encodeURIComponent(sessionToken);
|
||||
window.postMessage({ token: encodedToken, userData }, "*");
|
||||
url.searchParams.delete("extension-auth-success");
|
||||
window.history.replaceState({}, "", url.toString());
|
||||
}
|
||||
}
|
||||
}, [user, session])
|
||||
}, [user, session]);
|
||||
|
||||
// Show loading state while checking authentication and waitlist status
|
||||
if (!user) {
|
||||
|
|
@ -774,7 +746,7 @@ export default function Page() {
|
|||
<p className="text-white/60">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// If we have a user and they have access, show the main component
|
||||
|
|
@ -783,5 +755,5 @@ export default function Page() {
|
|||
<MemoryGraphPage />
|
||||
<InstallPrompt />
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,8 +55,6 @@ export default function ReferralPage() {
|
|||
checkReferral();
|
||||
}, [referralCode]);
|
||||
|
||||
|
||||
|
||||
const handleCopyLink = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(referralLink);
|
||||
|
|
@ -146,11 +144,10 @@ export default function ReferralPage() {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="text-center">
|
||||
<Link
|
||||
href="https://supermemory.ai"
|
||||
className="text-orange-500 hover:text-orange-400 text-sm underline"
|
||||
href="https://supermemory.ai"
|
||||
>
|
||||
Learn more about supermemory
|
||||
</Link>
|
||||
|
|
@ -178,10 +175,10 @@ export default function ReferralPage() {
|
|||
</p>
|
||||
</div>
|
||||
<Button
|
||||
className="shrink-0 border-white/10 hover:bg-white/5"
|
||||
onClick={handleCopyLink}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="shrink-0 border-white/10 hover:bg-white/5"
|
||||
>
|
||||
{copiedLink ? (
|
||||
<CheckIcon className="w-4 h-4" />
|
||||
|
|
@ -192,9 +189,9 @@ export default function ReferralPage() {
|
|||
</div>
|
||||
|
||||
<Button
|
||||
className="w-full border-white/10 text-white hover:bg-white/5"
|
||||
onClick={handleShare}
|
||||
variant="outline"
|
||||
className="w-full border-white/10 text-white hover:bg-white/5"
|
||||
>
|
||||
<ShareIcon className="w-4 h-4" />
|
||||
Share this link
|
||||
|
|
|
|||
|
|
@ -1,107 +1,107 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { $fetch } from "@lib/api"
|
||||
import { useSession } from "@lib/auth"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { Logo, LogoFull } from "@ui/assets/Logo"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { Input } from "@ui/components/input"
|
||||
import { GlassMenuEffect } from "@ui/other/glass-effect"
|
||||
import { ArrowRight, CheckCircle, Upload, Zap } from "lucide-react"
|
||||
import { AnimatePresence, motion } from "motion/react"
|
||||
import Link from "next/link"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { useEffect, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { Spinner } from "@/components/spinner"
|
||||
import { $fetch } from "@lib/api";
|
||||
import { useSession } from "@lib/auth";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Logo, LogoFull } from "@ui/assets/Logo";
|
||||
import { Button } from "@ui/components/button";
|
||||
import { Input } from "@ui/components/input";
|
||||
import { GlassMenuEffect } from "@ui/other/glass-effect";
|
||||
import { ArrowRight, CheckCircle, Upload, Zap } from "lucide-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import Link from "next/link";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Spinner } from "@/components/spinner";
|
||||
|
||||
interface MigrateMCPRequest {
|
||||
userId: string
|
||||
projectId: string
|
||||
userId: string;
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
interface MigrateMCPResponse {
|
||||
success: boolean
|
||||
migratedCount: number
|
||||
message: string
|
||||
documentIds?: string[]
|
||||
success: boolean;
|
||||
migratedCount: number;
|
||||
message: string;
|
||||
documentIds?: string[];
|
||||
}
|
||||
|
||||
export default function MigrateMCPPage() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const [mcpUrl, setMcpUrl] = useState("")
|
||||
const [projectId, setProjectId] = useState("default")
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [mcpUrl, setMcpUrl] = useState("");
|
||||
const [projectId, setProjectId] = useState("default");
|
||||
|
||||
const session = useSession()
|
||||
const session = useSession();
|
||||
|
||||
// Extract MCP URL from query parameter
|
||||
useEffect(() => {
|
||||
const urlParam = searchParams.get("url")
|
||||
const urlParam = searchParams.get("url");
|
||||
if (urlParam) {
|
||||
setMcpUrl(urlParam)
|
||||
setMcpUrl(urlParam);
|
||||
}
|
||||
}, [searchParams])
|
||||
}, [searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("session", session)
|
||||
console.log("session", session);
|
||||
if (!session.isPending && !session.data) {
|
||||
const redirectUrl = new URL("/login", window.location.href)
|
||||
redirectUrl.searchParams.set("redirect", window.location.href)
|
||||
router.push(redirectUrl.toString())
|
||||
return
|
||||
const redirectUrl = new URL("/login", window.location.href);
|
||||
redirectUrl.searchParams.set("redirect", window.location.href);
|
||||
router.push(redirectUrl.toString());
|
||||
return;
|
||||
}
|
||||
}, [session, router])
|
||||
}, [session, router]);
|
||||
|
||||
// Extract userId from MCP URL
|
||||
const getUserIdFromUrl = (url: string) => {
|
||||
return url.split("/").at(-2) || ""
|
||||
}
|
||||
return url.split("/").at(-2) || "";
|
||||
};
|
||||
|
||||
const migrateMutation = useMutation({
|
||||
mutationFn: async (data: MigrateMCPRequest) => {
|
||||
const response = await $fetch("@post/documents/migrate-mcp", {
|
||||
body: data,
|
||||
})
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(
|
||||
response.error?.message || "Failed to migrate documents",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return response.data
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: (data: MigrateMCPResponse) => {
|
||||
toast.success("Migration completed successfully", {
|
||||
description: data.message,
|
||||
})
|
||||
});
|
||||
// Redirect to home page after successful migration
|
||||
setTimeout(() => {
|
||||
router.push("/?open=mcp")
|
||||
}, 2000) // Wait 2 seconds to show the success message
|
||||
router.push("/?open=mcp");
|
||||
}, 2000); // Wait 2 seconds to show the success message
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error("Migration failed", {
|
||||
description: error.message || "An unexpected error occurred",
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
e.preventDefault();
|
||||
|
||||
const userId = getUserIdFromUrl(mcpUrl)
|
||||
const userId = getUserIdFromUrl(mcpUrl);
|
||||
if (!userId) {
|
||||
toast.error("Please enter a valid MCP URL")
|
||||
return
|
||||
toast.error("Please enter a valid MCP URL");
|
||||
return;
|
||||
}
|
||||
|
||||
migrateMutation.mutate({
|
||||
userId,
|
||||
projectId: projectId.trim() || "default",
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0f1419] overflow-hidden relative">
|
||||
|
|
@ -320,5 +320,5 @@ export default function MigrateMCPPage() {
|
|||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,4 +9,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -235,7 +235,11 @@ export function ConnectAIModal({
|
|||
parent.appendChild(fallback);
|
||||
}
|
||||
}}
|
||||
src={key === "mcp-url" ? "/mcp-icon.svg" : `/mcp-supported-tools/${key === "claude-code" ? "claude" : key}.png`}
|
||||
src={
|
||||
key === "mcp-url"
|
||||
? "/mcp-icon.svg"
|
||||
: `/mcp-supported-tools/${key === "claude-code" ? "claude" : key}.png`
|
||||
}
|
||||
width={20}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -46,12 +46,12 @@ export function CreateProjectDialog({
|
|||
return (
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<Dialog onOpenChange={onOpenChange} open={open}>
|
||||
<DialogContent className="sm:max-w-2xl bg-[#0f1419] backdrop-blur-xl border-white/10 text-white">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New Project</DialogTitle>
|
||||
|
|
@ -61,23 +61,23 @@ export function CreateProjectDialog({
|
|||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="flex flex-col gap-2"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
>
|
||||
<Label htmlFor="projectName">Project Name</Label>
|
||||
<Input
|
||||
id="projectName"
|
||||
className="bg-white/5 border-white/10 text-white"
|
||||
placeholder="My Awesome Project"
|
||||
value={projectName}
|
||||
id="projectName"
|
||||
onChange={(e) => setProjectName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && projectName.trim()) {
|
||||
handleCreate();
|
||||
}
|
||||
}}
|
||||
placeholder="My Awesome Project"
|
||||
value={projectName}
|
||||
/>
|
||||
<p className="text-xs text-white/50">
|
||||
This will help you organize your memories
|
||||
|
|
@ -90,10 +90,10 @@ export function CreateProjectDialog({
|
|||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="bg-white/5 hover:bg-white/10 border-white/10 text-white"
|
||||
onClick={handleClose}
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
|
@ -103,12 +103,12 @@ export function CreateProjectDialog({
|
|||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
className="bg-white/10 hover:bg-white/20 text-white border-white/20"
|
||||
disabled={
|
||||
createProjectMutation.isPending || !projectName.trim()
|
||||
}
|
||||
onClick={handleCreate}
|
||||
type="button"
|
||||
>
|
||||
{createProjectMutation.isPending ? (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
import { AnimatePresence, motion } from "framer-motion"
|
||||
import { Brain, Chrome, ExternalLink, Plus } from "lucide-react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { ConnectAIModal } from "@/components/connect-ai-modal"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Brain, Chrome, ExternalLink, Plus } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ConnectAIModal } from "@/components/connect-ai-modal";
|
||||
import { analytics } from "@/lib/analytics";
|
||||
|
||||
interface GetStartedProps {
|
||||
setShowAddMemoryView?: (show: boolean) => void
|
||||
setShowAddMemoryView?: (show: boolean) => void;
|
||||
}
|
||||
|
||||
export const GetStarted = ({ setShowAddMemoryView }: GetStartedProps) => {
|
||||
const [imageLoaded, setImageLoaded] = useState(false)
|
||||
const [showContent, setShowContent] = useState(false)
|
||||
const [showConnectAIModal, setShowConnectAIModal] = useState(false)
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
const [showContent, setShowContent] = useState(false);
|
||||
const [showConnectAIModal, setShowConnectAIModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const img = new Image()
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
setImageLoaded(true)
|
||||
setTimeout(() => setShowContent(true), 100)
|
||||
}
|
||||
img.src = "/images/onboarding.png"
|
||||
}, [])
|
||||
setImageLoaded(true);
|
||||
setTimeout(() => setShowContent(true), 100);
|
||||
};
|
||||
img.src = "/images/onboarding.png";
|
||||
}, []);
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
|
|
@ -33,7 +33,7 @@ export const GetStarted = ({ setShowAddMemoryView }: GetStartedProps) => {
|
|||
delayChildren: 0.2,
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const leftColumnVariants = {
|
||||
hidden: {
|
||||
|
|
@ -49,7 +49,7 @@ export const GetStarted = ({ setShowAddMemoryView }: GetStartedProps) => {
|
|||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const rightColumnVariants = {
|
||||
hidden: {
|
||||
|
|
@ -66,7 +66,7 @@ export const GetStarted = ({ setShowAddMemoryView }: GetStartedProps) => {
|
|||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const textLineVariants = {
|
||||
hidden: {
|
||||
|
|
@ -81,7 +81,7 @@ export const GetStarted = ({ setShowAddMemoryView }: GetStartedProps) => {
|
|||
ease: [0.25, 0.1, 0.25, 1] as const,
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const cardVariants = {
|
||||
hidden: {
|
||||
|
|
@ -98,7 +98,7 @@ export const GetStarted = ({ setShowAddMemoryView }: GetStartedProps) => {
|
|||
ease: [0.25, 0.1, 0.25, 1] as const,
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const backgroundVariants = {
|
||||
hidden: {
|
||||
|
|
@ -113,22 +113,22 @@ export const GetStarted = ({ setShowAddMemoryView }: GetStartedProps) => {
|
|||
ease: [0.25, 0.1, 0.25, 1] as const,
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const handleChromeExtension = () => {
|
||||
analytics.extensionInstallClicked()
|
||||
analytics.extensionInstallClicked();
|
||||
window.open(
|
||||
"https://chromewebstore.google.com/detail/supermemory/afpgkkipfdpeaflnpoaffkcankadgjfc",
|
||||
"_blank",
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleAddMemory = () => {
|
||||
setShowConnectAIModal(false)
|
||||
setShowConnectAIModal(false);
|
||||
if (setShowAddMemoryView) {
|
||||
setShowAddMemoryView(true)
|
||||
setShowAddMemoryView(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 overflow-hidden bg-black">
|
||||
|
|
@ -358,5 +358,5 @@ export const GetStarted = ({ setShowAddMemoryView }: GetStartedProps) => {
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -70,9 +70,9 @@ export function InstallPrompt() {
|
|||
<AnimatePresence>
|
||||
<motion.div
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
className="fixed bottom-4 left-4 right-4 z-50 mx-auto max-w-sm md:hidden"
|
||||
exit={{ y: 100, opacity: 0 }}
|
||||
initial={{ y: 100, opacity: 0 }}
|
||||
className="fixed bottom-4 left-4 right-4 z-50 mx-auto max-w-sm md:hidden"
|
||||
>
|
||||
<div className="bg-black/90 backdrop-blur-md text-white rounded-2xl p-4 shadow-2xl border border-white/10">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
|
|
@ -83,10 +83,10 @@ export function InstallPrompt() {
|
|||
<h3 className="font-semibold text-sm">Install Supermemory</h3>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleDismiss}
|
||||
className="text-white/60 hover:text-white h-6 w-6 p-0"
|
||||
onClick={handleDismiss}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
|
|
@ -107,19 +107,19 @@ export function InstallPrompt() {
|
|||
2. Select "Add to Home Screen" ➕
|
||||
</p>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={handleDismiss}
|
||||
className="w-full text-xs"
|
||||
onClick={handleDismiss}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
>
|
||||
Got it
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
className="w-full bg-[#0f1419] hover:bg-[#1a1f2a] text-white text-xs"
|
||||
onClick={handleInstall}
|
||||
size="sm"
|
||||
className="w-full bg-[#0f1419] hover:bg-[#1a1f2a] text-white text-xs"
|
||||
>
|
||||
<Download className="w-3 h-3 mr-1" />
|
||||
Add to Home Screen
|
||||
|
|
|
|||
|
|
@ -50,4 +50,4 @@ export const getSourceUrl = (document: DocumentWithMemories) => {
|
|||
}
|
||||
// Fallback to existing URL for all other document types
|
||||
return document.url;
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,415 +1,422 @@
|
|||
import { getDocumentIcon } from '@/lib/document-icon';
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
} from '@repo/ui/components/drawer';
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
} from "@repo/ui/components/drawer";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from '@repo/ui/components/sheet';
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@repo/ui/components/sheet";
|
||||
import {
|
||||
Tabs,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
TabsContent,
|
||||
} from '@repo/ui/components/tabs';
|
||||
import { colors } from '@repo/ui/memory-graph/constants';
|
||||
import type { DocumentsWithMemoriesResponseSchema } from '@repo/validation/api';
|
||||
import { Badge } from '@ui/components/badge';
|
||||
import { Brain, Calendar, CircleUserRound, ExternalLink, List, Sparkles } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import type { z } from 'zod';
|
||||
import { formatDate, getSourceUrl } from '.';
|
||||
import { Label1Regular } from '@ui/text/label/label-1-regular';
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@repo/ui/components/tabs";
|
||||
import { colors } from "@repo/ui/memory-graph/constants";
|
||||
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api";
|
||||
import { Badge } from "@ui/components/badge";
|
||||
import { Label1Regular } from "@ui/text/label/label-1-regular";
|
||||
import {
|
||||
Brain,
|
||||
Calendar,
|
||||
CircleUserRound,
|
||||
ExternalLink,
|
||||
List,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
import { memo } from "react";
|
||||
import type { z } from "zod";
|
||||
import { getDocumentIcon } from "@/lib/document-icon";
|
||||
import { formatDate, getSourceUrl } from ".";
|
||||
|
||||
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>;
|
||||
type DocumentWithMemories = DocumentsResponse['documents'][0];
|
||||
type MemoryEntry = DocumentWithMemories['memoryEntries'][0];
|
||||
type DocumentWithMemories = DocumentsResponse["documents"][0];
|
||||
type MemoryEntry = DocumentWithMemories["memoryEntries"][0];
|
||||
|
||||
const formatDocumentType = (type: string) => {
|
||||
// Special case for PDF
|
||||
if (type.toLowerCase() === 'pdf') return 'PDF';
|
||||
// Special case for PDF
|
||||
if (type.toLowerCase() === "pdf") return "PDF";
|
||||
|
||||
// Replace underscores with spaces and capitalize each word
|
||||
return type
|
||||
.split('_')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(' ');
|
||||
// Replace underscores with spaces and capitalize each word
|
||||
return type
|
||||
.split("_")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(" ");
|
||||
};
|
||||
|
||||
const MemoryDetailItem = memo(({ memory }: { memory: MemoryEntry }) => {
|
||||
return (
|
||||
<button
|
||||
className="p-4 rounded-lg transition-all relative overflow-hidden cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: memory.isLatest
|
||||
? colors.memory.primary
|
||||
: 'rgba(255, 255, 255, 0.02)',
|
||||
}}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
>
|
||||
<div className="flex items-start gap-2 relative z-10">
|
||||
<div
|
||||
className="p-1 rounded"
|
||||
style={{
|
||||
backgroundColor: memory.isLatest
|
||||
? colors.memory.secondary
|
||||
: 'transparent',
|
||||
}}
|
||||
>
|
||||
<Brain
|
||||
className={`w-4 h-4 flex-shrink-0 transition-all ${
|
||||
memory.isLatest ? 'text-blue-400' : 'text-blue-400/50'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 space-y-2">
|
||||
<Label1Regular
|
||||
className="text-sm leading-relaxed text-left"
|
||||
style={{ color: colors.text.primary }}
|
||||
>
|
||||
{memory.memory}
|
||||
</Label1Regular>
|
||||
<div className="flex gap-2 justify-between">
|
||||
<div
|
||||
className="flex items-center gap-4 text-xs"
|
||||
style={{ color: colors.text.muted }}
|
||||
>
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="w-3 h-3" />
|
||||
{formatDate(memory.createdAt)}
|
||||
</span>
|
||||
<span className="font-mono">v{memory.version}</span>
|
||||
{memory.sourceRelevanceScore && (
|
||||
<span
|
||||
className="flex items-center gap-1"
|
||||
style={{
|
||||
color:
|
||||
memory.sourceRelevanceScore > 70
|
||||
? colors.accent.emerald
|
||||
: colors.text.muted,
|
||||
}}
|
||||
>
|
||||
<Sparkles className="w-3 h-3" />
|
||||
{memory.sourceRelevanceScore}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{memory.isForgotten && (
|
||||
<Badge
|
||||
className="text-xs border-red-500/30 backdrop-blur-sm"
|
||||
style={{
|
||||
backgroundColor: colors.status.forgotten,
|
||||
color: '#dc2626',
|
||||
backdropFilter: 'blur(4px)',
|
||||
WebkitBackdropFilter: 'blur(4px)',
|
||||
}}
|
||||
variant="destructive"
|
||||
>
|
||||
Forgotten
|
||||
</Badge>
|
||||
)}
|
||||
{memory.isLatest && (
|
||||
<Badge
|
||||
className="text-xs"
|
||||
style={{
|
||||
backgroundColor: colors.memory.secondary,
|
||||
color: colors.text.primary,
|
||||
backdropFilter: 'blur(4px)',
|
||||
WebkitBackdropFilter: 'blur(4px)',
|
||||
}}
|
||||
variant="default"
|
||||
>
|
||||
Latest
|
||||
</Badge>
|
||||
)}
|
||||
{memory.forgetAfter && (
|
||||
<Badge
|
||||
className="text-xs backdrop-blur-sm"
|
||||
style={{
|
||||
color: colors.status.expiring,
|
||||
backgroundColor: 'rgba(251, 165, 36, 0.1)',
|
||||
backdropFilter: 'blur(4px)',
|
||||
WebkitBackdropFilter: 'blur(4px)',
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
Expires: {formatDate(memory.forgetAfter)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
return (
|
||||
<button
|
||||
className="p-4 rounded-lg transition-all relative overflow-hidden cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: memory.isLatest
|
||||
? colors.memory.primary
|
||||
: "rgba(255, 255, 255, 0.02)",
|
||||
}}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
>
|
||||
<div className="flex items-start gap-2 relative z-10">
|
||||
<div
|
||||
className="p-1 rounded"
|
||||
style={{
|
||||
backgroundColor: memory.isLatest
|
||||
? colors.memory.secondary
|
||||
: "transparent",
|
||||
}}
|
||||
>
|
||||
<Brain
|
||||
className={`w-4 h-4 flex-shrink-0 transition-all ${
|
||||
memory.isLatest ? "text-blue-400" : "text-blue-400/50"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 space-y-2">
|
||||
<Label1Regular
|
||||
className="text-sm leading-relaxed text-left"
|
||||
style={{ color: colors.text.primary }}
|
||||
>
|
||||
{memory.memory}
|
||||
</Label1Regular>
|
||||
<div className="flex gap-2 justify-between">
|
||||
<div
|
||||
className="flex items-center gap-4 text-xs"
|
||||
style={{ color: colors.text.muted }}
|
||||
>
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="w-3 h-3" />
|
||||
{formatDate(memory.createdAt)}
|
||||
</span>
|
||||
<span className="font-mono">v{memory.version}</span>
|
||||
{memory.sourceRelevanceScore && (
|
||||
<span
|
||||
className="flex items-center gap-1"
|
||||
style={{
|
||||
color:
|
||||
memory.sourceRelevanceScore > 70
|
||||
? colors.accent.emerald
|
||||
: colors.text.muted,
|
||||
}}
|
||||
>
|
||||
<Sparkles className="w-3 h-3" />
|
||||
{memory.sourceRelevanceScore}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{memory.isForgotten && (
|
||||
<Badge
|
||||
className="text-xs border-red-500/30 backdrop-blur-sm"
|
||||
style={{
|
||||
backgroundColor: colors.status.forgotten,
|
||||
color: "#dc2626",
|
||||
backdropFilter: "blur(4px)",
|
||||
WebkitBackdropFilter: "blur(4px)",
|
||||
}}
|
||||
variant="destructive"
|
||||
>
|
||||
Forgotten
|
||||
</Badge>
|
||||
)}
|
||||
{memory.isLatest && (
|
||||
<Badge
|
||||
className="text-xs"
|
||||
style={{
|
||||
backgroundColor: colors.memory.secondary,
|
||||
color: colors.text.primary,
|
||||
backdropFilter: "blur(4px)",
|
||||
WebkitBackdropFilter: "blur(4px)",
|
||||
}}
|
||||
variant="default"
|
||||
>
|
||||
Latest
|
||||
</Badge>
|
||||
)}
|
||||
{memory.forgetAfter && (
|
||||
<Badge
|
||||
className="text-xs backdrop-blur-sm"
|
||||
style={{
|
||||
color: colors.status.expiring,
|
||||
backgroundColor: "rgba(251, 165, 36, 0.1)",
|
||||
backdropFilter: "blur(4px)",
|
||||
WebkitBackdropFilter: "blur(4px)",
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
Expires: {formatDate(memory.forgetAfter)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
export const MemoryDetail = memo(
|
||||
({
|
||||
document,
|
||||
isOpen,
|
||||
onClose,
|
||||
isMobile,
|
||||
}: {
|
||||
document: DocumentWithMemories | null;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
isMobile: boolean;
|
||||
}) => {
|
||||
if (!document) return null;
|
||||
({
|
||||
document,
|
||||
isOpen,
|
||||
onClose,
|
||||
isMobile,
|
||||
}: {
|
||||
document: DocumentWithMemories | null;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
isMobile: boolean;
|
||||
}) => {
|
||||
if (!document) return null;
|
||||
|
||||
const activeMemories = document.memoryEntries.filter((m) => !m.isForgotten);
|
||||
const forgottenMemories = document.memoryEntries.filter(
|
||||
(m) => m.isForgotten
|
||||
);
|
||||
const activeMemories = (document.memoryEntries || []).filter(
|
||||
(m) => !m.isForgotten,
|
||||
);
|
||||
const forgottenMemories = (document.memoryEntries || []).filter(
|
||||
(m) => m.isForgotten,
|
||||
);
|
||||
|
||||
const HeaderContent = ({
|
||||
TitleComponent,
|
||||
}: {
|
||||
TitleComponent: typeof SheetTitle | typeof DrawerTitle;
|
||||
}) => (
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-start gap-3 flex-1">
|
||||
<div
|
||||
className="p-2 rounded-lg"
|
||||
style={{
|
||||
backgroundColor: colors.background.secondary,
|
||||
}}
|
||||
>
|
||||
{getDocumentIcon(document.type, 'w-5 h-5')}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<TitleComponent style={{ color: colors.text.primary }}>
|
||||
{document.title || 'Untitled Document'}
|
||||
</TitleComponent>
|
||||
<div
|
||||
className="flex items-center gap-2 mt-1 text-xs"
|
||||
style={{ color: colors.text.muted }}
|
||||
>
|
||||
<span>{formatDocumentType(document.type)}</span>
|
||||
<span>•</span>
|
||||
<span>{formatDate(document.createdAt)}</span>
|
||||
{document.url && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<button
|
||||
className="flex items-center gap-1 transition-all hover:gap-2"
|
||||
onClick={() => {
|
||||
const sourceUrl = getSourceUrl(document);
|
||||
window.open(sourceUrl ?? undefined, '_blank');
|
||||
}}
|
||||
style={{ color: colors.accent.primary }}
|
||||
type="button"
|
||||
>
|
||||
View source
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const HeaderContent = ({
|
||||
TitleComponent,
|
||||
}: {
|
||||
TitleComponent: typeof SheetTitle | typeof DrawerTitle;
|
||||
}) => (
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-start gap-3 flex-1">
|
||||
<div
|
||||
className="p-2 rounded-lg"
|
||||
style={{
|
||||
backgroundColor: colors.background.secondary,
|
||||
}}
|
||||
>
|
||||
{getDocumentIcon(document.type, "w-5 h-5")}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<TitleComponent style={{ color: colors.text.primary }}>
|
||||
{document.title || "Untitled Document"}
|
||||
</TitleComponent>
|
||||
<div
|
||||
className="flex items-center gap-2 mt-1 text-xs"
|
||||
style={{ color: colors.text.muted }}
|
||||
>
|
||||
<span>{formatDocumentType(document.type)}</span>
|
||||
<span>•</span>
|
||||
<span>{formatDate(document.createdAt)}</span>
|
||||
{document.url && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<button
|
||||
className="flex items-center gap-1 transition-all hover:gap-2"
|
||||
onClick={() => {
|
||||
const sourceUrl = getSourceUrl(document);
|
||||
window.open(sourceUrl ?? undefined, "_blank");
|
||||
}}
|
||||
style={{ color: colors.accent.primary }}
|
||||
type="button"
|
||||
>
|
||||
View source
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ContentAndSummarySection = () => {
|
||||
const hasContent = document.content && document.content.trim().length > 0;
|
||||
const hasSummary = document.summary && document.summary.trim().length > 0;
|
||||
|
||||
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 (
|
||||
<div className="mt-4">
|
||||
<Tabs defaultValue={defaultTab} className="w-full">
|
||||
<TabsList
|
||||
className={`grid w-full bg-white/5 border border-white/10 h-11 ${
|
||||
hasContent && hasSummary ? 'grid-cols-2' : 'grid-cols-1'
|
||||
}`}
|
||||
>
|
||||
{hasContent && (
|
||||
<TabsTrigger
|
||||
value="content"
|
||||
className="text-xs bg-transparent h-8"
|
||||
style={{ color: colors.text.secondary }}
|
||||
>
|
||||
<CircleUserRound className="w-3 h-3" />
|
||||
Original Content
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{hasSummary && (
|
||||
<TabsTrigger
|
||||
value="summary"
|
||||
className="text-xs flex items-center gap-1 bg-transparent h-8"
|
||||
style={{ color: colors.text.secondary }}
|
||||
>
|
||||
<List className="w-3 h-3" />
|
||||
Summary
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
const defaultTab = hasContent ? "content" : "summary";
|
||||
|
||||
{hasContent && (
|
||||
<TabsContent value="content" className="mt-3">
|
||||
<div className="p-3 rounded-lg max-h-48 overflow-y-auto custom-scrollbar bg-white/[0.03] border border-white/[0.08]">
|
||||
<p
|
||||
className="text-sm leading-relaxed whitespace-pre-wrap"
|
||||
style={{ color: colors.text.primary }}
|
||||
>
|
||||
{document.content}
|
||||
</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<Tabs className="w-full" defaultValue={defaultTab}>
|
||||
<TabsList
|
||||
className={`grid w-full bg-white/5 border border-white/10 h-11 ${
|
||||
hasContent && hasSummary ? "grid-cols-2" : "grid-cols-1"
|
||||
}`}
|
||||
>
|
||||
{hasContent && (
|
||||
<TabsTrigger
|
||||
className="text-xs bg-transparent h-8"
|
||||
style={{ color: colors.text.secondary }}
|
||||
value="content"
|
||||
>
|
||||
<CircleUserRound className="w-3 h-3" />
|
||||
Original Content
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{hasSummary && (
|
||||
<TabsTrigger
|
||||
className="text-xs flex items-center gap-1 bg-transparent h-8"
|
||||
style={{ color: colors.text.secondary }}
|
||||
value="summary"
|
||||
>
|
||||
<List className="w-3 h-3" />
|
||||
Summary
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
|
||||
{hasSummary && (
|
||||
<TabsContent value="summary" className="mt-3">
|
||||
<div className="p-3 rounded-lg max-h-48 overflow-y-auto custom-scrollbar bg-indigo-500/5 border border-indigo-500/15">
|
||||
<p
|
||||
className="text-sm leading-relaxed whitespace-pre-wrap"
|
||||
style={{ color: colors.text.muted }}
|
||||
>
|
||||
{document.summary}
|
||||
</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
{hasContent && (
|
||||
<TabsContent className="mt-3" value="content">
|
||||
<div className="p-3 rounded-lg max-h-48 overflow-y-auto custom-scrollbar bg-white/[0.03] border border-white/[0.08]">
|
||||
<p
|
||||
className="text-sm leading-relaxed whitespace-pre-wrap"
|
||||
style={{ color: colors.text.primary }}
|
||||
>
|
||||
{document.content}
|
||||
</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
const MemoryContent = () => (
|
||||
<div className="space-y-6 px-6">
|
||||
{activeMemories.length > 0 && (
|
||||
<div>
|
||||
<div
|
||||
className="text-sm font-medium mb-2 flex items-start gap-2 py-2"
|
||||
style={{
|
||||
color: colors.text.secondary,
|
||||
}}
|
||||
>
|
||||
Active Memories ({activeMemories.length})
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{activeMemories.map((memory) => (
|
||||
<div
|
||||
key={memory.id}
|
||||
>
|
||||
<MemoryDetailItem memory={memory} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{hasSummary && (
|
||||
<TabsContent className="mt-3" value="summary">
|
||||
<div className="p-3 rounded-lg max-h-48 overflow-y-auto custom-scrollbar bg-indigo-500/5 border border-indigo-500/15">
|
||||
<p
|
||||
className="text-sm leading-relaxed whitespace-pre-wrap"
|
||||
style={{ color: colors.text.muted }}
|
||||
>
|
||||
{document.summary}
|
||||
</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
{forgottenMemories.length > 0 && (
|
||||
<div>
|
||||
<div
|
||||
className="text-sm font-medium mb-4 px-3 py-2 rounded-lg opacity-60"
|
||||
style={{
|
||||
color: colors.text.muted,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.02)',
|
||||
}}
|
||||
>
|
||||
Forgotten Memories ({forgottenMemories.length})
|
||||
</div>
|
||||
<div className="space-y-3 opacity-40">
|
||||
{forgottenMemories.map((memory) => (
|
||||
<MemoryDetailItem key={memory.id} memory={memory} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
const MemoryContent = () => (
|
||||
<div className="space-y-6 px-6">
|
||||
{activeMemories.length > 0 && (
|
||||
<div>
|
||||
<div
|
||||
className="text-sm font-medium mb-2 flex items-start gap-2 py-2"
|
||||
style={{
|
||||
color: colors.text.secondary,
|
||||
}}
|
||||
>
|
||||
Active Memories ({activeMemories.length})
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{activeMemories.map((memory) => (
|
||||
<div key={memory.id}>
|
||||
<MemoryDetailItem memory={memory} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeMemories.length === 0 && forgottenMemories.length === 0 && (
|
||||
<div
|
||||
className="text-center py-12 rounded-lg"
|
||||
style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.02)',
|
||||
}}
|
||||
>
|
||||
<Brain
|
||||
className="w-12 h-12 mx-auto mb-4 opacity-30"
|
||||
style={{ color: colors.text.muted }}
|
||||
/>
|
||||
<p style={{ color: colors.text.muted }}>
|
||||
No memories found for this document
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
{forgottenMemories.length > 0 && (
|
||||
<div>
|
||||
<div
|
||||
className="text-sm font-medium mb-4 px-3 py-2 rounded-lg opacity-60"
|
||||
style={{
|
||||
color: colors.text.muted,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.02)",
|
||||
}}
|
||||
>
|
||||
Forgotten Memories ({forgottenMemories.length})
|
||||
</div>
|
||||
<div className="space-y-3 opacity-40">
|
||||
{forgottenMemories.map((memory) => (
|
||||
<MemoryDetailItem key={memory.id} memory={memory} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Drawer onOpenChange={onClose} open={isOpen}>
|
||||
<DrawerContent
|
||||
className="border-0 p-0 overflow-hidden max-h-[90vh]"
|
||||
style={{
|
||||
backgroundColor: colors.background.secondary,
|
||||
borderTop: `1px solid ${colors.document.border}`,
|
||||
backdropFilter: 'blur(20px)',
|
||||
WebkitBackdropFilter: 'blur(20px)',
|
||||
}}
|
||||
>
|
||||
{/* Header section with glass effect */}
|
||||
<div
|
||||
className="p-4 relative border-b"
|
||||
style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.02)',
|
||||
borderBottom: `1px solid ${colors.document.border}`,
|
||||
}}
|
||||
>
|
||||
<DrawerHeader className="pb-0 px-0 text-left">
|
||||
<HeaderContent TitleComponent={DrawerTitle} />
|
||||
</DrawerHeader>
|
||||
{activeMemories.length === 0 && forgottenMemories.length === 0 && (
|
||||
<div
|
||||
className="text-center py-12 rounded-lg"
|
||||
style={{
|
||||
backgroundColor: "rgba(255, 255, 255, 0.02)",
|
||||
}}
|
||||
>
|
||||
<Brain
|
||||
className="w-12 h-12 mx-auto mb-4 opacity-30"
|
||||
style={{ color: colors.text.muted }}
|
||||
/>
|
||||
<p style={{ color: colors.text.muted }}>
|
||||
No memories found for this document
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
<ContentAndSummarySection />
|
||||
</div>
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Drawer onOpenChange={onClose} open={isOpen}>
|
||||
<DrawerContent
|
||||
className="border-0 p-0 overflow-hidden max-h-[90vh]"
|
||||
style={{
|
||||
backgroundColor: colors.background.secondary,
|
||||
borderTop: `1px solid ${colors.document.border}`,
|
||||
backdropFilter: "blur(20px)",
|
||||
WebkitBackdropFilter: "blur(20px)",
|
||||
}}
|
||||
>
|
||||
{/* Header section with glass effect */}
|
||||
<div
|
||||
className="p-4 relative border-b"
|
||||
style={{
|
||||
backgroundColor: "rgba(255, 255, 255, 0.02)",
|
||||
borderBottom: `1px solid ${colors.document.border}`,
|
||||
}}
|
||||
>
|
||||
<DrawerHeader className="pb-0 px-0 text-left">
|
||||
<HeaderContent TitleComponent={DrawerTitle} />
|
||||
</DrawerHeader>
|
||||
|
||||
<div className="flex-1 memory-drawer-scroll overflow-y-auto">
|
||||
<MemoryContent />
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
<ContentAndSummarySection />
|
||||
</div>
|
||||
|
||||
return (
|
||||
<Sheet onOpenChange={onClose} open={isOpen}>
|
||||
<SheetContent
|
||||
className="w-full sm:max-w-2xl border-0 p-0 overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: colors.background.secondary,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="p-6 relative"
|
||||
style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.02)',
|
||||
}}
|
||||
>
|
||||
<SheetHeader className="pb-0">
|
||||
<HeaderContent TitleComponent={SheetTitle} />
|
||||
</SheetHeader>
|
||||
<div className="flex-1 memory-drawer-scroll overflow-y-auto">
|
||||
<MemoryContent />
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
<ContentAndSummarySection />
|
||||
</div>
|
||||
return (
|
||||
<Sheet onOpenChange={onClose} open={isOpen}>
|
||||
<SheetContent
|
||||
className="w-full sm:max-w-2xl border-0 p-0 overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: colors.background.secondary,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="p-6 relative"
|
||||
style={{
|
||||
backgroundColor: "rgba(255, 255, 255, 0.02)",
|
||||
}}
|
||||
>
|
||||
<SheetHeader className="pb-0">
|
||||
<HeaderContent TitleComponent={SheetTitle} />
|
||||
</SheetHeader>
|
||||
|
||||
<div className="h-[calc(100vh-200px)] memory-sheet-scroll overflow-y-auto">
|
||||
<MemoryContent />
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
<ContentAndSummarySection />
|
||||
</div>
|
||||
|
||||
<div className="h-[calc(100vh-200px)] memory-sheet-scroll overflow-y-auto">
|
||||
<MemoryContent />
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useIsMobile } from "@hooks/use-mobile"
|
||||
import { cn } from "@lib/utils"
|
||||
import { Badge } from "@repo/ui/components/badge"
|
||||
import { Card, CardContent, CardHeader } from "@repo/ui/components/card"
|
||||
import { useIsMobile } from "@hooks/use-mobile";
|
||||
import { useDeleteDocument } from "@lib/queries";
|
||||
import { cn } from "@lib/utils";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
|
|
@ -14,43 +13,43 @@ import {
|
|||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@repo/ui/components/alert-dialog"
|
||||
import { colors } from "@repo/ui/memory-graph/constants"
|
||||
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
|
||||
import { useVirtualizer } from "@tanstack/react-virtual"
|
||||
import { Brain, ExternalLink, Sparkles, Trash2 } from "lucide-react"
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import type { z } from "zod"
|
||||
import useResizeObserver from "@/hooks/use-resize-observer"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
import { useDeleteDocument } from "@lib/queries"
|
||||
import { useProject } from "@/stores"
|
||||
} from "@repo/ui/components/alert-dialog";
|
||||
import { Badge } from "@repo/ui/components/badge";
|
||||
import { Card, CardContent, CardHeader } from "@repo/ui/components/card";
|
||||
import { colors } from "@repo/ui/memory-graph/constants";
|
||||
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { Brain, ExternalLink, Sparkles, Trash2 } from "lucide-react";
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { z } from "zod";
|
||||
import useResizeObserver from "@/hooks/use-resize-observer";
|
||||
import { analytics } from "@/lib/analytics";
|
||||
import { getDocumentIcon } from "@/lib/document-icon";
|
||||
import { useProject } from "@/stores";
|
||||
import { formatDate, getSourceUrl } from "./memories";
|
||||
import { MemoryDetail } from "./memories/memory-detail";
|
||||
|
||||
import { MemoryDetail } from "./memories/memory-detail"
|
||||
import { getDocumentIcon } from "@/lib/document-icon"
|
||||
import { formatDate, getSourceUrl } from "./memories"
|
||||
|
||||
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
|
||||
type DocumentWithMemories = DocumentsResponse["documents"][0]
|
||||
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>;
|
||||
type DocumentWithMemories = DocumentsResponse["documents"][0];
|
||||
|
||||
interface MemoryListViewProps {
|
||||
children?: React.ReactNode
|
||||
documents: DocumentWithMemories[]
|
||||
isLoading: boolean
|
||||
isLoadingMore: boolean
|
||||
error: Error | null
|
||||
totalLoaded: number
|
||||
hasMore: boolean
|
||||
loadMoreDocuments: () => Promise<void>
|
||||
children?: React.ReactNode;
|
||||
documents: DocumentWithMemories[];
|
||||
isLoading: boolean;
|
||||
isLoadingMore: boolean;
|
||||
error: Error | null;
|
||||
totalLoaded: number;
|
||||
hasMore: boolean;
|
||||
loadMoreDocuments: () => Promise<void>;
|
||||
}
|
||||
|
||||
const GreetingMessage = memo(() => {
|
||||
const getGreeting = () => {
|
||||
const hour = new Date().getHours()
|
||||
if (hour < 12) return "Good morning"
|
||||
if (hour < 17) return "Good afternoon"
|
||||
return "Good evening"
|
||||
}
|
||||
const hour = new Date().getHours();
|
||||
if (hour < 12) return "Good morning";
|
||||
if (hour < 17) return "Good afternoon";
|
||||
return "Good evening";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 mb-3 px-4 md:mb-6 md:mt-3">
|
||||
|
|
@ -66,8 +65,8 @@ const GreetingMessage = memo(() => {
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const DocumentCard = memo(
|
||||
({
|
||||
|
|
@ -75,21 +74,23 @@ const DocumentCard = memo(
|
|||
onOpenDetails,
|
||||
onDelete,
|
||||
}: {
|
||||
document: DocumentWithMemories
|
||||
onOpenDetails: (document: DocumentWithMemories) => void
|
||||
onDelete: (document: DocumentWithMemories) => void
|
||||
document: DocumentWithMemories;
|
||||
onOpenDetails: (document: DocumentWithMemories) => void;
|
||||
onDelete: (document: DocumentWithMemories) => void;
|
||||
}) => {
|
||||
const activeMemories = document.memoryEntries.filter((m) => !m.isForgotten)
|
||||
const forgottenMemories = document.memoryEntries.filter(
|
||||
const activeMemories = (document.memoryEntries || []).filter(
|
||||
(m) => !m.isForgotten,
|
||||
);
|
||||
const forgottenMemories = (document.memoryEntries || []).filter(
|
||||
(m) => m.isForgotten,
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="h-full mx-4 p-4 transition-all cursor-pointer group relative overflow-hidden border-0 gap-2 md:w-full"
|
||||
onClick={() => {
|
||||
analytics.documentCardClicked()
|
||||
onOpenDetails(document)
|
||||
analytics.documentCardClicked();
|
||||
onOpenDetails(document);
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: colors.document.primary,
|
||||
|
|
@ -112,9 +113,9 @@ const DocumentCard = memo(
|
|||
<button
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
const sourceUrl = getSourceUrl(document)
|
||||
window.open(sourceUrl ?? undefined, "_blank")
|
||||
e.stopPropagation();
|
||||
const sourceUrl = getSourceUrl(document);
|
||||
window.open(sourceUrl ?? undefined, "_blank");
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: "rgba(255, 255, 255, 0.05)",
|
||||
|
|
@ -141,7 +142,20 @@ const DocumentCard = memo(
|
|||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{activeMemories.length > 0 && (
|
||||
{(document as any).isOptimistic && (
|
||||
<Badge
|
||||
className="text-xs text-blue-200 animate-pulse"
|
||||
style={{
|
||||
backgroundColor: "rgba(59, 130, 246, 0.2)",
|
||||
borderColor: "rgba(59, 130, 246, 0.3)",
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
<Sparkles className="w-3 h-3 mr-1" />
|
||||
Supermemory updating...
|
||||
</Badge>
|
||||
)}
|
||||
{!(document as any).isOptimistic && activeMemories.length > 0 && (
|
||||
<Badge
|
||||
className="text-xs text-accent-foreground"
|
||||
style={{
|
||||
|
|
@ -173,7 +187,7 @@ const DocumentCard = memo(
|
|||
<button
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity p-1.5 rounded-md hover:bg-red-500/20"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.stopPropagation();
|
||||
}}
|
||||
style={{
|
||||
color: colors.text.muted,
|
||||
|
|
@ -194,7 +208,7 @@ const DocumentCard = memo(
|
|||
<AlertDialogFooter>
|
||||
<AlertDialogCancel
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
|
|
@ -202,8 +216,8 @@ const DocumentCard = memo(
|
|||
<AlertDialogAction
|
||||
className="bg-red-600 hover:bg-red-700 text-white"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDelete(document)
|
||||
e.stopPropagation();
|
||||
onDelete(document);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
|
|
@ -214,9 +228,9 @@ const DocumentCard = memo(
|
|||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
);
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
export const MemoryListView = ({
|
||||
children,
|
||||
|
|
@ -227,86 +241,86 @@ export const MemoryListView = ({
|
|||
hasMore,
|
||||
loadMoreDocuments,
|
||||
}: MemoryListViewProps) => {
|
||||
const [selectedSpace, _] = useState<string>("all")
|
||||
const [selectedSpace, _] = useState<string>("all");
|
||||
const [selectedDocument, setSelectedDocument] =
|
||||
useState<DocumentWithMemories | null>(null)
|
||||
const [isDetailOpen, setIsDetailOpen] = useState(false)
|
||||
const parentRef = useRef<HTMLDivElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const isMobile = useIsMobile()
|
||||
const { selectedProject } = useProject()
|
||||
const deleteDocumentMutation = useDeleteDocument(selectedProject)
|
||||
useState<DocumentWithMemories | null>(null);
|
||||
const [isDetailOpen, setIsDetailOpen] = useState(false);
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const isMobile = useIsMobile();
|
||||
const { selectedProject } = useProject();
|
||||
const deleteDocumentMutation = useDeleteDocument(selectedProject);
|
||||
|
||||
const gap = 14
|
||||
const gap = 14;
|
||||
|
||||
const handleDeleteDocument = useCallback(
|
||||
(document: DocumentWithMemories) => {
|
||||
deleteDocumentMutation.mutate(document.id)
|
||||
deleteDocumentMutation.mutate(document.id);
|
||||
},
|
||||
[deleteDocumentMutation],
|
||||
)
|
||||
);
|
||||
|
||||
const { width: containerWidth } = useResizeObserver(containerRef)
|
||||
const columnWidth = isMobile ? containerWidth : 320
|
||||
const { width: containerWidth } = useResizeObserver(containerRef);
|
||||
const columnWidth = isMobile ? containerWidth : 320;
|
||||
const columns = Math.max(
|
||||
1,
|
||||
Math.floor((containerWidth + gap) / (columnWidth + gap)),
|
||||
)
|
||||
);
|
||||
|
||||
// Filter documents based on selected space
|
||||
const filteredDocuments = useMemo(() => {
|
||||
if (!documents) return []
|
||||
if (!documents) return [];
|
||||
|
||||
if (selectedSpace === "all") {
|
||||
return documents
|
||||
return documents;
|
||||
}
|
||||
|
||||
return documents
|
||||
.map((doc) => ({
|
||||
...doc,
|
||||
memoryEntries: doc.memoryEntries.filter(
|
||||
memoryEntries: (doc.memoryEntries || []).filter(
|
||||
(memory) =>
|
||||
(memory.spaceContainerTag ?? memory.spaceId) === selectedSpace,
|
||||
),
|
||||
}))
|
||||
.filter((doc) => doc.memoryEntries.length > 0)
|
||||
}, [documents, selectedSpace])
|
||||
.filter((doc) => (doc.memoryEntries || []).length > 0);
|
||||
}, [documents, selectedSpace]);
|
||||
|
||||
const handleOpenDetails = useCallback((document: DocumentWithMemories) => {
|
||||
analytics.memoryDetailOpened()
|
||||
setSelectedDocument(document)
|
||||
setIsDetailOpen(true)
|
||||
}, [])
|
||||
analytics.memoryDetailOpened();
|
||||
setSelectedDocument(document);
|
||||
setIsDetailOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleCloseDetails = useCallback(() => {
|
||||
setIsDetailOpen(false)
|
||||
setTimeout(() => setSelectedDocument(null), 300)
|
||||
}, [])
|
||||
setIsDetailOpen(false);
|
||||
setTimeout(() => setSelectedDocument(null), 300);
|
||||
}, []);
|
||||
|
||||
const virtualItems = useMemo(() => {
|
||||
const items = []
|
||||
const items = [];
|
||||
for (let i = 0; i < filteredDocuments.length; i += columns) {
|
||||
items.push(filteredDocuments.slice(i, i + columns))
|
||||
items.push(filteredDocuments.slice(i, i + columns));
|
||||
}
|
||||
return items
|
||||
}, [filteredDocuments, columns])
|
||||
return items;
|
||||
}, [filteredDocuments, columns]);
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: virtualItems.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
overscan: 5,
|
||||
estimateSize: () => 200,
|
||||
})
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const [lastItem] = [...virtualizer.getVirtualItems()].reverse()
|
||||
const [lastItem] = [...virtualizer.getVirtualItems()].reverse();
|
||||
|
||||
if (!lastItem || !hasMore || isLoadingMore) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastItem.index >= virtualItems.length - 1) {
|
||||
loadMoreDocuments()
|
||||
loadMoreDocuments();
|
||||
}
|
||||
}, [
|
||||
hasMore,
|
||||
|
|
@ -314,7 +328,7 @@ export const MemoryListView = ({
|
|||
loadMoreDocuments,
|
||||
virtualizer.getVirtualItems(),
|
||||
virtualItems.length,
|
||||
])
|
||||
]);
|
||||
|
||||
// Always render with consistent structure
|
||||
return (
|
||||
|
|
@ -355,8 +369,8 @@ export const MemoryListView = ({
|
|||
</div>
|
||||
) : (
|
||||
<div
|
||||
ref={parentRef}
|
||||
className="h-full overflow-auto mt-20 custom-scrollbar"
|
||||
ref={parentRef}
|
||||
>
|
||||
<GreetingMessage />
|
||||
|
||||
|
|
@ -367,15 +381,15 @@ export const MemoryListView = ({
|
|||
}}
|
||||
>
|
||||
{virtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const rowItems = virtualItems[virtualRow.index]
|
||||
if (!rowItems) return null
|
||||
const rowItems = virtualItems[virtualRow.index];
|
||||
if (!rowItems) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={virtualRow.key}
|
||||
data-index={virtualRow.index}
|
||||
ref={virtualizer.measureElement}
|
||||
className="absolute top-0 left-0 w-full"
|
||||
data-index={virtualRow.index}
|
||||
key={virtualRow.key}
|
||||
ref={virtualizer.measureElement}
|
||||
style={{
|
||||
transform: `translateY(${virtualRow.start + virtualRow.index * gap}px)`,
|
||||
}}
|
||||
|
|
@ -389,15 +403,15 @@ export const MemoryListView = ({
|
|||
>
|
||||
{rowItems.map((document, columnIndex) => (
|
||||
<DocumentCard
|
||||
key={`${document.id}-${virtualRow.index}-${columnIndex}`}
|
||||
document={document}
|
||||
onOpenDetails={handleOpenDetails}
|
||||
key={`${document.id}-${virtualRow.index}-${columnIndex}`}
|
||||
onDelete={handleDeleteDocument}
|
||||
onOpenDetails={handleOpenDetails}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
|
|
@ -417,10 +431,10 @@ export const MemoryListView = ({
|
|||
|
||||
<MemoryDetail
|
||||
document={selectedDocument}
|
||||
isMobile={isMobile}
|
||||
isOpen={isDetailOpen}
|
||||
onClose={handleCloseDetails}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,28 +1,28 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useIsMobile } from "@hooks/use-mobile"
|
||||
import { useIsMobile } from "@hooks/use-mobile";
|
||||
import {
|
||||
fetchConsumerProProduct,
|
||||
fetchMemoriesFeature,
|
||||
} from "@repo/lib/queries"
|
||||
import { Button } from "@repo/ui/components/button"
|
||||
import { ConnectAIModal } from "./connect-ai-modal"
|
||||
import { HeadingH2Bold } from "@repo/ui/text/heading/heading-h2-bold"
|
||||
import { GlassMenuEffect } from "@ui/other/glass-effect"
|
||||
import { useCustomer } from "autumn-js/react"
|
||||
import { MessageSquareMore, Plus, Puzzle, User, X } from "lucide-react"
|
||||
import { AnimatePresence, LayoutGroup, motion } from "motion/react"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { Drawer } from "vaul"
|
||||
import { useMobilePanel } from "@/lib/mobile-panel-context"
|
||||
import { TOUR_STEP_IDS } from "@/lib/tour-constants"
|
||||
import { useChatOpen } from "@/stores"
|
||||
import { ProjectSelector } from "./project-selector"
|
||||
import { useTour } from "./tour"
|
||||
import { AddMemoryExpandedView, AddMemoryView } from "./views/add-memory"
|
||||
import { IntegrationsView } from "./views/integrations"
|
||||
import { ProfileView } from "./views/profile"
|
||||
} from "@repo/lib/queries";
|
||||
import { Button } from "@repo/ui/components/button";
|
||||
import { HeadingH2Bold } from "@repo/ui/text/heading/heading-h2-bold";
|
||||
import { GlassMenuEffect } from "@ui/other/glass-effect";
|
||||
import { useCustomer } from "autumn-js/react";
|
||||
import { MessageSquareMore, Plus, Puzzle, User, X } from "lucide-react";
|
||||
import { AnimatePresence, LayoutGroup, motion } from "motion/react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Drawer } from "vaul";
|
||||
import { useMobilePanel } from "@/lib/mobile-panel-context";
|
||||
import { TOUR_STEP_IDS } from "@/lib/tour-constants";
|
||||
import { useChatOpen } from "@/stores";
|
||||
import { ConnectAIModal } from "./connect-ai-modal";
|
||||
import { ProjectSelector } from "./project-selector";
|
||||
import { useTour } from "./tour";
|
||||
import { AddMemoryExpandedView, AddMemoryView } from "./views/add-memory";
|
||||
import { IntegrationsView } from "./views/integrations";
|
||||
import { ProfileView } from "./views/profile";
|
||||
|
||||
const MCPIcon = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
|
|
@ -37,13 +37,13 @@ const MCPIcon = ({ className }: { className?: string }) => {
|
|||
<path d="M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z" />
|
||||
<path d="M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function Menu({ id }: { id?: string }) {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const openParam = searchParams.get("open")
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const openParam = searchParams.get("open");
|
||||
|
||||
// Valid view names that can be opened via URL parameter
|
||||
const validViews = [
|
||||
|
|
@ -52,52 +52,52 @@ function Menu({ id }: { id?: string }) {
|
|||
"projects",
|
||||
"profile",
|
||||
"integrations",
|
||||
] as const
|
||||
type ValidView = (typeof validViews)[number]
|
||||
] as const;
|
||||
type ValidView = (typeof validViews)[number];
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [expandedView, setExpandedView] = useState<
|
||||
"addUrl" | "mcp" | "projects" | "profile" | "integrations" | null
|
||||
>(null)
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
||||
const [isCollapsing, setIsCollapsing] = useState(false)
|
||||
const [showAddMemoryView, setShowAddMemoryView] = useState(false)
|
||||
const [showConnectAIModal, setShowConnectAIModal] = useState(false)
|
||||
const isMobile = useIsMobile()
|
||||
const { activePanel, setActivePanel } = useMobilePanel()
|
||||
const { setMenuExpanded } = useTour()
|
||||
const autumn = useCustomer()
|
||||
const { setIsOpen } = useChatOpen()
|
||||
>(null);
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
const [isCollapsing, setIsCollapsing] = useState(false);
|
||||
const [showAddMemoryView, setShowAddMemoryView] = useState(false);
|
||||
const [showConnectAIModal, setShowConnectAIModal] = useState(false);
|
||||
const isMobile = useIsMobile();
|
||||
const { activePanel, setActivePanel } = useMobilePanel();
|
||||
const { setMenuExpanded } = useTour();
|
||||
const autumn = useCustomer();
|
||||
const { setIsOpen } = useChatOpen();
|
||||
|
||||
const { data: memoriesCheck } = fetchMemoriesFeature(autumn)
|
||||
const { data: memoriesCheck } = fetchMemoriesFeature(autumn);
|
||||
|
||||
const memoriesUsed = memoriesCheck?.usage ?? 0
|
||||
const memoriesLimit = memoriesCheck?.included_usage ?? 0
|
||||
const memoriesUsed = memoriesCheck?.usage ?? 0;
|
||||
const memoriesLimit = memoriesCheck?.included_usage ?? 0;
|
||||
|
||||
const { data: proCheck } = fetchConsumerProProduct(autumn)
|
||||
const { data: proCheck } = fetchConsumerProProduct(autumn);
|
||||
|
||||
useEffect(() => {
|
||||
if (memoriesCheck) {
|
||||
console.log({ memoriesCheck })
|
||||
console.log({ memoriesCheck });
|
||||
}
|
||||
|
||||
if (proCheck) {
|
||||
console.log({ proCheck })
|
||||
console.log({ proCheck });
|
||||
}
|
||||
}, [memoriesCheck, proCheck])
|
||||
}, [memoriesCheck, proCheck]);
|
||||
|
||||
// Function to clear the 'open' parameter from URL
|
||||
const clearOpenParam = useCallback(() => {
|
||||
const newSearchParams = new URLSearchParams(searchParams.toString())
|
||||
newSearchParams.delete("open")
|
||||
const newUrl = `${window.location.pathname}${newSearchParams.toString() ? `?${newSearchParams.toString()}` : ""}`
|
||||
router.replace(newUrl)
|
||||
}, [searchParams, router])
|
||||
const newSearchParams = new URLSearchParams(searchParams.toString());
|
||||
newSearchParams.delete("open");
|
||||
const newUrl = `${window.location.pathname}${newSearchParams.toString() ? `?${newSearchParams.toString()}` : ""}`;
|
||||
router.replace(newUrl);
|
||||
}, [searchParams, router]);
|
||||
|
||||
const isProUser = proCheck?.allowed ?? false
|
||||
const isProUser = proCheck?.allowed ?? false;
|
||||
|
||||
const shouldShowLimitWarning =
|
||||
!isProUser && memoriesUsed >= memoriesLimit * 0.8
|
||||
!isProUser && memoriesUsed >= memoriesLimit * 0.8;
|
||||
|
||||
// Map menu item keys to tour IDs
|
||||
const menuItemTourIds: Record<string, string> = {
|
||||
|
|
@ -105,7 +105,7 @@ function Menu({ id }: { id?: string }) {
|
|||
projects: TOUR_STEP_IDS.MENU_PROJECTS,
|
||||
mcp: TOUR_STEP_IDS.MENU_MCP,
|
||||
integrations: "", // No tour ID for integrations yet
|
||||
}
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
|
|
@ -138,70 +138,70 @@ function Menu({ id }: { id?: string }) {
|
|||
key: "profile" as const,
|
||||
disabled: false,
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
const handleMenuItemClick = (
|
||||
key: "chat" | "addUrl" | "mcp" | "projects" | "profile" | "integrations",
|
||||
) => {
|
||||
if (key === "chat") {
|
||||
setIsOpen(true)
|
||||
setIsMobileMenuOpen(false)
|
||||
setIsOpen(true);
|
||||
setIsMobileMenuOpen(false);
|
||||
if (isMobile) {
|
||||
setActivePanel("chat")
|
||||
setActivePanel("chat");
|
||||
}
|
||||
} else if (key === "mcp") {
|
||||
// Open ConnectAIModal directly for MCP
|
||||
setIsMobileMenuOpen(false)
|
||||
setExpandedView(null)
|
||||
setShowConnectAIModal(true)
|
||||
setIsMobileMenuOpen(false);
|
||||
setExpandedView(null);
|
||||
setShowConnectAIModal(true);
|
||||
} else {
|
||||
if (expandedView === key) {
|
||||
setIsCollapsing(true)
|
||||
setExpandedView(null)
|
||||
setIsCollapsing(true);
|
||||
setExpandedView(null);
|
||||
} else if (key === "addUrl") {
|
||||
setShowAddMemoryView(true)
|
||||
setExpandedView(null)
|
||||
setShowAddMemoryView(true);
|
||||
setExpandedView(null);
|
||||
} else {
|
||||
setExpandedView(key)
|
||||
setExpandedView(key);
|
||||
}
|
||||
if (isMobile) {
|
||||
setActivePanel("menu")
|
||||
setActivePanel("menu");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handle initial view opening based on URL parameter
|
||||
useEffect(() => {
|
||||
if (openParam) {
|
||||
if (openParam === "chat") {
|
||||
setIsOpen(true)
|
||||
setIsMobileMenuOpen(false)
|
||||
setIsOpen(true);
|
||||
setIsMobileMenuOpen(false);
|
||||
if (isMobile) {
|
||||
setActivePanel("chat")
|
||||
setActivePanel("chat");
|
||||
}
|
||||
} else if (openParam === "mcp") {
|
||||
// Open ConnectAIModal directly for MCP
|
||||
setIsMobileMenuOpen(false)
|
||||
setExpandedView(null)
|
||||
setShowConnectAIModal(true)
|
||||
setIsMobileMenuOpen(false);
|
||||
setExpandedView(null);
|
||||
setShowConnectAIModal(true);
|
||||
} else if (openParam === "addUrl") {
|
||||
setShowAddMemoryView(true)
|
||||
setExpandedView(null)
|
||||
setShowAddMemoryView(true);
|
||||
setExpandedView(null);
|
||||
if (isMobile) {
|
||||
setIsMobileMenuOpen(true)
|
||||
setActivePanel("menu")
|
||||
setIsMobileMenuOpen(true);
|
||||
setActivePanel("menu");
|
||||
}
|
||||
} else if (validViews.includes(openParam as ValidView)) {
|
||||
// For other valid views like "profile", "integrations"
|
||||
setExpandedView(openParam as ValidView)
|
||||
setExpandedView(openParam as ValidView);
|
||||
if (isMobile) {
|
||||
setIsMobileMenuOpen(true)
|
||||
setActivePanel("menu")
|
||||
setIsMobileMenuOpen(true);
|
||||
setActivePanel("menu");
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the parameter from URL after performing any action
|
||||
clearOpenParam()
|
||||
clearOpenParam();
|
||||
}
|
||||
}, [
|
||||
openParam,
|
||||
|
|
@ -210,30 +210,31 @@ function Menu({ id }: { id?: string }) {
|
|||
setActivePanel,
|
||||
validViews,
|
||||
clearOpenParam,
|
||||
])
|
||||
]);
|
||||
|
||||
// Watch for active panel changes on mobile
|
||||
useEffect(() => {
|
||||
if (isMobile && activePanel !== "menu" && activePanel !== null) {
|
||||
// Another panel became active, close the menu
|
||||
setIsMobileMenuOpen(false)
|
||||
setExpandedView(null)
|
||||
setIsMobileMenuOpen(false);
|
||||
setExpandedView(null);
|
||||
}
|
||||
}, [isMobile, activePanel])
|
||||
}, [isMobile, activePanel]);
|
||||
|
||||
// Notify tour provider about expansion state changes
|
||||
useEffect(() => {
|
||||
const isExpanded = isMobile
|
||||
? isMobileMenuOpen || !!expandedView
|
||||
: isHovered || !!expandedView
|
||||
setMenuExpanded(isExpanded)
|
||||
}, [isMobile, isMobileMenuOpen, isHovered, expandedView, setMenuExpanded])
|
||||
: isHovered || !!expandedView;
|
||||
setMenuExpanded(isExpanded);
|
||||
}, [isMobile, isMobileMenuOpen, isHovered, expandedView, setMenuExpanded]);
|
||||
|
||||
// Calculate width based on state
|
||||
const menuWidth = expandedView || isCollapsing ? 600 : isHovered ? 160 : 56
|
||||
const menuWidth = expandedView || isCollapsing ? 600 : isHovered ? 160 : 56;
|
||||
|
||||
// Dynamic z-index for mobile based on active panel
|
||||
const mobileZIndex = isMobile && activePanel === "menu" ? "z-[70]" : "z-[100]"
|
||||
const mobileZIndex =
|
||||
isMobile && activePanel === "menu" ? "z-[70]" : "z-[100]";
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -442,8 +443,8 @@ function Menu({ id }: { id?: string }) {
|
|||
<Button
|
||||
className="text-white/70 hover:text-white transition-colors duration-200"
|
||||
onClick={() => {
|
||||
setIsCollapsing(true)
|
||||
setExpandedView(null)
|
||||
setIsCollapsing(true);
|
||||
setExpandedView(null);
|
||||
}}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
|
|
@ -479,14 +480,14 @@ function Menu({ id }: { id?: string }) {
|
|||
{/* Mobile Menu with Vaul Drawer */}
|
||||
{isMobile && (
|
||||
<Drawer.Root
|
||||
open={isMobileMenuOpen || !!expandedView}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setIsMobileMenuOpen(false)
|
||||
setExpandedView(null)
|
||||
setActivePanel(null)
|
||||
setIsMobileMenuOpen(false);
|
||||
setExpandedView(null);
|
||||
setActivePanel(null);
|
||||
}
|
||||
}}
|
||||
open={isMobileMenuOpen || !!expandedView}
|
||||
>
|
||||
{/* Menu Trigger Button */}
|
||||
{!isMobileMenuOpen && !expandedView && (
|
||||
|
|
@ -497,8 +498,8 @@ function Menu({ id }: { id?: string }) {
|
|||
className="w-14 h-14 flex items-center justify-center text-white rounded-full shadow-2xl"
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
onClick={() => {
|
||||
setIsMobileMenuOpen(true)
|
||||
setActivePanel("menu")
|
||||
setIsMobileMenuOpen(true);
|
||||
setActivePanel("menu");
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
|
|
@ -594,13 +595,13 @@ function Menu({ id }: { id?: string }) {
|
|||
initial={{ opacity: 0, y: 10 }}
|
||||
layout
|
||||
onClick={() => {
|
||||
handleMenuItemClick(item.key)
|
||||
handleMenuItemClick(item.key);
|
||||
if (
|
||||
item.key !== "mcp" &&
|
||||
item.key !== "profile" &&
|
||||
item.key !== "integrations"
|
||||
) {
|
||||
setIsMobileMenuOpen(false)
|
||||
setIsMobileMenuOpen(false);
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
|
|
@ -664,8 +665,8 @@ function Menu({ id }: { id?: string }) {
|
|||
<Button
|
||||
className="text-white/70 hover:text-white transition-colors duration-200"
|
||||
onClick={() => {
|
||||
setIsCollapsing(true)
|
||||
setExpandedView(null)
|
||||
setIsCollapsing(true);
|
||||
setExpandedView(null);
|
||||
}}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
|
|
@ -707,7 +708,7 @@ function Menu({ id }: { id?: string }) {
|
|||
<Button className="hidden">Connect AI Assistant</Button>
|
||||
</ConnectAIModal>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default Menu
|
||||
export default Menu;
|
||||
|
|
|
|||
|
|
@ -150,18 +150,18 @@ export function ProjectSelector() {
|
|||
{isOpen && (
|
||||
<>
|
||||
<motion.div
|
||||
className="fixed inset-0 z-40"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="fixed inset-0 z-40"
|
||||
exit={{ opacity: 0 }}
|
||||
initial={{ opacity: 0 }}
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
className="absolute top-full left-0 mt-1 w-56 bg-[#0f1419] backdrop-blur-xl border border-white/10 rounded-md shadow-xl z-50 overflow-hidden"
|
||||
initial={{ opacity: 0, y: -5, scale: 0.98 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
className="absolute top-full left-0 mt-1 w-56 bg-[#0f1419] backdrop-blur-xl border border-white/10 rounded-md shadow-xl z-50 overflow-hidden"
|
||||
exit={{ opacity: 0, y: -5, scale: 0.98 }}
|
||||
initial={{ opacity: 0, y: -5, scale: 0.98 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
<div className="p-1.5 max-h-64 overflow-y-auto">
|
||||
|
|
@ -187,14 +187,14 @@ export function ProjectSelector() {
|
|||
.filter((p: Project) => p.containerTag !== DEFAULT_PROJECT_ID)
|
||||
.map((project: Project, index: number) => (
|
||||
<motion.div
|
||||
key={project.id}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className={`flex items-center justify-between p-2 rounded-md transition-colors group ${
|
||||
selectedProject === project.containerTag
|
||||
? "bg-white/15"
|
||||
: "hover:bg-white/8"
|
||||
}`}
|
||||
initial={{ opacity: 0, x: -5 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
key={project.id}
|
||||
transition={{ delay: index * 0.03 }}
|
||||
>
|
||||
<div
|
||||
|
|
@ -278,12 +278,12 @@ export function ProjectSelector() {
|
|||
))}
|
||||
|
||||
<motion.div
|
||||
className="flex items-center gap-2 p-2 rounded-md hover:bg-white/8 transition-colors cursor-pointer border-t border-white/10 mt-1"
|
||||
onClick={handleCreateNewProject}
|
||||
whileHover={{ x: 1 }}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="flex items-center gap-2 p-2 rounded-md hover:bg-white/8 transition-colors cursor-pointer border-t border-white/10 mt-1"
|
||||
initial={{ opacity: 0 }}
|
||||
onClick={handleCreateNewProject}
|
||||
transition={{ delay: (projects.length + 1) * 0.03 }}
|
||||
whileHover={{ x: 1 }}
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5 text-white/70" />
|
||||
<span className="text-xs font-medium text-white/80">
|
||||
|
|
@ -297,8 +297,8 @@ export function ProjectSelector() {
|
|||
</AnimatePresence>
|
||||
|
||||
<CreateProjectDialog
|
||||
open={showCreateDialog}
|
||||
onOpenChange={setShowCreateDialog}
|
||||
open={showCreateDialog}
|
||||
/>
|
||||
|
||||
{/* Delete Project Dialog */}
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ export function ReferralUpgradeModal({
|
|||
|
||||
if (user?.isAnonymous) {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<Dialog onOpenChange={onClose} open={isOpen}>
|
||||
<DialogContent className="sm:max-w-md bg-[#0f1419] backdrop-blur-xl border-white/10 text-white">
|
||||
<motion.div
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
|
|
@ -136,7 +136,7 @@ export function ReferralUpgradeModal({
|
|||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<Dialog onOpenChange={onClose} open={isOpen}>
|
||||
<DialogContent className="sm:max-w-lg bg-[#0f1419] backdrop-blur-xl border-white/10 text-white">
|
||||
<motion.div
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
|
|
@ -177,16 +177,16 @@ export function ReferralUpgradeModal({
|
|||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<Tabs defaultValue="refer" className="w-full">
|
||||
<Tabs className="w-full" defaultValue="refer">
|
||||
<TabsList className="grid w-full grid-cols-2 bg-white/5">
|
||||
<TabsTrigger value="refer" className="flex items-center gap-2">
|
||||
<TabsTrigger className="flex items-center gap-2" value="refer">
|
||||
<Users className="w-4 h-4" />
|
||||
Refer Friends
|
||||
</TabsTrigger>
|
||||
{!isPro && (
|
||||
<TabsTrigger
|
||||
value="upgrade"
|
||||
className="flex items-center gap-2"
|
||||
value="upgrade"
|
||||
>
|
||||
<CreditCard className="w-4 h-4" />
|
||||
Upgrade Plan
|
||||
|
|
@ -194,7 +194,7 @@ export function ReferralUpgradeModal({
|
|||
)}
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="refer" className="space-y-4 mt-6">
|
||||
<TabsContent className="space-y-4 mt-6" value="refer">
|
||||
<div className="text-center">
|
||||
<Gift className="w-12 h-12 text-blue-400 mx-auto mb-3" />
|
||||
<HeadingH3Bold className="text-white mb-2">
|
||||
|
|
@ -241,7 +241,7 @@ export function ReferralUpgradeModal({
|
|||
</TabsContent>
|
||||
|
||||
{!isPro && (
|
||||
<TabsContent value="upgrade" className="space-y-4 mt-6">
|
||||
<TabsContent className="space-y-4 mt-6" value="upgrade">
|
||||
<div className="text-center">
|
||||
<CreditCard className="w-12 h-12 text-purple-400 mx-auto mb-3" />
|
||||
<HeadingH3Bold className="text-white mb-2">
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ function TextShimmerComponent({
|
|||
|
||||
return (
|
||||
<MotionComponent
|
||||
animate={{ backgroundPosition: "0% center" }}
|
||||
className={cn(
|
||||
"relative inline-block bg-[length:250%_100%,auto] bg-clip-text",
|
||||
"text-transparent [--base-color:#a1a1aa] [--base-gradient-color:#000]",
|
||||
|
|
@ -36,18 +37,18 @@ function TextShimmerComponent({
|
|||
className,
|
||||
)}
|
||||
initial={{ backgroundPosition: "100% center" }}
|
||||
animate={{ backgroundPosition: "0% center" }}
|
||||
transition={{
|
||||
repeat: Infinity,
|
||||
duration,
|
||||
ease: "linear",
|
||||
}}
|
||||
style={
|
||||
{
|
||||
"--spread": `${dynamicSpread}px`,
|
||||
backgroundImage: `var(--bg), linear-gradient(var(--base-color), var(--base-color))`,
|
||||
backgroundImage:
|
||||
"var(--bg), linear-gradient(var(--base-color), var(--base-color))",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
transition={{
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
duration,
|
||||
ease: "linear",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</MotionComponent>
|
||||
|
|
|
|||
|
|
@ -1,67 +1,67 @@
|
|||
import { Button } from '@repo/ui/components/button';
|
||||
import { Loader2, type LucideIcon } from 'lucide-react';
|
||||
import { motion } from 'motion/react';
|
||||
import { Button } from "@repo/ui/components/button";
|
||||
import { Loader2, type LucideIcon } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
interface ActionButtonsProps {
|
||||
onCancel: () => void;
|
||||
onSubmit?: () => void;
|
||||
submitText: string;
|
||||
submitIcon?: LucideIcon;
|
||||
isSubmitting?: boolean;
|
||||
isSubmitDisabled?: boolean;
|
||||
submitType?: 'button' | 'submit';
|
||||
className?: string;
|
||||
onCancel: () => void;
|
||||
onSubmit?: () => void;
|
||||
submitText: string;
|
||||
submitIcon?: LucideIcon;
|
||||
isSubmitting?: boolean;
|
||||
isSubmitDisabled?: boolean;
|
||||
submitType?: "button" | "submit";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ActionButtons({
|
||||
onCancel,
|
||||
onSubmit,
|
||||
submitText,
|
||||
submitIcon: SubmitIcon,
|
||||
isSubmitting = false,
|
||||
isSubmitDisabled = false,
|
||||
submitType = 'submit',
|
||||
className = '',
|
||||
onCancel,
|
||||
onSubmit,
|
||||
submitText,
|
||||
submitIcon: SubmitIcon,
|
||||
isSubmitting = false,
|
||||
isSubmitDisabled = false,
|
||||
submitType = "submit",
|
||||
className = "",
|
||||
}: ActionButtonsProps) {
|
||||
return (
|
||||
<div className={`flex gap-3 order-1 sm:order-2 justify-end ${className}`}>
|
||||
<Button
|
||||
className="hover:bg-white/10 text-white border-none flex-1 sm:flex-initial"
|
||||
onClick={onCancel}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
return (
|
||||
<div className={`flex gap-3 order-1 sm:order-2 justify-end ${className}`}>
|
||||
<Button
|
||||
className="hover:bg-white/10 text-white border-none flex-1 sm:flex-initial"
|
||||
onClick={onCancel}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="flex-1 sm:flex-initial"
|
||||
>
|
||||
<Button
|
||||
className="bg-white/10 hover:bg-white/20 text-white border-white/20 w-full"
|
||||
disabled={isSubmitting || isSubmitDisabled}
|
||||
onClick={submitType === 'button' ? onSubmit : undefined}
|
||||
type={submitType}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
{submitText.includes('Add')
|
||||
? 'Adding...'
|
||||
: submitText.includes('Upload')
|
||||
? 'Uploading...'
|
||||
: 'Processing...'}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{SubmitIcon && <SubmitIcon className="h-4 w-4 mr-2" />}
|
||||
{submitText}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
<motion.div
|
||||
className="flex-1 sm:flex-initial"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Button
|
||||
className="bg-white/10 hover:bg-white/20 text-white border-white/20 w-full"
|
||||
disabled={isSubmitting || isSubmitDisabled}
|
||||
onClick={submitType === "button" ? onSubmit : undefined}
|
||||
type={submitType}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
{submitText.includes("Add")
|
||||
? "Adding..."
|
||||
: submitText.includes("Upload")
|
||||
? "Uploading..."
|
||||
: "Processing..."}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{SubmitIcon && <SubmitIcon className="h-4 w-4 mr-2" />}
|
||||
{submitText}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { $fetch } from "@lib/api";
|
||||
import { $fetch } from "@lib/api"
|
||||
import {
|
||||
fetchConsumerProProduct,
|
||||
fetchMemoriesFeature,
|
||||
} from "@repo/lib/queries";
|
||||
import { Button } from "@repo/ui/components/button";
|
||||
} from "@repo/lib/queries"
|
||||
import { Button } from "@repo/ui/components/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -11,18 +11,19 @@ import {
|
|||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@repo/ui/components/dialog";
|
||||
import { Input } from "@repo/ui/components/input";
|
||||
import { Label } from "@repo/ui/components/label";
|
||||
import { Textarea } from "@repo/ui/components/textarea";
|
||||
import { useForm } from "@tanstack/react-form";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
} from "@repo/ui/components/dialog"
|
||||
import { Input } from "@repo/ui/components/input"
|
||||
import { Label } from "@repo/ui/components/label"
|
||||
import { Textarea } from "@repo/ui/components/textarea"
|
||||
import type { GetMemoryResponseSchema } from "@repo/validation/api"
|
||||
import { useForm } from "@tanstack/react-form"
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import {
|
||||
Dropzone,
|
||||
DropzoneContent,
|
||||
DropzoneEmptyState,
|
||||
} from "@ui/components/shadcn-io/dropzone";
|
||||
import { useCustomer } from "autumn-js/react";
|
||||
} from "@ui/components/shadcn-io/dropzone"
|
||||
import { useCustomer } from "autumn-js/react"
|
||||
import {
|
||||
Brain,
|
||||
FileIcon,
|
||||
|
|
@ -31,19 +32,19 @@ import {
|
|||
PlugIcon,
|
||||
Plus,
|
||||
UploadIcon,
|
||||
} from "lucide-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { analytics } from "@/lib/analytics";
|
||||
import { useProject } from "@/stores";
|
||||
import { ConnectionsTabContent } from "../connections-tab-content";
|
||||
import { ActionButtons } from "./action-buttons";
|
||||
import { MemoryUsageRing } from "./memory-usage-ring";
|
||||
import { ProjectSelection } from "./project-selection";
|
||||
import { TabButton } from "./tab-button";
|
||||
} from "lucide-react"
|
||||
import { AnimatePresence, motion } from "motion/react"
|
||||
import dynamic from "next/dynamic"
|
||||
import { useEffect, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { z } from "zod"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
import { useProject } from "@/stores"
|
||||
import { ConnectionsTabContent } from "../connections-tab-content"
|
||||
import { ActionButtons } from "./action-buttons"
|
||||
import { MemoryUsageRing } from "./memory-usage-ring"
|
||||
import { ProjectSelection } from "./project-selection"
|
||||
import { TabButton } from "./tab-button"
|
||||
|
||||
const TextEditor = dynamic(
|
||||
() => import("./text-editor").then((mod) => ({ default: mod.TextEditor })),
|
||||
|
|
@ -64,91 +65,100 @@ const TextEditor = dynamic(
|
|||
),
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
// Simple function to extract plain text title from HTML content
|
||||
const getPlainTextTitle = (htmlContent: string) => {
|
||||
const temp = document.createElement("div");
|
||||
temp.innerHTML = htmlContent;
|
||||
const plainText = temp.textContent || temp.innerText || htmlContent;
|
||||
const firstLine = plainText.split("\n")[0].trim();
|
||||
const temp = document.createElement("div")
|
||||
temp.innerHTML = htmlContent
|
||||
const plainText = temp.textContent || temp.innerText || htmlContent
|
||||
|
||||
if (!plainText) {
|
||||
return "Untitled"
|
||||
}
|
||||
const firstLine = plainText.split("\n")[0]?.trim()
|
||||
|
||||
if (!firstLine) {
|
||||
return plainText.substring(0, 100)
|
||||
}
|
||||
|
||||
return firstLine.length > 0
|
||||
? firstLine.substring(0, 100)
|
||||
: plainText.substring(0, 100);
|
||||
};
|
||||
: plainText.substring(0, 100)
|
||||
}
|
||||
|
||||
export function AddMemoryView({
|
||||
onClose,
|
||||
initialTab = "note",
|
||||
}: {
|
||||
onClose?: () => void;
|
||||
initialTab?: "note" | "link" | "file" | "connect";
|
||||
onClose?: () => void
|
||||
initialTab?: "note" | "link" | "file" | "connect"
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const { selectedProject, setSelectedProject } = useProject();
|
||||
const [showAddDialog, setShowAddDialog] = useState(true);
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
||||
const queryClient = useQueryClient()
|
||||
const { selectedProject, setSelectedProject } = useProject()
|
||||
const [showAddDialog, setShowAddDialog] = useState(true)
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([])
|
||||
const [activeTab, setActiveTab] = useState<
|
||||
"note" | "link" | "file" | "connect"
|
||||
>(initialTab);
|
||||
const autumn = useCustomer();
|
||||
const [showCreateProjectDialog, setShowCreateProjectDialog] = useState(false);
|
||||
const [newProjectName, setNewProjectName] = useState("");
|
||||
>(initialTab)
|
||||
const autumn = useCustomer()
|
||||
const [showCreateProjectDialog, setShowCreateProjectDialog] = useState(false)
|
||||
const [newProjectName, setNewProjectName] = useState("")
|
||||
|
||||
// Check memory limits
|
||||
const { data: memoriesCheck } = fetchMemoriesFeature(autumn);
|
||||
const { data: memoriesCheck } = fetchMemoriesFeature(autumn)
|
||||
|
||||
const memoriesUsed = memoriesCheck?.usage ?? 0;
|
||||
const memoriesLimit = memoriesCheck?.included_usage ?? 0;
|
||||
const memoriesUsed = memoriesCheck?.usage ?? 0
|
||||
const memoriesLimit = memoriesCheck?.included_usage ?? 0
|
||||
|
||||
// Fetch projects for the dropdown
|
||||
const { data: projects = [], isLoading: isLoadingProjects } = useQuery({
|
||||
queryKey: ["projects"],
|
||||
queryFn: async () => {
|
||||
const response = await $fetch("@get/projects");
|
||||
const response = await $fetch("@get/projects")
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error?.message || "Failed to load projects");
|
||||
throw new Error(response.error?.message || "Failed to load projects")
|
||||
}
|
||||
|
||||
return response.data?.projects || [];
|
||||
return response.data?.projects || []
|
||||
},
|
||||
staleTime: 30 * 1000,
|
||||
});
|
||||
})
|
||||
|
||||
// Create project mutation
|
||||
const createProjectMutation = useMutation({
|
||||
mutationFn: async (name: string) => {
|
||||
const response = await $fetch("@post/projects", {
|
||||
body: { name },
|
||||
});
|
||||
})
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error?.message || "Failed to create project");
|
||||
throw new Error(response.error?.message || "Failed to create project")
|
||||
}
|
||||
|
||||
return response.data;
|
||||
return response.data
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
analytics.projectCreated();
|
||||
toast.success("Project created successfully!");
|
||||
setShowCreateProjectDialog(false);
|
||||
setNewProjectName("");
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||
analytics.projectCreated()
|
||||
toast.success("Project created successfully!")
|
||||
setShowCreateProjectDialog(false)
|
||||
setNewProjectName("")
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] })
|
||||
// Set the newly created project as selected
|
||||
if (data?.containerTag) {
|
||||
setSelectedProject(data.containerTag);
|
||||
setSelectedProject(data.containerTag)
|
||||
// Update form values
|
||||
addContentForm.setFieldValue("project", data.containerTag);
|
||||
fileUploadForm.setFieldValue("project", data.containerTag);
|
||||
addContentForm.setFieldValue("project", data.containerTag)
|
||||
fileUploadForm.setFieldValue("project", data.containerTag)
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Failed to create project", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
})
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const addContentForm = useForm({
|
||||
defaultValues: {
|
||||
|
|
@ -160,8 +170,8 @@ export function AddMemoryView({
|
|||
content: value.content,
|
||||
project: value.project,
|
||||
contentType: activeTab as "note" | "link",
|
||||
});
|
||||
formApi.reset();
|
||||
})
|
||||
formApi.reset()
|
||||
},
|
||||
validators: {
|
||||
onChange: z.object({
|
||||
|
|
@ -169,19 +179,19 @@ export function AddMemoryView({
|
|||
project: z.string(),
|
||||
}),
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
// Re-validate content field when tab changes between note/link
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: It is what it is
|
||||
useEffect(() => {
|
||||
// Trigger validation of the content field when switching between note/link
|
||||
if (activeTab === "note" || activeTab === "link") {
|
||||
const currentValue = addContentForm.getFieldValue("content");
|
||||
const currentValue = addContentForm.getFieldValue("content")
|
||||
if (currentValue) {
|
||||
addContentForm.validateField("content", "change");
|
||||
addContentForm.validateField("content", "change")
|
||||
}
|
||||
}
|
||||
}, [activeTab]);
|
||||
}, [activeTab])
|
||||
|
||||
// Form for file upload metadata
|
||||
const fileUploadForm = useForm({
|
||||
|
|
@ -192,8 +202,8 @@ export function AddMemoryView({
|
|||
},
|
||||
onSubmit: async ({ value, formApi }) => {
|
||||
if (selectedFiles.length === 0) {
|
||||
toast.error("Please select a file to upload");
|
||||
return;
|
||||
toast.error("Please select a file to upload")
|
||||
return
|
||||
}
|
||||
|
||||
for (const file of selectedFiles) {
|
||||
|
|
@ -202,13 +212,13 @@ export function AddMemoryView({
|
|||
title: value.title || undefined,
|
||||
description: value.description || undefined,
|
||||
project: value.project,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
formApi.reset();
|
||||
setSelectedFiles([]);
|
||||
formApi.reset()
|
||||
setSelectedFiles([])
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const addContentMutation = useMutation({
|
||||
mutationFn: async ({
|
||||
|
|
@ -216,11 +226,11 @@ export function AddMemoryView({
|
|||
project,
|
||||
contentType,
|
||||
}: {
|
||||
content: string;
|
||||
project: string;
|
||||
contentType: "note" | "link";
|
||||
content: string
|
||||
project: string
|
||||
contentType: "note" | "link"
|
||||
}) => {
|
||||
console.log("📤 Creating memory...");
|
||||
console.log("📤 Creating memory...")
|
||||
|
||||
const response = await $fetch("@post/documents", {
|
||||
body: {
|
||||
|
|
@ -230,34 +240,34 @@ export function AddMemoryView({
|
|||
sm_source: "consumer",
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(
|
||||
response.error?.message || `Failed to add ${contentType}`,
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
console.log("✅ Memory created:", response.data);
|
||||
return response.data;
|
||||
console.log("✅ Memory created:", response.data)
|
||||
return response.data
|
||||
},
|
||||
onMutate: async ({ content, project, contentType }) => {
|
||||
console.log("🚀 OPTIMISTIC UPDATE: Starting for", contentType);
|
||||
console.log("🚀 OPTIMISTIC UPDATE: Starting for", contentType)
|
||||
|
||||
// Cancel queries to prevent conflicts
|
||||
await queryClient.cancelQueries({
|
||||
queryKey: ["documents-with-memories", project],
|
||||
});
|
||||
console.log("📞 QUERIES CANCELLED for project:", project);
|
||||
})
|
||||
console.log("📞 QUERIES CANCELLED for project:", project)
|
||||
|
||||
// Get previous data for rollback
|
||||
const previousMemories = queryClient.getQueryData([
|
||||
"documents-with-memories",
|
||||
project,
|
||||
]);
|
||||
])
|
||||
|
||||
// Create optimistic memory with proper title
|
||||
const tempId = `temp-${Date.now()}`;
|
||||
const tempId = `temp-${Date.now()}`
|
||||
const optimisticMemory = {
|
||||
id: tempId,
|
||||
content: contentType === "link" ? "" : content,
|
||||
|
|
@ -270,9 +280,9 @@ export function AddMemoryView({
|
|||
updatedAt: new Date().toISOString(),
|
||||
memoryEntries: [],
|
||||
isOptimistic: true,
|
||||
};
|
||||
}
|
||||
|
||||
console.log("🎯 Adding optimistic memory:", optimisticMemory);
|
||||
console.log("🎯 Adding optimistic memory:", optimisticMemory)
|
||||
|
||||
// Add to cache optimistically
|
||||
queryClient.setQueryData(
|
||||
|
|
@ -280,19 +290,19 @@ export function AddMemoryView({
|
|||
(old: any) => {
|
||||
if (old?.pages) {
|
||||
// Infinite query structure
|
||||
const newPages = [...old.pages];
|
||||
const newPages = [...old.pages]
|
||||
if (newPages.length > 0) {
|
||||
newPages[0] = {
|
||||
...newPages[0],
|
||||
documents: [optimisticMemory, ...(newPages[0].documents || [])],
|
||||
};
|
||||
}
|
||||
} else {
|
||||
newPages.push({
|
||||
documents: [optimisticMemory],
|
||||
pagination: { currentPage: 1, totalPages: 1, totalCount: 1 },
|
||||
});
|
||||
})
|
||||
}
|
||||
return { ...old, pages: newPages };
|
||||
return { ...old, pages: newPages }
|
||||
}
|
||||
|
||||
// Regular query structure
|
||||
|
|
@ -302,44 +312,46 @@ export function AddMemoryView({
|
|||
documents: [optimisticMemory, ...(old.documents || [])],
|
||||
totalCount: (old.totalCount || 0) + 1,
|
||||
}
|
||||
: { documents: [optimisticMemory], totalCount: 1 };
|
||||
: { documents: [optimisticMemory], totalCount: 1 }
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
return { previousMemories, optimisticId: tempId };
|
||||
return { previousMemories, optimisticId: tempId }
|
||||
},
|
||||
onError: (error, variables, context) => {
|
||||
console.log("❌ Mutation failed, rolling back");
|
||||
console.log("❌ Mutation failed, rolling back")
|
||||
if (context?.previousMemories) {
|
||||
queryClient.setQueryData(
|
||||
["documents-with-memories", variables.project],
|
||||
context.previousMemories,
|
||||
);
|
||||
)
|
||||
}
|
||||
toast.error(`Failed to add ${variables.contentType}`);
|
||||
toast.error(`Failed to add ${variables.contentType}`)
|
||||
},
|
||||
onSuccess: (data, variables, context) => {
|
||||
console.log(
|
||||
"✅ Mutation succeeded, starting simple polling for memory:",
|
||||
data.id,
|
||||
);
|
||||
)
|
||||
analytics.memoryAdded({
|
||||
type: variables.contentType === "link" ? "link" : "note",
|
||||
project_id: variables.project,
|
||||
content_length: variables.content.length,
|
||||
});
|
||||
})
|
||||
|
||||
toast.success(
|
||||
`${variables.contentType === "link" ? "Link" : "Note"} added successfully!`,
|
||||
);
|
||||
setShowAddDialog(false);
|
||||
onClose?.();
|
||||
)
|
||||
setShowAddDialog(false)
|
||||
onClose?.()
|
||||
|
||||
// Simple polling to replace optimistic update when ready
|
||||
const pollMemory = async () => {
|
||||
try {
|
||||
const memory = await $fetch(`@get/documents/${data.id}`);
|
||||
console.log("🔍 Polling memory:", memory.data);
|
||||
const memory = await $fetch<z.infer<typeof GetMemoryResponseSchema>>(
|
||||
`@get/documents/${data.id}`,
|
||||
)
|
||||
console.log("🔍 Polling memory:", memory.data)
|
||||
|
||||
if (memory.data && !memory.error) {
|
||||
// Check if memory has been processed (has memory entries, substantial content, and NOT untitled/processing)
|
||||
|
|
@ -347,17 +359,16 @@ export function AddMemoryView({
|
|||
memory.data.title &&
|
||||
!memory.data.title.toLowerCase().includes("untitled") &&
|
||||
!memory.data.title.toLowerCase().includes("processing") &&
|
||||
memory.data.title.length > 3;
|
||||
memory.data.title.length > 3
|
||||
|
||||
const isReady =
|
||||
memory.data.memoryEntries?.length > 0 ||
|
||||
(memory.data.content &&
|
||||
memory.data.content.length > 10 &&
|
||||
hasRealTitle);
|
||||
memory.data.content &&
|
||||
memory.data.content.length > 10 &&
|
||||
hasRealTitle
|
||||
|
||||
console.log("📊 Memory ready check:", {
|
||||
isReady,
|
||||
hasMemoryEntries: memory.data.memoryEntries?.length,
|
||||
hasMemoryEntries: 0,
|
||||
hasContent: memory.data.content?.length,
|
||||
title: memory.data.title,
|
||||
hasRealTitle,
|
||||
|
|
@ -367,10 +378,10 @@ export function AddMemoryView({
|
|||
titleNotProcessing:
|
||||
memory.data.title &&
|
||||
!memory.data.title.toLowerCase().includes("processing"),
|
||||
});
|
||||
})
|
||||
|
||||
if (isReady) {
|
||||
console.log("✅ Memory ready, replacing optimistic update");
|
||||
console.log("✅ Memory ready, replacing optimistic update")
|
||||
// Replace optimistic memory with real data
|
||||
queryClient.setQueryData(
|
||||
["documents-with-memories", variables.project],
|
||||
|
|
@ -380,48 +391,48 @@ export function AddMemoryView({
|
|||
...page,
|
||||
documents: page.documents.map((doc: any) => {
|
||||
if (doc.isOptimistic && doc.id.startsWith("temp-")) {
|
||||
return { ...memory.data, isOptimistic: false };
|
||||
return { ...memory.data, isOptimistic: false }
|
||||
}
|
||||
return doc;
|
||||
return doc
|
||||
}),
|
||||
}));
|
||||
return { ...old, pages: newPages };
|
||||
}))
|
||||
return { ...old, pages: newPages }
|
||||
}
|
||||
return old;
|
||||
return old
|
||||
},
|
||||
);
|
||||
return true; // Stop polling
|
||||
)
|
||||
return true // Stop polling
|
||||
}
|
||||
}
|
||||
return false; // Continue polling
|
||||
return false // Continue polling
|
||||
} catch (error) {
|
||||
console.error("❌ Error polling memory:", error);
|
||||
return false;
|
||||
console.error("❌ Error polling memory:", error)
|
||||
return false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Poll every 3 seconds for up to 30 attempts (90 seconds)
|
||||
let attempts = 0;
|
||||
const maxAttempts = 30;
|
||||
let attempts = 0
|
||||
const maxAttempts = 30
|
||||
|
||||
const poll = async () => {
|
||||
if (attempts >= maxAttempts) {
|
||||
console.log("⚠️ Polling stopped after max attempts");
|
||||
return;
|
||||
console.log("⚠️ Polling stopped after max attempts")
|
||||
return
|
||||
}
|
||||
|
||||
const isDone = await pollMemory();
|
||||
attempts++;
|
||||
const isDone = await pollMemory()
|
||||
attempts++
|
||||
|
||||
if (!isDone && attempts < maxAttempts) {
|
||||
setTimeout(poll, 3000);
|
||||
setTimeout(poll, 3000)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Start polling after 2 seconds
|
||||
setTimeout(poll, 2000);
|
||||
setTimeout(poll, 2000)
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const fileUploadMutation = useMutation({
|
||||
mutationFn: async ({
|
||||
|
|
@ -430,10 +441,10 @@ export function AddMemoryView({
|
|||
description,
|
||||
project,
|
||||
}: {
|
||||
file: File;
|
||||
title?: string;
|
||||
description?: string;
|
||||
project: string;
|
||||
file: File
|
||||
title?: string
|
||||
description?: string
|
||||
project: string
|
||||
}) => {
|
||||
// TEMPORARILY DISABLED: Limit check disabled
|
||||
// Check if user can add more memories
|
||||
|
|
@ -443,9 +454,9 @@ export function AddMemoryView({
|
|||
// );
|
||||
// }
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("containerTags", JSON.stringify([project]));
|
||||
const formData = new FormData()
|
||||
formData.append("file", file)
|
||||
formData.append("containerTags", JSON.stringify([project]))
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_BACKEND_URL}/v3/documents/file`,
|
||||
|
|
@ -454,14 +465,14 @@ export function AddMemoryView({
|
|||
body: formData,
|
||||
credentials: "include",
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || "Failed to upload file");
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || "Failed to upload file")
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const data = await response.json()
|
||||
|
||||
// If we have metadata, we can update the document after creation
|
||||
if (title || description) {
|
||||
|
|
@ -473,23 +484,23 @@ export function AddMemoryView({
|
|||
sm_source: "consumer", // Use "consumer" source to bypass limits
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
return data;
|
||||
return data
|
||||
},
|
||||
// Optimistic update
|
||||
onMutate: async ({ file, title, description, project }) => {
|
||||
// Cancel any outgoing refetches
|
||||
await queryClient.cancelQueries({
|
||||
queryKey: ["documents-with-memories", project],
|
||||
});
|
||||
})
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousMemories = queryClient.getQueryData([
|
||||
"documents-with-memories",
|
||||
project,
|
||||
]);
|
||||
])
|
||||
|
||||
// Create optimistic memory for the file
|
||||
const optimisticMemory = {
|
||||
|
|
@ -509,7 +520,7 @@ export function AddMemoryView({
|
|||
mimeType: file.type,
|
||||
},
|
||||
memoryEntries: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Optimistically update to include the new memory
|
||||
queryClient.setQueryData(
|
||||
|
|
@ -518,41 +529,41 @@ export function AddMemoryView({
|
|||
// Handle infinite query structure
|
||||
if (old?.pages) {
|
||||
// This is an infinite query - add to the first page
|
||||
const newPages = [...old.pages];
|
||||
const newPages = [...old.pages]
|
||||
if (newPages.length > 0) {
|
||||
// Add to the first page
|
||||
const firstPage = { ...newPages[0] };
|
||||
const firstPage = { ...newPages[0] }
|
||||
firstPage.documents = [
|
||||
optimisticMemory,
|
||||
...(firstPage.documents || []),
|
||||
];
|
||||
newPages[0] = firstPage;
|
||||
]
|
||||
newPages[0] = firstPage
|
||||
} else {
|
||||
// No pages yet, create the first page
|
||||
newPages.push({
|
||||
documents: [optimisticMemory],
|
||||
pagination: { currentPage: 1, totalPages: 1, totalCount: 1 },
|
||||
totalCount: 1,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...old,
|
||||
pages: newPages,
|
||||
};
|
||||
}
|
||||
}
|
||||
// Fallback for regular query structure
|
||||
if (!old) return { documents: [optimisticMemory], totalCount: 1 };
|
||||
if (!old) return { documents: [optimisticMemory], totalCount: 1 }
|
||||
return {
|
||||
...old,
|
||||
documents: [optimisticMemory, ...(old.documents || [])],
|
||||
totalCount: (old.totalCount || 0) + 1,
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
// Return a context object with the snapshotted value
|
||||
return { previousMemories };
|
||||
return { previousMemories }
|
||||
},
|
||||
// If the mutation fails, roll back to the previous value
|
||||
onError: (error, variables, context) => {
|
||||
|
|
@ -560,11 +571,11 @@ export function AddMemoryView({
|
|||
queryClient.setQueryData(
|
||||
["documents-with-memories", variables.project],
|
||||
context.previousMemories,
|
||||
);
|
||||
)
|
||||
}
|
||||
toast.error("Failed to upload file", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
})
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
analytics.memoryAdded({
|
||||
|
|
@ -572,18 +583,18 @@ export function AddMemoryView({
|
|||
project_id: variables.project,
|
||||
file_size: variables.file.size,
|
||||
file_type: variables.file.type,
|
||||
});
|
||||
})
|
||||
toast.success("File uploaded successfully!", {
|
||||
description: "Your file is being processed",
|
||||
});
|
||||
setShowAddDialog(false);
|
||||
onClose?.();
|
||||
})
|
||||
setShowAddDialog(false)
|
||||
onClose?.()
|
||||
},
|
||||
// Don't invalidate queries immediately - let optimistic updates work
|
||||
// onSettled: () => {
|
||||
// queryClient.invalidateQueries({ queryKey: ["documents-with-memories"] })
|
||||
// },
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
|
|
@ -591,8 +602,8 @@ export function AddMemoryView({
|
|||
<Dialog
|
||||
key="add-memory-dialog"
|
||||
onOpenChange={(open) => {
|
||||
setShowAddDialog(open);
|
||||
if (!open) onClose?.();
|
||||
setShowAddDialog(open)
|
||||
if (!open) onClose?.()
|
||||
}}
|
||||
open={showAddDialog}
|
||||
>
|
||||
|
|
@ -651,9 +662,9 @@ export function AddMemoryView({
|
|||
<div className="space-y-4">
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
addContentForm.handleSubmit();
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
addContentForm.handleSubmit()
|
||||
}}
|
||||
>
|
||||
<div className="grid gap-4">
|
||||
|
|
@ -669,9 +680,9 @@ export function AddMemoryView({
|
|||
validators={{
|
||||
onChange: ({ value }) => {
|
||||
if (!value || value.trim() === "") {
|
||||
return "Note is required";
|
||||
return "Note is required"
|
||||
}
|
||||
return undefined;
|
||||
return undefined
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
|
@ -753,9 +764,9 @@ export function AddMemoryView({
|
|||
isSubmitDisabled={!addContentForm.state.canSubmit}
|
||||
isSubmitting={addContentMutation.isPending}
|
||||
onCancel={() => {
|
||||
setShowAddDialog(false);
|
||||
onClose?.();
|
||||
addContentForm.reset();
|
||||
setShowAddDialog(false)
|
||||
onClose?.()
|
||||
addContentForm.reset()
|
||||
}}
|
||||
submitIcon={Plus}
|
||||
submitText="Add Note"
|
||||
|
|
@ -769,9 +780,9 @@ export function AddMemoryView({
|
|||
<div className="space-y-4">
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
addContentForm.handleSubmit();
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
addContentForm.handleSubmit()
|
||||
}}
|
||||
>
|
||||
<div className="grid gap-4">
|
||||
|
|
@ -793,13 +804,13 @@ export function AddMemoryView({
|
|||
validators={{
|
||||
onChange: ({ value }) => {
|
||||
if (!value || value.trim() === "") {
|
||||
return "Link is required";
|
||||
return "Link is required"
|
||||
}
|
||||
try {
|
||||
new URL(value);
|
||||
return undefined;
|
||||
new URL(value)
|
||||
return undefined
|
||||
} catch {
|
||||
return "Please enter a valid link";
|
||||
return "Please enter a valid link"
|
||||
}
|
||||
},
|
||||
}}
|
||||
|
|
@ -879,9 +890,9 @@ export function AddMemoryView({
|
|||
isSubmitDisabled={!addContentForm.state.canSubmit}
|
||||
isSubmitting={addContentMutation.isPending}
|
||||
onCancel={() => {
|
||||
setShowAddDialog(false);
|
||||
onClose?.();
|
||||
addContentForm.reset();
|
||||
setShowAddDialog(false)
|
||||
onClose?.()
|
||||
addContentForm.reset()
|
||||
}}
|
||||
submitIcon={Plus}
|
||||
submitText="Add Link"
|
||||
|
|
@ -895,9 +906,9 @@ export function AddMemoryView({
|
|||
<div className="space-y-4">
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
fileUploadForm.handleSubmit();
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
fileUploadForm.handleSubmit()
|
||||
}}
|
||||
>
|
||||
<div className="grid gap-4">
|
||||
|
|
@ -1031,10 +1042,10 @@ export function AddMemoryView({
|
|||
isSubmitDisabled={selectedFiles.length === 0}
|
||||
isSubmitting={fileUploadMutation.isPending}
|
||||
onCancel={() => {
|
||||
setShowAddDialog(false);
|
||||
onClose?.();
|
||||
fileUploadForm.reset();
|
||||
setSelectedFiles([]);
|
||||
setShowAddDialog(false)
|
||||
onClose?.()
|
||||
fileUploadForm.reset()
|
||||
setSelectedFiles([])
|
||||
}}
|
||||
submitIcon={UploadIcon}
|
||||
submitText="Upload File"
|
||||
|
|
@ -1102,8 +1113,8 @@ export function AddMemoryView({
|
|||
<Button
|
||||
className="bg-white/5 hover:bg-white/10 border-white/10 text-white w-full sm:w-auto"
|
||||
onClick={() => {
|
||||
setShowCreateProjectDialog(false);
|
||||
setNewProjectName("");
|
||||
setShowCreateProjectDialog(false)
|
||||
setNewProjectName("")
|
||||
}}
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
|
@ -1140,19 +1151,19 @@ export function AddMemoryView({
|
|||
</Dialog>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export function AddMemoryExpandedView() {
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const [showDialog, setShowDialog] = useState(false)
|
||||
const [selectedTab, setSelectedTab] = useState<
|
||||
"note" | "link" | "file" | "connect"
|
||||
>("note");
|
||||
>("note")
|
||||
|
||||
const handleOpenDialog = (tab: "note" | "link" | "file" | "connect") => {
|
||||
setSelectedTab(tab);
|
||||
setShowDialog(true);
|
||||
};
|
||||
setSelectedTab(tab)
|
||||
setShowDialog(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -1223,5 +1234,5 @@ export function AddMemoryExpandedView() {
|
|||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
interface MemoryUsageRingProps {
|
||||
memoriesUsed: number;
|
||||
memoriesLimit: number;
|
||||
className?: string;
|
||||
memoriesUsed: number
|
||||
memoriesLimit: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function MemoryUsageRing({
|
||||
|
|
@ -9,10 +9,10 @@ export function MemoryUsageRing({
|
|||
memoriesLimit,
|
||||
className = "",
|
||||
}: MemoryUsageRingProps) {
|
||||
const usagePercentage = memoriesUsed / memoriesLimit;
|
||||
const usagePercentage = memoriesUsed / memoriesLimit
|
||||
const strokeColor =
|
||||
memoriesUsed >= memoriesLimit * 0.8 ? "rgb(251 191 36)" : "rgb(34 197 94)";
|
||||
const circumference = 2 * Math.PI * 10;
|
||||
memoriesUsed >= memoriesLimit * 0.8 ? "rgb(251 191 36)" : "rgb(34 197 94)"
|
||||
const circumference = 2 * Math.PI * 10
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -20,27 +20,28 @@ export function MemoryUsageRing({
|
|||
title={`${memoriesUsed} of ${memoriesLimit} memories used`}
|
||||
>
|
||||
<svg className="w-6 h-6 transform -rotate-90" viewBox="0 0 24 24">
|
||||
<title>Memory Usage</title>
|
||||
{/* Background circle */}
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
fill="none"
|
||||
r="10"
|
||||
stroke="rgb(255 255 255 / 0.1)"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
/>
|
||||
{/* Progress circle */}
|
||||
<circle
|
||||
className="transition-all duration-300"
|
||||
cx="12"
|
||||
cy="12"
|
||||
fill="none"
|
||||
r="10"
|
||||
stroke={strokeColor}
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
strokeDasharray={`${circumference}`}
|
||||
strokeDashoffset={`${circumference * (1 - usagePercentage)}`}
|
||||
className="transition-all duration-300"
|
||||
strokeLinecap="round"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
|
|
@ -49,5 +50,5 @@ export function MemoryUsageRing({
|
|||
{memoriesUsed} / {memoriesLimit}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,94 +1,94 @@
|
|||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@repo/ui/components/select';
|
||||
import { Plus } from 'lucide-react';
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@repo/ui/components/select";
|
||||
import { Plus } from "lucide-react";
|
||||
|
||||
interface Project {
|
||||
id?: string;
|
||||
containerTag: string;
|
||||
name: string;
|
||||
id?: string;
|
||||
containerTag: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface ProjectSelectionProps {
|
||||
projects: Project[];
|
||||
selectedProject: string;
|
||||
onProjectChange: (value: string) => void;
|
||||
onCreateProject: () => void;
|
||||
disabled?: boolean;
|
||||
isLoading?: boolean;
|
||||
className?: string;
|
||||
id?: string;
|
||||
projects: Project[];
|
||||
selectedProject: string;
|
||||
onProjectChange: (value: string) => void;
|
||||
onCreateProject: () => void;
|
||||
disabled?: boolean;
|
||||
isLoading?: boolean;
|
||||
className?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export function ProjectSelection({
|
||||
projects,
|
||||
selectedProject,
|
||||
onProjectChange,
|
||||
onCreateProject,
|
||||
disabled = false,
|
||||
isLoading = false,
|
||||
className = '',
|
||||
id = 'project-select',
|
||||
projects,
|
||||
selectedProject,
|
||||
onProjectChange,
|
||||
onCreateProject,
|
||||
disabled = false,
|
||||
isLoading = false,
|
||||
className = "",
|
||||
id = "project-select",
|
||||
}: ProjectSelectionProps) {
|
||||
const handleValueChange = (value: string) => {
|
||||
if (value === 'create-new-project') {
|
||||
onCreateProject();
|
||||
} else {
|
||||
onProjectChange(value);
|
||||
}
|
||||
};
|
||||
const handleValueChange = (value: string) => {
|
||||
if (value === "create-new-project") {
|
||||
onCreateProject();
|
||||
} else {
|
||||
onProjectChange(value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
key={`${id}-${selectedProject}`}
|
||||
disabled={isLoading || disabled}
|
||||
onValueChange={handleValueChange}
|
||||
value={selectedProject}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={`bg-white/5 border-white/10 text-white ${className}`}
|
||||
id={id}
|
||||
>
|
||||
<SelectValue placeholder="Select a project" />
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
className="bg-black/90 backdrop-blur-xl border-white/10 z-[90]"
|
||||
position="popper"
|
||||
sideOffset={5}
|
||||
>
|
||||
<SelectItem
|
||||
className="text-white hover:bg-white/10"
|
||||
key="default"
|
||||
value="sm_project_default"
|
||||
>
|
||||
Default Project
|
||||
</SelectItem>
|
||||
{projects
|
||||
.filter((p) => p.containerTag !== 'sm_project_default' && p.id)
|
||||
.map((project) => (
|
||||
<SelectItem
|
||||
className="text-white hover:bg-white/10"
|
||||
key={project.id || project.containerTag}
|
||||
value={project.containerTag}
|
||||
>
|
||||
{project.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem
|
||||
className="text-white hover:bg-white/10 border-t border-white/10 mt-1"
|
||||
key="create-new"
|
||||
value="create-new-project"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>Create new project</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
return (
|
||||
<Select
|
||||
disabled={isLoading || disabled}
|
||||
key={`${id}-${selectedProject}`}
|
||||
onValueChange={handleValueChange}
|
||||
value={selectedProject}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={`bg-white/5 border-white/10 text-white ${className}`}
|
||||
id={id}
|
||||
>
|
||||
<SelectValue placeholder="Select a project" />
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
className="bg-black/90 backdrop-blur-xl border-white/10 z-[90]"
|
||||
position="popper"
|
||||
sideOffset={5}
|
||||
>
|
||||
<SelectItem
|
||||
className="text-white hover:bg-white/10"
|
||||
key="default"
|
||||
value="sm_project_default"
|
||||
>
|
||||
Default Project
|
||||
</SelectItem>
|
||||
{projects
|
||||
.filter((p) => p.containerTag !== "sm_project_default" && p.id)
|
||||
.map((project) => (
|
||||
<SelectItem
|
||||
className="text-white hover:bg-white/10"
|
||||
key={project.id || project.containerTag}
|
||||
value={project.containerTag}
|
||||
>
|
||||
{project.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem
|
||||
className="text-white hover:bg-white/10 border-t border-white/10 mt-1"
|
||||
key="create-new"
|
||||
value="create-new-project"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>Create new project</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,28 @@
|
|||
import type { LucideIcon } from 'lucide-react';
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
interface TabButtonProps {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export function TabButton({
|
||||
icon: Icon,
|
||||
label,
|
||||
isActive,
|
||||
onClick,
|
||||
icon: Icon,
|
||||
label,
|
||||
isActive,
|
||||
onClick,
|
||||
}: TabButtonProps) {
|
||||
return (
|
||||
<button
|
||||
className={`flex items-center gap-1.5 text-xs sm:text-xs px-4 sm:px-3 py-2 sm:py-1 h-8 sm:h-6 rounded-sm transition-colors whitespace-nowrap min-w-0 ${
|
||||
isActive ? 'bg-white/10' : 'hover:bg-white/5'
|
||||
}`}
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
>
|
||||
<Icon className="h-4 w-4 sm:h-3 sm:w-3" />
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
return (
|
||||
<button
|
||||
className={`flex items-center gap-1.5 text-xs sm:text-xs px-4 sm:px-3 py-2 sm:py-1 h-8 sm:h-6 rounded-sm transition-colors whitespace-nowrap min-w-0 ${
|
||||
isActive ? "bg-white/10" : "hover:bg-white/5"
|
||||
}`}
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
>
|
||||
<Icon className="h-4 w-4 sm:h-3 sm:w-3" />
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -399,8 +399,6 @@ export function TextEditor({
|
|||
title: string;
|
||||
}) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn(
|
||||
"h-8 w-8 !p-0 text-white/70 transition-all duration-200 rounded-sm",
|
||||
"hover:bg-white/15 hover:text-white hover:scale-105",
|
||||
|
|
@ -408,8 +406,10 @@ export function TextEditor({
|
|||
isActive && "bg-white/20 text-white",
|
||||
)}
|
||||
onMouseDown={onMouseDown}
|
||||
size="sm"
|
||||
title={title}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<Icon
|
||||
className={cn(
|
||||
|
|
@ -426,13 +426,20 @@ export function TextEditor({
|
|||
<Slate
|
||||
editor={editor}
|
||||
initialValue={editorValue}
|
||||
onValueChange={handleSlateChange}
|
||||
onSelectionChange={() => setSelection(editor.selection)}
|
||||
onValueChange={handleSlateChange}
|
||||
>
|
||||
<Editable
|
||||
className={cn(
|
||||
"outline-none w-full h-full text-white placeholder:text-white/50",
|
||||
disabled && "opacity-50 cursor-not-allowed",
|
||||
)}
|
||||
onBlur={onBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
readOnly={disabled}
|
||||
renderElement={renderElement}
|
||||
renderLeaf={renderLeaf}
|
||||
placeholder={placeholder}
|
||||
renderPlaceholder={({ children, attributes }) => {
|
||||
return (
|
||||
<div {...attributes} className="mt-2">
|
||||
|
|
@ -440,13 +447,6 @@ export function TextEditor({
|
|||
</div>
|
||||
);
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={onBlur}
|
||||
readOnly={disabled}
|
||||
className={cn(
|
||||
"outline-none w-full h-full text-white placeholder:text-white/50",
|
||||
disabled && "opacity-50 cursor-not-allowed",
|
||||
)}
|
||||
style={{
|
||||
minHeight: "11rem",
|
||||
maxHeight: "15rem",
|
||||
|
|
|
|||
|
|
@ -50,12 +50,12 @@ export function ChatRewrite() {
|
|||
{getCurrentChat()?.title ?? "New Chat"}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<Dialog onOpenChange={setIsDialogOpen} open={isDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => analytics.chatHistoryViewed()}
|
||||
size="icon"
|
||||
variant="outline"
|
||||
>
|
||||
<HistoryIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
|
|
@ -77,19 +77,19 @@ export function ChatRewrite() {
|
|||
const isActive = c.id === currentChatId;
|
||||
return (
|
||||
<div
|
||||
key={c.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
setCurrentChatId(c.id);
|
||||
setIsDialogOpen(false);
|
||||
}}
|
||||
aria-current={isActive ? "true" : undefined}
|
||||
className={cn(
|
||||
"flex items-center justify-between rounded-md px-3 py-2 outline-none",
|
||||
"transition-colors",
|
||||
isActive ? "bg-primary/10" : "hover:bg-muted",
|
||||
)}
|
||||
aria-current={isActive ? "true" : undefined}
|
||||
key={c.id}
|
||||
onClick={() => {
|
||||
setCurrentChatId(c.id);
|
||||
setIsDialogOpen(false);
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -107,14 +107,14 @@ export function ChatRewrite() {
|
|||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label="Delete conversation"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
analytics.chatDeleted();
|
||||
}}
|
||||
aria-label="Delete conversation"
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<Trash2 className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
|
|
@ -129,22 +129,22 @@ export function ChatRewrite() {
|
|||
</div>
|
||||
</ScrollArea>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="w-full border-dashed"
|
||||
onClick={handleNewChat}
|
||||
size="lg"
|
||||
variant="outline"
|
||||
>
|
||||
<Plus className="size-4 mr-1" /> New Conversation
|
||||
</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Button variant="outline" size="icon" onClick={handleNewChat}>
|
||||
<Button onClick={handleNewChat} size="icon" variant="outline">
|
||||
<Plus className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setIsOpen(false)}
|
||||
size="icon"
|
||||
variant="outline"
|
||||
>
|
||||
<X className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { $fetch } from "@lib/api"
|
||||
import { Button } from "@repo/ui/components/button"
|
||||
import { Skeleton } from "@repo/ui/components/skeleton"
|
||||
import type { ConnectionResponseSchema } from "@repo/validation/api"
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons"
|
||||
import { useCustomer } from "autumn-js/react"
|
||||
import { Trash2 } from "lucide-react"
|
||||
import { AnimatePresence, motion } from "motion/react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import type { z } from "zod"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
import { useProject } from "@/stores"
|
||||
import { $fetch } from "@lib/api";
|
||||
import { Button } from "@repo/ui/components/button";
|
||||
import { Skeleton } from "@repo/ui/components/skeleton";
|
||||
import type { ConnectionResponseSchema } from "@repo/validation/api";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons";
|
||||
import { useCustomer } from "autumn-js/react";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { z } from "zod";
|
||||
import { analytics } from "@/lib/analytics";
|
||||
import { useProject } from "@/stores";
|
||||
|
||||
// Define types
|
||||
type Connection = z.infer<typeof ConnectionResponseSchema>
|
||||
type Connection = z.infer<typeof ConnectionResponseSchema>;
|
||||
|
||||
// Connector configurations
|
||||
const CONNECTORS = {
|
||||
|
|
@ -35,27 +35,27 @@ const CONNECTORS = {
|
|||
description: "Access your Microsoft Office documents",
|
||||
icon: OneDrive,
|
||||
},
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
type ConnectorProvider = keyof typeof CONNECTORS
|
||||
type ConnectorProvider = keyof typeof CONNECTORS;
|
||||
|
||||
export function ConnectionsTabContent() {
|
||||
const queryClient = useQueryClient()
|
||||
const { selectedProject } = useProject()
|
||||
const autumn = useCustomer()
|
||||
const [isProUser, setIsProUser] = useState(false)
|
||||
const queryClient = useQueryClient();
|
||||
const { selectedProject } = useProject();
|
||||
const autumn = useCustomer();
|
||||
const [isProUser, setIsProUser] = useState(false);
|
||||
|
||||
const handleUpgrade = async () => {
|
||||
try {
|
||||
await autumn.attach({
|
||||
productId: "consumer_pro",
|
||||
successUrl: "https://app.supermemory.ai/",
|
||||
})
|
||||
window.location.reload()
|
||||
});
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Set pro user status when autumn data loads
|
||||
useEffect(() => {
|
||||
|
|
@ -64,16 +64,16 @@ export function ConnectionsTabContent() {
|
|||
autumn.customer?.products.some(
|
||||
(product) => product.id === "consumer_pro",
|
||||
) ?? false,
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [autumn.isLoading, autumn.customer])
|
||||
}, [autumn.isLoading, autumn.customer]);
|
||||
|
||||
// Get connections data directly from autumn customer
|
||||
const connectionsFeature = autumn.customer?.features?.connections
|
||||
const connectionsUsed = connectionsFeature?.usage ?? 0
|
||||
const connectionsLimit = connectionsFeature?.included_usage ?? 0
|
||||
const connectionsFeature = autumn.customer?.features?.connections;
|
||||
const connectionsUsed = connectionsFeature?.usage ?? 0;
|
||||
const connectionsLimit = connectionsFeature?.included_usage ?? 0;
|
||||
|
||||
const canAddConnection = connectionsUsed < connectionsLimit
|
||||
const canAddConnection = connectionsUsed < connectionsLimit;
|
||||
|
||||
// Fetch connections
|
||||
const {
|
||||
|
|
@ -87,26 +87,28 @@ export function ConnectionsTabContent() {
|
|||
body: {
|
||||
containerTags: [],
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error?.message || "Failed to load connections")
|
||||
throw new Error(
|
||||
response.error?.message || "Failed to load connections",
|
||||
);
|
||||
}
|
||||
|
||||
return response.data as Connection[]
|
||||
return response.data as Connection[];
|
||||
},
|
||||
staleTime: 30 * 1000,
|
||||
refetchInterval: 60 * 1000,
|
||||
})
|
||||
});
|
||||
|
||||
// Show error toast if connections fail to load
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
toast.error("Failed to load connections", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
});
|
||||
}
|
||||
}, [error])
|
||||
}, [error]);
|
||||
|
||||
// Add connection mutation
|
||||
const addConnectionMutation = useMutation({
|
||||
|
|
@ -115,7 +117,7 @@ export function ConnectionsTabContent() {
|
|||
if (!canAddConnection && !isProUser) {
|
||||
throw new Error(
|
||||
"Free plan doesn't include connections. Upgrade to Pro for unlimited connections.",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const response = await $fetch("@post/connections/:provider", {
|
||||
|
|
@ -124,61 +126,61 @@ export function ConnectionsTabContent() {
|
|||
redirectUrl: window.location.href,
|
||||
containerTags: [selectedProject],
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
// biome-ignore lint/style/noNonNullAssertion: its fine
|
||||
if ("data" in response && !("error" in response.data!)) {
|
||||
return response.data
|
||||
return response.data;
|
||||
}
|
||||
|
||||
throw new Error(response.error?.message || "Failed to connect")
|
||||
throw new Error(response.error?.message || "Failed to connect");
|
||||
},
|
||||
onSuccess: (data, provider) => {
|
||||
analytics.connectionAdded(provider)
|
||||
analytics.connectionAuthStarted()
|
||||
analytics.connectionAdded(provider);
|
||||
analytics.connectionAuthStarted();
|
||||
autumn.track({
|
||||
featureId: "connections",
|
||||
value: 1,
|
||||
})
|
||||
});
|
||||
if (data?.authLink) {
|
||||
window.location.href = data.authLink
|
||||
window.location.href = data.authLink;
|
||||
}
|
||||
},
|
||||
onError: (error, provider) => {
|
||||
analytics.connectionAuthFailed()
|
||||
analytics.connectionAuthFailed();
|
||||
toast.error(`Failed to connect ${provider}`, {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
// Delete connection mutation
|
||||
const deleteConnectionMutation = useMutation({
|
||||
mutationFn: async (connectionId: string) => {
|
||||
await $fetch(`@delete/connections/${connectionId}`)
|
||||
await $fetch(`@delete/connections/${connectionId}`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
analytics.connectionDeleted()
|
||||
analytics.connectionDeleted();
|
||||
toast.success(
|
||||
"Connection removal has started. supermemory will permanently delete the documents in the next few minutes.",
|
||||
)
|
||||
queryClient.invalidateQueries({ queryKey: ["connections"] })
|
||||
);
|
||||
queryClient.invalidateQueries({ queryKey: ["connections"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Failed to remove connection", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const getProviderIcon = (provider: string) => {
|
||||
const connector = CONNECTORS[provider as ConnectorProvider]
|
||||
const connector = CONNECTORS[provider as ConnectorProvider];
|
||||
if (connector) {
|
||||
const Icon = connector.icon
|
||||
return <Icon className="h-10 w-10" />
|
||||
const Icon = connector.icon;
|
||||
return <Icon className="h-10 w-10" />;
|
||||
}
|
||||
return <span className="text-2xl">📎</span>
|
||||
}
|
||||
return <span className="text-2xl">📎</span>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
|
@ -310,7 +312,7 @@ export function ConnectionsTabContent() {
|
|||
</h3>
|
||||
<div className="grid gap-3">
|
||||
{Object.entries(CONNECTORS).map(([provider, config], index) => {
|
||||
const Icon = config.icon
|
||||
const Icon = config.icon;
|
||||
return (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
@ -324,7 +326,7 @@ export function ConnectionsTabContent() {
|
|||
className="justify-start h-auto p-4 bg-white/5 hover:bg-white/10 border-white/10 text-white w-full"
|
||||
disabled={addConnectionMutation.isPending}
|
||||
onClick={() => {
|
||||
addConnectionMutation.mutate(provider as ConnectorProvider)
|
||||
addConnectionMutation.mutate(provider as ConnectorProvider);
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
|
|
@ -337,10 +339,10 @@ export function ConnectionsTabContent() {
|
|||
</div>
|
||||
</Button>
|
||||
</motion.div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<typeof ConnectionResponseSchema>
|
||||
type Connection = z.infer<typeof ConnectionResponseSchema>;
|
||||
|
||||
const CONNECTORS = {
|
||||
"google-drive": {
|
||||
|
|
@ -47,66 +47,66 @@ const CONNECTORS = {
|
|||
description: "Access your Microsoft Office documents",
|
||||
icon: OneDrive,
|
||||
},
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
type ConnectorProvider = keyof typeof CONNECTORS
|
||||
type ConnectorProvider = keyof typeof CONNECTORS;
|
||||
|
||||
const ChromeIcon = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
preserveAspectRatio="xMidYMid"
|
||||
viewBox="0 0 190.5 190.5"
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>Google Chrome Icon</title>
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M95.252 142.873c26.304 0 47.627-21.324 47.627-47.628s-21.323-47.628-47.627-47.628-47.627 21.324-47.627 47.628 21.323 47.628 47.627 47.628z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
fill="#229342"
|
||||
d="m54.005 119.07-41.24-71.43a95.227 95.227 0 0 0-.003 95.25 95.234 95.234 0 0 0 82.496 47.61l41.24-71.43v-.011a47.613 47.613 0 0 1-17.428 17.443 47.62 47.62 0 0 1-47.632.007 47.62 47.62 0 0 1-17.433-17.437z"
|
||||
fill="#229342"
|
||||
/>
|
||||
<path
|
||||
fill="#fbc116"
|
||||
d="m136.495 119.067-41.239 71.43a95.229 95.229 0 0 0 82.489-47.622A95.24 95.24 0 0 0 190.5 95.248a95.237 95.237 0 0 0-12.772-47.623H95.249l-.01.007a47.62 47.62 0 0 1 23.819 6.372 47.618 47.618 0 0 1 17.439 17.431 47.62 47.62 0 0 1-.001 47.633z"
|
||||
fill="#fbc116"
|
||||
/>
|
||||
<path
|
||||
fill="#1a73e8"
|
||||
d="M95.252 132.961c20.824 0 37.705-16.881 37.705-37.706S116.076 57.55 95.252 57.55 57.547 74.431 57.547 95.255s16.881 37.706 37.705 37.706z"
|
||||
fill="#1a73e8"
|
||||
/>
|
||||
<path
|
||||
fill="#e33b2e"
|
||||
d="M95.252 47.628h82.479A95.237 95.237 0 0 0 142.87 12.76 95.23 95.23 0 0 0 95.245 0a95.222 95.222 0 0 0-47.623 12.767 95.23 95.23 0 0 0-34.856 34.872l41.24 71.43.011.006a47.62 47.62 0 0 1-.015-47.633 47.61 47.61 0 0 1 41.252-23.815z"
|
||||
fill="#e33b2e"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
);
|
||||
|
||||
export function IntegrationsView() {
|
||||
const { org } = useAuth()
|
||||
const queryClient = useQueryClient()
|
||||
const { selectedProject } = useProject()
|
||||
const autumn = useCustomer()
|
||||
const [showApiKeyModal, setShowApiKeyModal] = useState(false)
|
||||
const [apiKey, setApiKey] = useState<string>("")
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [isProUser, setIsProUser] = useState(false)
|
||||
const { org } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
const { selectedProject } = useProject();
|
||||
const autumn = useCustomer();
|
||||
const [showApiKeyModal, setShowApiKeyModal] = useState(false);
|
||||
const [apiKey, setApiKey] = useState<string>("");
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [isProUser, setIsProUser] = useState(false);
|
||||
const [selectedShortcutType, setSelectedShortcutType] = useState<
|
||||
"add" | "search" | null
|
||||
>(null)
|
||||
const apiKeyId = useId()
|
||||
>(null);
|
||||
const apiKeyId = useId();
|
||||
|
||||
const handleUpgrade = async () => {
|
||||
try {
|
||||
await autumn.attach({
|
||||
productId: "consumer_pro",
|
||||
successUrl: "https://app.supermemory.ai/",
|
||||
})
|
||||
window.location.reload()
|
||||
});
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!autumn.isLoading) {
|
||||
|
|
@ -114,15 +114,15 @@ export function IntegrationsView() {
|
|||
autumn.customer?.products.some(
|
||||
(product) => product.id === "consumer_pro",
|
||||
) ?? false,
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [autumn.isLoading, autumn.customer])
|
||||
}, [autumn.isLoading, autumn.customer]);
|
||||
|
||||
const { data: connectionsCheck } = fetchConnectionsFeature(autumn)
|
||||
const connectionsUsed = connectionsCheck?.balance ?? 0
|
||||
const connectionsLimit = connectionsCheck?.included_usage ?? 0
|
||||
const { data: connectionsCheck } = fetchConnectionsFeature(autumn);
|
||||
const connectionsUsed = connectionsCheck?.balance ?? 0;
|
||||
const connectionsLimit = connectionsCheck?.included_usage ?? 0;
|
||||
|
||||
const canAddConnection = connectionsUsed < connectionsLimit
|
||||
const canAddConnection = connectionsUsed < connectionsLimit;
|
||||
|
||||
const {
|
||||
data: connections = [],
|
||||
|
|
@ -135,17 +135,19 @@ export function IntegrationsView() {
|
|||
body: {
|
||||
containerTags: [],
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error?.message || "Failed to load connections")
|
||||
throw new Error(
|
||||
response.error?.message || "Failed to load connections",
|
||||
);
|
||||
}
|
||||
|
||||
return response.data as Connection[]
|
||||
return response.data as Connection[];
|
||||
},
|
||||
staleTime: 30 * 1000,
|
||||
refetchInterval: 60 * 1000,
|
||||
})
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (connectionsError) {
|
||||
|
|
@ -154,16 +156,16 @@ export function IntegrationsView() {
|
|||
connectionsError instanceof Error
|
||||
? connectionsError.message
|
||||
: "Unknown error",
|
||||
})
|
||||
});
|
||||
}
|
||||
}, [connectionsError])
|
||||
}, [connectionsError]);
|
||||
|
||||
const addConnectionMutation = useMutation({
|
||||
mutationFn: async (provider: ConnectorProvider) => {
|
||||
if (!canAddConnection && !isProUser) {
|
||||
throw new Error(
|
||||
"Free plan doesn't include connections. Upgrade to Pro for unlimited connections.",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const response = await $fetch("@post/connections/:provider", {
|
||||
|
|
@ -172,47 +174,47 @@ export function IntegrationsView() {
|
|||
redirectUrl: window.location.href,
|
||||
containerTags: [selectedProject],
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
// biome-ignore lint/style/noNonNullAssertion: its fine
|
||||
if ("data" in response && !("error" in response.data!)) {
|
||||
return response.data
|
||||
return response.data;
|
||||
}
|
||||
|
||||
throw new Error(response.error?.message || "Failed to connect")
|
||||
throw new Error(response.error?.message || "Failed to connect");
|
||||
},
|
||||
onSuccess: (data, provider) => {
|
||||
analytics.connectionAdded(provider)
|
||||
analytics.connectionAuthStarted()
|
||||
analytics.connectionAdded(provider);
|
||||
analytics.connectionAuthStarted();
|
||||
if (data?.authLink) {
|
||||
window.location.href = data.authLink
|
||||
window.location.href = data.authLink;
|
||||
}
|
||||
},
|
||||
onError: (error, provider) => {
|
||||
analytics.connectionAuthFailed()
|
||||
analytics.connectionAuthFailed();
|
||||
toast.error(`Failed to connect ${provider}`, {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const deleteConnectionMutation = useMutation({
|
||||
mutationFn: async (connectionId: string) => {
|
||||
await $fetch(`@delete/connections/${connectionId}`)
|
||||
await $fetch(`@delete/connections/${connectionId}`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
analytics.connectionDeleted()
|
||||
analytics.connectionDeleted();
|
||||
toast.success(
|
||||
"Connection removal has started. supermemory will permanently delete all documents related to the connection in the next few minutes.",
|
||||
)
|
||||
queryClient.invalidateQueries({ queryKey: ["connections"] })
|
||||
);
|
||||
queryClient.invalidateQueries({ queryKey: ["connections"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Failed to remove connection", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const createApiKeyMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
|
|
@ -223,60 +225,60 @@ export function IntegrationsView() {
|
|||
},
|
||||
name: `ios-${generateId().slice(0, 8)}`,
|
||||
prefix: `sm_${org?.id}_`,
|
||||
})
|
||||
return res.key
|
||||
});
|
||||
return res.key;
|
||||
},
|
||||
onSuccess: (apiKey) => {
|
||||
setApiKey(apiKey)
|
||||
setShowApiKeyModal(true)
|
||||
setCopied(false)
|
||||
handleCopyApiKey()
|
||||
setApiKey(apiKey);
|
||||
setShowApiKeyModal(true);
|
||||
setCopied(false);
|
||||
handleCopyApiKey();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Failed to create API key", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const handleShortcutClick = (shortcutType: "add" | "search") => {
|
||||
setSelectedShortcutType(shortcutType)
|
||||
createApiKeyMutation.mutate()
|
||||
}
|
||||
setSelectedShortcutType(shortcutType);
|
||||
createApiKeyMutation.mutate();
|
||||
};
|
||||
|
||||
const handleCopyApiKey = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(apiKey)
|
||||
setCopied(true)
|
||||
toast.success("API key copied to clipboard!")
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
await navigator.clipboard.writeText(apiKey);
|
||||
setCopied(true);
|
||||
toast.success("API key copied to clipboard!");
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
toast.error("Failed to copy API key")
|
||||
toast.error("Failed to copy API key");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenShortcut = () => {
|
||||
if (!selectedShortcutType) {
|
||||
toast.error("No shortcut type selected")
|
||||
return
|
||||
toast.error("No shortcut type selected");
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedShortcutType === "add") {
|
||||
window.open(ADD_MEMORY_SHORTCUT_URL, "_blank")
|
||||
window.open(ADD_MEMORY_SHORTCUT_URL, "_blank");
|
||||
} else if (selectedShortcutType === "search") {
|
||||
window.open(SEARCH_MEMORY_SHORTCUT_URL, "_blank")
|
||||
window.open(SEARCH_MEMORY_SHORTCUT_URL, "_blank");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDialogClose = (open: boolean) => {
|
||||
setShowApiKeyModal(open)
|
||||
setShowApiKeyModal(open);
|
||||
if (!open) {
|
||||
// Reset state when dialog closes
|
||||
setSelectedShortcutType(null)
|
||||
setApiKey("")
|
||||
setCopied(false)
|
||||
setSelectedShortcutType(null);
|
||||
setApiKey("");
|
||||
setCopied(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 sm:space-y-4 custom-scrollbar">
|
||||
|
|
@ -298,32 +300,32 @@ export function IntegrationsView() {
|
|||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-2 sm:gap-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex-1 text-white hover:bg-blue-500/10 bg-[#171F59]/75 "
|
||||
onClick={() => handleShortcutClick("add")}
|
||||
disabled={createApiKeyMutation.isPending}
|
||||
onClick={() => handleShortcutClick("add")}
|
||||
variant="ghost"
|
||||
>
|
||||
<Image
|
||||
src="/images/ios-shortcuts.png"
|
||||
alt="iOS Shortcuts"
|
||||
width={20}
|
||||
height={20}
|
||||
src="/images/ios-shortcuts.png"
|
||||
width={20}
|
||||
/>
|
||||
{createApiKeyMutation.isPending
|
||||
? "Creating..."
|
||||
: "Add Memory Shortcut"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex-1 text-white hover:bg-blue-500/10 bg-[#171F59]/75"
|
||||
onClick={() => handleShortcutClick("search")}
|
||||
disabled={createApiKeyMutation.isPending}
|
||||
onClick={() => handleShortcutClick("search")}
|
||||
variant="ghost"
|
||||
>
|
||||
<Image
|
||||
src="/images/ios-shortcuts.png"
|
||||
alt="iOS Shortcuts"
|
||||
width={20}
|
||||
height={20}
|
||||
src="/images/ios-shortcuts.png"
|
||||
width={20}
|
||||
/>
|
||||
{createApiKeyMutation.isPending
|
||||
? "Creating..."
|
||||
|
|
@ -352,8 +354,8 @@ export function IntegrationsView() {
|
|||
"https://chromewebstore.google.com/detail/supermemory/afpgkkipfdpeaflnpoaffkcankadgjfc",
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
)
|
||||
analytics.extensionInstallClicked()
|
||||
);
|
||||
analytics.extensionInstallClicked();
|
||||
}}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
|
|
@ -465,11 +467,11 @@ export function IntegrationsView() {
|
|||
) : (
|
||||
<div className="space-y-2">
|
||||
{Object.entries(CONNECTORS).map(([provider, config], index) => {
|
||||
const Icon = config.icon
|
||||
const Icon = config.icon;
|
||||
const connection = connections.find(
|
||||
(conn) => conn.provider === provider,
|
||||
)
|
||||
const isConnected = !!connection
|
||||
);
|
||||
const isConnected = !!connection;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
|
|
@ -548,9 +550,9 @@ export function IntegrationsView() {
|
|||
</span>
|
||||
</div>
|
||||
<motion.div
|
||||
className="flex-shrink-0"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
<Button
|
||||
className="bg-blue-600/20 hover:bg-blue-600/30 text-blue-400 border-blue-600/30 min-w-[80px] disabled:cursor-not-allowed"
|
||||
|
|
@ -560,7 +562,7 @@ export function IntegrationsView() {
|
|||
onClick={() => {
|
||||
addConnectionMutation.mutate(
|
||||
provider as ConnectorProvider,
|
||||
)
|
||||
);
|
||||
}}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
|
|
@ -575,7 +577,7 @@ export function IntegrationsView() {
|
|||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -587,10 +589,10 @@ export function IntegrationsView() {
|
|||
More integrations are coming soon! Have a suggestion? Share it with us
|
||||
on{" "}
|
||||
<a
|
||||
href="https://x.com/supermemoryai"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-orange-500 hover:text-orange-400 underline"
|
||||
href="https://x.com/supermemoryai"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
X
|
||||
</a>
|
||||
|
|
@ -599,7 +601,7 @@ export function IntegrationsView() {
|
|||
</div>
|
||||
|
||||
{/* API Key Modal */}
|
||||
<Dialog open={showApiKeyModal} onOpenChange={handleDialogClose}>
|
||||
<Dialog onOpenChange={handleDialogClose} open={showApiKeyModal}>
|
||||
<DialogPortal>
|
||||
<DialogContent className="bg-[#0f1419] border-white/10 text-white md:max-w-md z-[100]">
|
||||
<DialogHeader>
|
||||
|
|
@ -618,24 +620,24 @@ export function IntegrationsView() {
|
|||
{/* API Key Section */}
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor={apiKeyId}
|
||||
className="text-sm font-medium text-white/80"
|
||||
htmlFor={apiKeyId}
|
||||
>
|
||||
Your API Key
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
className="flex-1 bg-white/5 border border-white/20 rounded-lg px-3 py-2 text-sm text-white font-mono"
|
||||
id={apiKeyId}
|
||||
readOnly
|
||||
type="text"
|
||||
value={apiKey}
|
||||
readOnly
|
||||
className="flex-1 bg-white/5 border border-white/20 rounded-lg px-3 py-2 text-sm text-white font-mono"
|
||||
/>
|
||||
<Button
|
||||
className="text-white/70 hover:text-white hover:bg-white/10"
|
||||
onClick={handleCopyApiKey}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={handleCopyApiKey}
|
||||
className="text-white/70 hover:text-white hover:bg-white/10"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="h-4 w-4 text-green-400" />
|
||||
|
|
@ -681,16 +683,16 @@ export function IntegrationsView() {
|
|||
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button
|
||||
onClick={handleOpenShortcut}
|
||||
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white"
|
||||
disabled={!selectedShortcutType}
|
||||
onClick={handleOpenShortcut}
|
||||
>
|
||||
<Image
|
||||
src="/images/ios-shortcuts.png"
|
||||
alt="iOS Shortcuts"
|
||||
width={16}
|
||||
height={16}
|
||||
className="mr-2"
|
||||
height={16}
|
||||
src="/images/ios-shortcuts.png"
|
||||
width={16}
|
||||
/>
|
||||
Add to Shortcuts
|
||||
</Button>
|
||||
|
|
@ -700,5 +702,5 @@ export function IntegrationsView() {
|
|||
</DialogPortal>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { $fetch } from "@lib/api"
|
||||
import { authClient } from "@lib/auth"
|
||||
import { useAuth } from "@lib/auth-context"
|
||||
import { useForm } from "@tanstack/react-form"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { Button } from "@ui/components/button"
|
||||
import { $fetch } from "@lib/api";
|
||||
import { authClient } from "@lib/auth";
|
||||
import { useAuth } from "@lib/auth-context";
|
||||
import { useForm } from "@tanstack/react-form";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Button } from "@ui/components/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -11,18 +11,18 @@ import {
|
|||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@ui/components/dialog"
|
||||
import { Input } from "@ui/components/input"
|
||||
import { CopyableCell } from "@ui/copyable-cell"
|
||||
import { Loader2 } from "lucide-react"
|
||||
import { AnimatePresence, motion } from "motion/react"
|
||||
import Image from "next/image"
|
||||
import { generateSlug } from "random-word-slugs"
|
||||
import { useEffect, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { z } from "zod/v4"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
import { InstallationDialogContent } from "./installation-dialog-content"
|
||||
} from "@ui/components/dialog";
|
||||
import { Input } from "@ui/components/input";
|
||||
import { CopyableCell } from "@ui/copyable-cell";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import Image from "next/image";
|
||||
import { generateSlug } from "random-word-slugs";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod/v4";
|
||||
import { analytics } from "@/lib/analytics";
|
||||
import { InstallationDialogContent } from "./installation-dialog-content";
|
||||
|
||||
// Validation schemas
|
||||
const mcpMigrationSchema = z.object({
|
||||
|
|
@ -33,56 +33,56 @@ const mcpMigrationSchema = z.object({
|
|||
/^https:\/\/mcp\.supermemory\.ai\/[^/]+\/sse$/,
|
||||
"Link must be in format: https://mcp.supermemory.ai/userId/sse",
|
||||
),
|
||||
})
|
||||
});
|
||||
|
||||
export function MCPView() {
|
||||
const [isMigrateDialogOpen, setIsMigrateDialogOpen] = useState(false)
|
||||
const projectId = localStorage.getItem("selectedProject") ?? "default"
|
||||
const { org } = useAuth()
|
||||
const [apiKey, setApiKey] = useState<string>()
|
||||
const [isInstallDialogOpen, setIsInstallDialogOpen] = useState(false)
|
||||
const [isMigrateDialogOpen, setIsMigrateDialogOpen] = useState(false);
|
||||
const projectId = localStorage.getItem("selectedProject") ?? "default";
|
||||
const { org } = useAuth();
|
||||
const [apiKey, setApiKey] = useState<string>();
|
||||
const [isInstallDialogOpen, setIsInstallDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
analytics.mcpViewOpened()
|
||||
}, [])
|
||||
analytics.mcpViewOpened();
|
||||
}, []);
|
||||
const apiKeyMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (apiKey) return apiKey
|
||||
if (apiKey) return apiKey;
|
||||
const res = await authClient.apiKey.create({
|
||||
metadata: {
|
||||
organizationId: org?.id,
|
||||
},
|
||||
name: generateSlug(),
|
||||
prefix: `sm_${org?.id}_`,
|
||||
})
|
||||
return res.key
|
||||
});
|
||||
return res.key;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
setApiKey(data)
|
||||
setIsInstallDialogOpen(true)
|
||||
setApiKey(data);
|
||||
setIsInstallDialogOpen(true);
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
// Form for MCP migration
|
||||
const mcpMigrationForm = useForm({
|
||||
defaultValues: { url: "" },
|
||||
onSubmit: async ({ value, formApi }) => {
|
||||
const userId = extractUserIdFromMCPUrl(value.url)
|
||||
const userId = extractUserIdFromMCPUrl(value.url);
|
||||
if (userId) {
|
||||
migrateMCPMutation.mutate({ userId, projectId })
|
||||
formApi.reset()
|
||||
migrateMCPMutation.mutate({ userId, projectId });
|
||||
formApi.reset();
|
||||
}
|
||||
},
|
||||
validators: {
|
||||
onChange: mcpMigrationSchema,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const extractUserIdFromMCPUrl = (url: string): string | null => {
|
||||
const regex = /^https:\/\/mcp\.supermemory\.ai\/([^/]+)\/sse$/
|
||||
const match = url.trim().match(regex)
|
||||
return match?.[1] || null
|
||||
}
|
||||
const regex = /^https:\/\/mcp\.supermemory\.ai\/([^/]+)\/sse$/;
|
||||
const match = url.trim().match(regex);
|
||||
return match?.[1] || null;
|
||||
};
|
||||
|
||||
// Migrate MCP mutation
|
||||
const migrateMCPMutation = useMutation({
|
||||
|
|
@ -90,33 +90,33 @@ export function MCPView() {
|
|||
userId,
|
||||
projectId,
|
||||
}: {
|
||||
userId: string
|
||||
projectId: string
|
||||
userId: string;
|
||||
projectId: string;
|
||||
}) => {
|
||||
const response = await $fetch("@post/documents/migrate-mcp", {
|
||||
body: { userId, projectId },
|
||||
})
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(
|
||||
response.error?.message || "Failed to migrate documents",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return response.data
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
toast.success("Migration completed!", {
|
||||
description: `Successfully migrated ${data?.migratedCount} documents`,
|
||||
})
|
||||
setIsMigrateDialogOpen(false)
|
||||
});
|
||||
setIsMigrateDialogOpen(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Migration failed", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
|
@ -155,9 +155,9 @@ export function MCPView() {
|
|||
<Button
|
||||
disabled={apiKeyMutation.isPending}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
apiKeyMutation.mutate()
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
apiKeyMutation.mutate();
|
||||
}}
|
||||
>
|
||||
Install Now
|
||||
|
|
@ -213,9 +213,9 @@ export function MCPView() {
|
|||
</DialogHeader>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
mcpMigrationForm.handleSubmit()
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
mcpMigrationForm.handleSubmit();
|
||||
}}
|
||||
>
|
||||
<div className="grid gap-4">
|
||||
|
|
@ -268,8 +268,8 @@ export function MCPView() {
|
|||
<Button
|
||||
className="bg-white/5 hover:bg-white/10 border-white/10 text-white"
|
||||
onClick={() => {
|
||||
setIsMigrateDialogOpen(false)
|
||||
mcpMigrationForm.reset()
|
||||
setIsMigrateDialogOpen(false);
|
||||
mcpMigrationForm.reset();
|
||||
}}
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
|
@ -307,5 +307,5 @@ export function MCPView() {
|
|||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { $fetch } from "@repo/lib/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Button } from "@ui/components/button";
|
||||
import {
|
||||
DialogContent,
|
||||
|
|
@ -6,6 +8,7 @@ import {
|
|||
DialogTitle,
|
||||
} from "@ui/components/dialog";
|
||||
import { Input } from "@ui/components/input";
|
||||
import { Label } from "@ui/components/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
|
|
@ -13,13 +16,10 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@ui/components/select";
|
||||
import { Label } from "@ui/components/label";
|
||||
import { CopyIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { analytics } from "@/lib/analytics";
|
||||
import { $fetch } from "@repo/lib/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
const clients = {
|
||||
cursor: "Cursor",
|
||||
|
|
@ -67,7 +67,7 @@ export function InstallationDialogContent() {
|
|||
|
||||
if (selectedProject && selectedProject !== "none") {
|
||||
// Remove the "sm_project_" prefix from the containerTag
|
||||
const projectId = selectedProject.replace(/^sm_project_/, '');
|
||||
const projectId = selectedProject.replace(/^sm_project_/, "");
|
||||
command += ` --project ${projectId}`;
|
||||
}
|
||||
|
||||
|
|
@ -79,8 +79,8 @@ export function InstallationDialogContent() {
|
|||
<DialogHeader>
|
||||
<DialogTitle>Install the supermemory MCP Server</DialogTitle>
|
||||
<DialogDescription>
|
||||
Select the app and project you want to install supermemory MCP to, then run the
|
||||
following command:
|
||||
Select the app and project you want to install supermemory MCP to,
|
||||
then run the following command:
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ export function InstallationDialogContent() {
|
|||
onValueChange={(value) => setClient(value as keyof typeof clients)}
|
||||
value={client}
|
||||
>
|
||||
<SelectTrigger id="client-select" className="w-full">
|
||||
<SelectTrigger className="w-full" id="client-select">
|
||||
<SelectValue placeholder="Select client" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
@ -107,27 +107,30 @@ export function InstallationDialogContent() {
|
|||
<div className="space-y-2">
|
||||
<Label htmlFor="project-select">Target Project (Optional)</Label>
|
||||
<Select
|
||||
disabled={isLoadingProjects}
|
||||
onValueChange={setSelectedProject}
|
||||
value={selectedProject || "none"}
|
||||
disabled={isLoadingProjects}
|
||||
>
|
||||
<SelectTrigger id="project-select" className="w-full">
|
||||
<SelectTrigger className="w-full" id="project-select">
|
||||
<SelectValue placeholder="Select project" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-black/90 backdrop-blur-xl border-white/10">
|
||||
<SelectItem value="none" className="text-white hover:bg-white/10">
|
||||
<SelectItem className="text-white hover:bg-white/10" value="none">
|
||||
Auto-select project
|
||||
</SelectItem>
|
||||
<SelectItem value="sm_project_default" className="text-white hover:bg-white/10">
|
||||
<SelectItem
|
||||
className="text-white hover:bg-white/10"
|
||||
value="sm_project_default"
|
||||
>
|
||||
Default Project
|
||||
</SelectItem>
|
||||
{projects
|
||||
.filter((p: Project) => p.containerTag !== "sm_project_default")
|
||||
.map((project: Project) => (
|
||||
<SelectItem
|
||||
className="text-white hover:bg-white/10"
|
||||
key={project.id}
|
||||
value={project.containerTag}
|
||||
className="text-white hover:bg-white/10"
|
||||
>
|
||||
{project.name}
|
||||
</SelectItem>
|
||||
|
|
@ -139,8 +142,8 @@ export function InstallationDialogContent() {
|
|||
<div className="space-y-2">
|
||||
<Label htmlFor="command-input">Installation Command</Label>
|
||||
<Input
|
||||
id="command-input"
|
||||
className="font-mono text-xs!"
|
||||
id="command-input"
|
||||
readOnly
|
||||
value={generateInstallCommand()}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { authClient } from "@lib/auth"
|
||||
import { useAuth } from "@lib/auth-context"
|
||||
import { Button } from "@repo/ui/components/button"
|
||||
import { HeadingH3Bold } from "@repo/ui/text/heading/heading-h3-bold"
|
||||
import { useCustomer } from "autumn-js/react"
|
||||
import { $fetch } from "@lib/api";
|
||||
import { authClient } from "@lib/auth";
|
||||
import { useAuth } from "@lib/auth-context";
|
||||
import { Button } from "@repo/ui/components/button";
|
||||
import { HeadingH3Bold } from "@repo/ui/text/heading/heading-h3-bold";
|
||||
import { useCustomer } from "autumn-js/react";
|
||||
import {
|
||||
CheckCircle,
|
||||
CreditCard,
|
||||
|
|
@ -12,48 +13,47 @@ import {
|
|||
LogOut,
|
||||
User,
|
||||
X,
|
||||
} from "lucide-react"
|
||||
import { motion } from "motion/react"
|
||||
import Link from "next/link"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useEffect, useState } from "react"
|
||||
import { analytics } from "@/lib/analytics"
|
||||
import { $fetch } from "@lib/api"
|
||||
} from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { analytics } from "@/lib/analytics";
|
||||
|
||||
export function ProfileView() {
|
||||
const router = useRouter()
|
||||
const { user: session } = useAuth()
|
||||
const router = useRouter();
|
||||
const { user: session } = useAuth();
|
||||
const {
|
||||
customer,
|
||||
isLoading: isCustomerLoading,
|
||||
openBillingPortal,
|
||||
attach,
|
||||
} = useCustomer()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
} = useCustomer();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [billingData, setBillingData] = useState<{
|
||||
isPro: boolean
|
||||
memoriesUsed: number
|
||||
memoriesLimit: number
|
||||
connectionsUsed: number
|
||||
connectionsLimit: number
|
||||
isPro: boolean;
|
||||
memoriesUsed: number;
|
||||
memoriesLimit: number;
|
||||
connectionsUsed: number;
|
||||
connectionsLimit: number;
|
||||
}>({
|
||||
isPro: false,
|
||||
memoriesUsed: 0,
|
||||
memoriesLimit: 0,
|
||||
connectionsUsed: 0,
|
||||
connectionsLimit: 0,
|
||||
})
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isCustomerLoading) {
|
||||
const memoriesFeature = customer?.features?.memories ?? {
|
||||
usage: 0,
|
||||
included_usage: 0,
|
||||
}
|
||||
};
|
||||
const connectionsFeature = customer?.features?.connections ?? {
|
||||
usage: 0,
|
||||
included_usage: 0,
|
||||
}
|
||||
};
|
||||
|
||||
setBillingData({
|
||||
isPro:
|
||||
|
|
@ -64,30 +64,30 @@ export function ProfileView() {
|
|||
memoriesLimit: memoriesFeature?.included_usage ?? 0,
|
||||
connectionsUsed: connectionsFeature?.usage ?? 0,
|
||||
connectionsLimit: connectionsFeature?.included_usage ?? 0,
|
||||
})
|
||||
});
|
||||
}
|
||||
}, [isCustomerLoading, customer])
|
||||
}, [isCustomerLoading, customer]);
|
||||
|
||||
const handleLogout = () => {
|
||||
analytics.userSignedOut()
|
||||
authClient.signOut()
|
||||
router.push("/login")
|
||||
}
|
||||
analytics.userSignedOut();
|
||||
authClient.signOut();
|
||||
router.push("/login");
|
||||
};
|
||||
|
||||
const handleUpgrade = async () => {
|
||||
setIsLoading(true)
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const upgradeResult = await attach({
|
||||
productId: "consumer_pro",
|
||||
successUrl: "https://app.supermemory.ai/",
|
||||
})
|
||||
});
|
||||
if (
|
||||
upgradeResult.statusCode === 200 &&
|
||||
upgradeResult.data &&
|
||||
"code" in upgradeResult.data
|
||||
) {
|
||||
const isProPlanActivated =
|
||||
upgradeResult.data.code === "new_product_attached"
|
||||
upgradeResult.data.code === "new_product_attached";
|
||||
if (isProPlanActivated && session?.name && session?.email) {
|
||||
try {
|
||||
await $fetch("@post/emails/welcome/pro", {
|
||||
|
|
@ -95,24 +95,24 @@ export function ProfileView() {
|
|||
email: session?.email,
|
||||
firstName: session?.name,
|
||||
},
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
setIsLoading(false)
|
||||
console.error(error);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handle manage billing
|
||||
const handleManageBilling = async () => {
|
||||
await openBillingPortal({
|
||||
returnUrl: "https://app.supermemory.ai",
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (session?.isAnonymous) {
|
||||
return (
|
||||
|
|
@ -137,7 +137,7 @@ export function ProfileView() {
|
|||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -337,5 +337,5 @@ export function ProfileView() {
|
|||
Sign Out
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,54 +1,54 @@
|
|||
import { colors } from '@repo/ui/memory-graph/constants';
|
||||
import { colors } from "@repo/ui/memory-graph/constants";
|
||||
import {
|
||||
GoogleDocs,
|
||||
MicrosoftWord,
|
||||
NotionDoc,
|
||||
GoogleDrive,
|
||||
GoogleSheets,
|
||||
GoogleSlides,
|
||||
PDF,
|
||||
OneDrive,
|
||||
MicrosoftOneNote,
|
||||
MicrosoftPowerpoint,
|
||||
MicrosoftExcel,
|
||||
} from '@ui/assets/icons';
|
||||
import { FileText } from 'lucide-react';
|
||||
GoogleDocs,
|
||||
GoogleDrive,
|
||||
GoogleSheets,
|
||||
GoogleSlides,
|
||||
MicrosoftExcel,
|
||||
MicrosoftOneNote,
|
||||
MicrosoftPowerpoint,
|
||||
MicrosoftWord,
|
||||
NotionDoc,
|
||||
OneDrive,
|
||||
PDF,
|
||||
} from "@ui/assets/icons";
|
||||
import { FileText } from "lucide-react";
|
||||
|
||||
export const getDocumentIcon = (type: string, className: string) => {
|
||||
const iconProps = {
|
||||
className,
|
||||
style: { color: colors.text.muted },
|
||||
};
|
||||
const iconProps = {
|
||||
className,
|
||||
style: { color: colors.text.muted },
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case 'google_doc':
|
||||
return <GoogleDocs {...iconProps} />;
|
||||
case 'google_sheet':
|
||||
return <GoogleSheets {...iconProps} />;
|
||||
case 'google_slide':
|
||||
return <GoogleSlides {...iconProps} />;
|
||||
case 'google_drive':
|
||||
return <GoogleDrive {...iconProps} />;
|
||||
case 'notion':
|
||||
case 'notion_doc':
|
||||
return <NotionDoc {...iconProps} />;
|
||||
case 'word':
|
||||
case 'microsoft_word':
|
||||
return <MicrosoftWord {...iconProps} />;
|
||||
case 'excel':
|
||||
case 'microsoft_excel':
|
||||
return <MicrosoftExcel {...iconProps} />;
|
||||
case 'powerpoint':
|
||||
case 'microsoft_powerpoint':
|
||||
return <MicrosoftPowerpoint {...iconProps} />;
|
||||
case 'onenote':
|
||||
case 'microsoft_onenote':
|
||||
return <MicrosoftOneNote {...iconProps} />;
|
||||
case 'onedrive':
|
||||
return <OneDrive {...iconProps} />;
|
||||
case 'pdf':
|
||||
return <PDF {...iconProps} />;
|
||||
default:
|
||||
return <FileText {...iconProps} />;
|
||||
}
|
||||
switch (type) {
|
||||
case "google_doc":
|
||||
return <GoogleDocs {...iconProps} />;
|
||||
case "google_sheet":
|
||||
return <GoogleSheets {...iconProps} />;
|
||||
case "google_slide":
|
||||
return <GoogleSlides {...iconProps} />;
|
||||
case "google_drive":
|
||||
return <GoogleDrive {...iconProps} />;
|
||||
case "notion":
|
||||
case "notion_doc":
|
||||
return <NotionDoc {...iconProps} />;
|
||||
case "word":
|
||||
case "microsoft_word":
|
||||
return <MicrosoftWord {...iconProps} />;
|
||||
case "excel":
|
||||
case "microsoft_excel":
|
||||
return <MicrosoftExcel {...iconProps} />;
|
||||
case "powerpoint":
|
||||
case "microsoft_powerpoint":
|
||||
return <MicrosoftPowerpoint {...iconProps} />;
|
||||
case "onenote":
|
||||
case "microsoft_onenote":
|
||||
return <MicrosoftOneNote {...iconProps} />;
|
||||
case "onedrive":
|
||||
return <OneDrive {...iconProps} />;
|
||||
case "pdf":
|
||||
return <PDF {...iconProps} />;
|
||||
default:
|
||||
return <FileText {...iconProps} />;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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).*)",
|
||||
],
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
initOpenNextCloudflareForDev();
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
3
bun.lock
3
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=="],
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import { tool } from "ai"
|
||||
import Supermemory from "supermemory"
|
||||
import { z } from "zod"
|
||||
import { tool } from "ai";
|
||||
import Supermemory from "supermemory";
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Supermemory configuration
|
||||
* Only one of `projectId` or `containerTags` can be provided.
|
||||
*/
|
||||
export interface SupermemoryToolsConfig {
|
||||
baseUrl?: string
|
||||
containerTags?: string[]
|
||||
projectId?: string
|
||||
baseUrl?: string;
|
||||
containerTags?: string[];
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -22,11 +22,11 @@ export function supermemoryTools(
|
|||
const client = new Supermemory({
|
||||
apiKey,
|
||||
...(config?.baseUrl ? { baseURL: config.baseUrl } : {}),
|
||||
})
|
||||
});
|
||||
|
||||
const containerTags = config?.projectId
|
||||
? [`sm_project_${config?.projectId}`]
|
||||
: (config?.containerTags ?? ["sm_project_default"])
|
||||
: (config?.containerTags ?? ["sm_project_default"]);
|
||||
|
||||
const searchMemories = tool({
|
||||
description:
|
||||
|
|
@ -60,21 +60,21 @@ export function supermemoryTools(
|
|||
limit,
|
||||
chunkThreshold: 0.6,
|
||||
includeFullDocs,
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
results: response.results,
|
||||
count: response.results?.length || 0,
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const addMemory = tool({
|
||||
description:
|
||||
|
|
@ -88,31 +88,31 @@ export function supermemoryTools(
|
|||
}),
|
||||
execute: async ({ memory }) => {
|
||||
try {
|
||||
const metadata: Record<string, string | number | boolean> = {}
|
||||
const metadata: Record<string, string | number | boolean> = {};
|
||||
|
||||
const response = await client.memories.add({
|
||||
content: memory,
|
||||
containerTags,
|
||||
...(Object.keys(metadata).length > 0 && { metadata }),
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
memory: response,
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
searchMemories,
|
||||
addMemory,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Export individual tool creators for more flexibility
|
||||
|
|
@ -120,14 +120,14 @@ export const searchMemoriesTool = (
|
|||
apiKey: string,
|
||||
config?: SupermemoryToolsConfig,
|
||||
) => {
|
||||
const { searchMemories } = supermemoryTools(apiKey, config)
|
||||
return searchMemories
|
||||
}
|
||||
const { searchMemories } = supermemoryTools(apiKey, config);
|
||||
return searchMemories;
|
||||
};
|
||||
|
||||
export const addMemoryTool = (
|
||||
apiKey: string,
|
||||
config?: SupermemoryToolsConfig,
|
||||
) => {
|
||||
const { addMemory } = supermemoryTools(apiKey, config)
|
||||
return addMemory
|
||||
}
|
||||
const { addMemory } = supermemoryTools(apiKey, config);
|
||||
return addMemory;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
};
|
||||
mql.addEventListener("change", onChange);
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
return () => mql.removeEventListener("change", onChange);
|
||||
}, []);
|
||||
|
||||
return !!isMobile
|
||||
return !!isMobile;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import {
|
||||
createContext,
|
||||
|
|
@ -6,75 +6,74 @@ import {
|
|||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react"
|
||||
import { authClient, useSession } from "./auth"
|
||||
} from "react";
|
||||
import { authClient, useSession } from "./auth";
|
||||
|
||||
type Organization = typeof authClient.$Infer.ActiveOrganization
|
||||
type SessionData = NonNullable<ReturnType<typeof useSession>["data"]>
|
||||
type Organization = typeof authClient.$Infer.ActiveOrganization;
|
||||
type SessionData = NonNullable<ReturnType<typeof useSession>["data"]>;
|
||||
|
||||
interface AuthContextType {
|
||||
session: SessionData["session"] | null
|
||||
user: SessionData["user"] | null
|
||||
org: Organization | null
|
||||
setActiveOrg: (orgSlug: string) => Promise<void>
|
||||
session: SessionData["session"] | null;
|
||||
user: SessionData["user"] | null;
|
||||
org: Organization | null;
|
||||
setActiveOrg: (orgSlug: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const { data: session } = useSession()
|
||||
const [org, setOrg] = useState<Organization | null>(null)
|
||||
const { data: session } = useSession();
|
||||
const [org, setOrg] = useState<Organization | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (session?.session.activeOrganizationId) {
|
||||
authClient.organization.getFullOrganization().then((org) => {
|
||||
setOrg(org)
|
||||
})
|
||||
setOrg(org);
|
||||
});
|
||||
}
|
||||
}, [session?.session.activeOrganizationId])
|
||||
}, [session?.session.activeOrganizationId]);
|
||||
|
||||
// When a session exists and there is a pending login method recorded,
|
||||
// promote it to the last-used method (successful login) and clear pending.
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return
|
||||
if (!session?.session) return
|
||||
if (typeof window === "undefined") return;
|
||||
if (!session?.session) return;
|
||||
|
||||
try {
|
||||
const pendingMethod = localStorage.getItem(
|
||||
"supermemory-pending-login-method",
|
||||
)
|
||||
);
|
||||
const pendingTsRaw = localStorage.getItem(
|
||||
"supermemory-pending-login-timestamp",
|
||||
)
|
||||
);
|
||||
|
||||
if (pendingMethod) {
|
||||
const now = Date.now()
|
||||
const ts = pendingTsRaw ? Number.parseInt(pendingTsRaw, 10) : NaN
|
||||
const isFresh = Number.isFinite(ts) && now - ts < 10 * 60 * 1000 // 10 minutes TTL
|
||||
const now = Date.now();
|
||||
const ts = pendingTsRaw
|
||||
? Number.parseInt(pendingTsRaw, 10)
|
||||
: Number.NaN;
|
||||
const isFresh = Number.isFinite(ts) && now - ts < 10 * 60 * 1000; // 10 minutes TTL
|
||||
|
||||
if (isFresh) {
|
||||
localStorage.setItem(
|
||||
"supermemory-last-login-method",
|
||||
pendingMethod,
|
||||
)
|
||||
localStorage.setItem("supermemory-last-login-method", pendingMethod);
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
} catch {}
|
||||
// Always clear pending markers once a session is present
|
||||
try {
|
||||
localStorage.removeItem("supermemory-pending-login-method")
|
||||
localStorage.removeItem("supermemory-pending-login-timestamp")
|
||||
} catch { }
|
||||
}, [session?.session])
|
||||
localStorage.removeItem("supermemory-pending-login-method");
|
||||
localStorage.removeItem("supermemory-pending-login-timestamp");
|
||||
} catch {}
|
||||
}, [session?.session]);
|
||||
|
||||
const setActiveOrg = async (slug: string) => {
|
||||
if (!slug) return
|
||||
if (!slug) return;
|
||||
|
||||
const activeOrg = await authClient.organization.setActive({
|
||||
organizationSlug: slug,
|
||||
})
|
||||
setOrg(activeOrg)
|
||||
}
|
||||
});
|
||||
setOrg(activeOrg);
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
|
|
@ -87,13 +86,13 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext)
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useAuth must be used within an AuthProvider")
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
return context
|
||||
return context;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
],
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<string, any>) => {
|
||||
posthog.capture("user_interaction", {
|
||||
|
|
@ -123,8 +126,8 @@ export function useInteractionTracking() {
|
|||
user_id: session?.user?.id,
|
||||
timestamp: new Date().toISOString(),
|
||||
...details,
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const trackFormSubmission = (
|
||||
formName: string,
|
||||
|
|
@ -138,15 +141,15 @@ export function useInteractionTracking() {
|
|||
user_id: session?.user?.id,
|
||||
timestamp: new Date().toISOString(),
|
||||
...details,
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const trackButtonClick = (buttonName: string, context?: string) => {
|
||||
trackInteraction("button_click", {
|
||||
button_name: buttonName,
|
||||
context,
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const trackLinkClick = (
|
||||
url: string,
|
||||
|
|
@ -157,27 +160,27 @@ export function useInteractionTracking() {
|
|||
url,
|
||||
link_text: linkText,
|
||||
external,
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const trackModalOpen = (modalName: string) => {
|
||||
trackInteraction("modal_open", {
|
||||
modal_name: modalName,
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const trackModalClose = (modalName: string) => {
|
||||
trackInteraction("modal_close", {
|
||||
modal_name: modalName,
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const trackTabChange = (fromTab: string, toTab: string) => {
|
||||
trackInteraction("tab_change", {
|
||||
from_tab: fromTab,
|
||||
to_tab: toTab,
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
trackInteraction,
|
||||
|
|
@ -187,5 +190,5 @@ export function useInteractionTracking() {
|
|||
trackModalOpen,
|
||||
trackModalClose,
|
||||
trackTabChange,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { customAlphabet } from "nanoid"
|
||||
import { customAlphabet } from "nanoid";
|
||||
|
||||
export const generateId = () =>
|
||||
customAlphabet("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz")(
|
||||
22,
|
||||
)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,56 +1,56 @@
|
|||
// Singleton WebGL context manager for glass effects
|
||||
class GlassEffectManager {
|
||||
private static instance: GlassEffectManager | null = null
|
||||
private canvas: HTMLCanvasElement | null = null
|
||||
private gl: WebGLRenderingContext | null = null
|
||||
private program: WebGLProgram | null = null
|
||||
private uniforms: Record<string, WebGLUniformLocation | null> = {}
|
||||
private effects: Map<string, EffectInstance> = new Map()
|
||||
private animationFrame: number | null = null
|
||||
private startTime: number = performance.now()
|
||||
private mousePositions: Map<string, { x: number; y: number }> = new Map()
|
||||
private static instance: GlassEffectManager | null = null;
|
||||
private canvas: HTMLCanvasElement | null = null;
|
||||
private gl: WebGLRenderingContext | null = null;
|
||||
private program: WebGLProgram | null = null;
|
||||
private uniforms: Record<string, WebGLUniformLocation | null> = {};
|
||||
private effects: Map<string, EffectInstance> = new Map();
|
||||
private animationFrame: number | null = null;
|
||||
private startTime: number = performance.now();
|
||||
private mousePositions: Map<string, { x: number; y: number }> = new Map();
|
||||
|
||||
static getInstance(): GlassEffectManager {
|
||||
if (!GlassEffectManager.instance) {
|
||||
GlassEffectManager.instance = new GlassEffectManager()
|
||||
GlassEffectManager.instance = new GlassEffectManager();
|
||||
}
|
||||
return GlassEffectManager.instance
|
||||
return GlassEffectManager.instance;
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
this.initializeContext()
|
||||
this.initializeContext();
|
||||
}
|
||||
|
||||
private initializeContext() {
|
||||
// Create offscreen canvas
|
||||
this.canvas = document.createElement("canvas")
|
||||
this.canvas.width = 1024 // Default size, will be adjusted
|
||||
this.canvas.height = 1024
|
||||
this.canvas = document.createElement("canvas");
|
||||
this.canvas.width = 1024; // Default size, will be adjusted
|
||||
this.canvas.height = 1024;
|
||||
|
||||
this.gl = this.canvas.getContext("webgl", {
|
||||
alpha: true,
|
||||
premultipliedAlpha: false,
|
||||
preserveDrawingBuffer: true,
|
||||
})
|
||||
});
|
||||
|
||||
if (!this.gl) {
|
||||
console.error("WebGL not supported")
|
||||
return
|
||||
console.error("WebGL not supported");
|
||||
return;
|
||||
}
|
||||
|
||||
this.setupShaders()
|
||||
this.startRenderLoop()
|
||||
this.setupShaders();
|
||||
this.startRenderLoop();
|
||||
}
|
||||
|
||||
private setupShaders() {
|
||||
if (!this.gl) return
|
||||
if (!this.gl) return;
|
||||
|
||||
const vsSource = `
|
||||
attribute vec2 position;
|
||||
void main() {
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
const fsSource = `
|
||||
precision mediump float;
|
||||
|
|
@ -106,48 +106,48 @@ class GlassEffectManager {
|
|||
|
||||
gl_FragColor = vec4(glassColor, alpha);
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
const createShader = (type: number, source: string) => {
|
||||
const shader = this.gl!.createShader(type)
|
||||
if (!shader) return null
|
||||
const shader = this.gl!.createShader(type);
|
||||
if (!shader) return null;
|
||||
|
||||
this.gl!.shaderSource(shader, source)
|
||||
this.gl!.compileShader(shader)
|
||||
this.gl!.shaderSource(shader, source);
|
||||
this.gl!.compileShader(shader);
|
||||
|
||||
if (!this.gl!.getShaderParameter(shader, this.gl!.COMPILE_STATUS)) {
|
||||
console.error("Shader error:", this.gl!.getShaderInfoLog(shader))
|
||||
this.gl!.deleteShader(shader)
|
||||
return null
|
||||
console.error("Shader error:", this.gl!.getShaderInfoLog(shader));
|
||||
this.gl!.deleteShader(shader);
|
||||
return null;
|
||||
}
|
||||
return shader
|
||||
}
|
||||
return shader;
|
||||
};
|
||||
|
||||
const vs = createShader(this.gl.VERTEX_SHADER, vsSource)
|
||||
const fs = createShader(this.gl.FRAGMENT_SHADER, fsSource)
|
||||
if (!vs || !fs) return
|
||||
const vs = createShader(this.gl.VERTEX_SHADER, vsSource);
|
||||
const fs = createShader(this.gl.FRAGMENT_SHADER, fsSource);
|
||||
if (!vs || !fs) return;
|
||||
|
||||
this.program = this.gl.createProgram()
|
||||
if (!this.program) return
|
||||
this.program = this.gl.createProgram();
|
||||
if (!this.program) return;
|
||||
|
||||
this.gl.attachShader(this.program, vs)
|
||||
this.gl.attachShader(this.program, fs)
|
||||
this.gl.linkProgram(this.program)
|
||||
this.gl.attachShader(this.program, vs);
|
||||
this.gl.attachShader(this.program, fs);
|
||||
this.gl.linkProgram(this.program);
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: Well, not a hook
|
||||
this.gl.useProgram(this.program)
|
||||
this.gl.useProgram(this.program);
|
||||
|
||||
// Buffer setup
|
||||
const buffer = this.gl.createBuffer()
|
||||
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer)
|
||||
const buffer = this.gl.createBuffer();
|
||||
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer);
|
||||
this.gl.bufferData(
|
||||
this.gl.ARRAY_BUFFER,
|
||||
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
|
||||
this.gl.STATIC_DRAW,
|
||||
)
|
||||
);
|
||||
|
||||
const position = this.gl.getAttribLocation(this.program, "position")
|
||||
this.gl.enableVertexAttribArray(position)
|
||||
this.gl.vertexAttribPointer(position, 2, this.gl.FLOAT, false, 0, 0)
|
||||
const position = this.gl.getAttribLocation(this.program, "position");
|
||||
this.gl.enableVertexAttribArray(position);
|
||||
this.gl.vertexAttribPointer(position, 2, this.gl.FLOAT, false, 0, 0);
|
||||
|
||||
// Store uniform locations
|
||||
this.uniforms = {
|
||||
|
|
@ -155,11 +155,11 @@ class GlassEffectManager {
|
|||
time: this.gl.getUniformLocation(this.program, "iTime"),
|
||||
mouse: this.gl.getUniformLocation(this.program, "iMouse"),
|
||||
expanded: this.gl.getUniformLocation(this.program, "iExpanded"),
|
||||
}
|
||||
};
|
||||
|
||||
// Enable blending
|
||||
this.gl.enable(this.gl.BLEND)
|
||||
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA)
|
||||
this.gl.enable(this.gl.BLEND);
|
||||
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
|
||||
}
|
||||
|
||||
registerEffect(
|
||||
|
|
@ -168,8 +168,8 @@ class GlassEffectManager {
|
|||
isExpanded: boolean,
|
||||
): () => void {
|
||||
// Ensure minimum dimensions
|
||||
const width = Math.max(1, targetCanvas.width)
|
||||
const height = Math.max(1, targetCanvas.height)
|
||||
const width = Math.max(1, targetCanvas.width);
|
||||
const height = Math.max(1, targetCanvas.height);
|
||||
|
||||
const effect: EffectInstance = {
|
||||
id,
|
||||
|
|
@ -177,58 +177,58 @@ class GlassEffectManager {
|
|||
isExpanded,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
};
|
||||
|
||||
this.effects.set(id, effect)
|
||||
this.mousePositions.set(id, { x: 0, y: 0 })
|
||||
this.effects.set(id, effect);
|
||||
this.mousePositions.set(id, { x: 0, y: 0 });
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
this.effects.delete(id)
|
||||
this.mousePositions.delete(id)
|
||||
this.effects.delete(id);
|
||||
this.mousePositions.delete(id);
|
||||
if (this.effects.size === 0 && this.animationFrame) {
|
||||
cancelAnimationFrame(this.animationFrame)
|
||||
this.animationFrame = null
|
||||
cancelAnimationFrame(this.animationFrame);
|
||||
this.animationFrame = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
updateMousePosition(id: string, x: number, y: number) {
|
||||
this.mousePositions.set(id, { x, y })
|
||||
this.mousePositions.set(id, { x, y });
|
||||
}
|
||||
|
||||
updateExpanded(id: string, isExpanded: boolean) {
|
||||
const effect = this.effects.get(id)
|
||||
const effect = this.effects.get(id);
|
||||
if (effect) {
|
||||
effect.isExpanded = isExpanded
|
||||
effect.isExpanded = isExpanded;
|
||||
}
|
||||
}
|
||||
|
||||
updateSize(id: string, width: number, height: number) {
|
||||
const effect = this.effects.get(id)
|
||||
const effect = this.effects.get(id);
|
||||
if (effect) {
|
||||
// Ensure minimum dimensions
|
||||
effect.width = Math.max(1, width)
|
||||
effect.height = Math.max(1, height)
|
||||
effect.width = Math.max(1, width);
|
||||
effect.height = Math.max(1, height);
|
||||
}
|
||||
}
|
||||
|
||||
private startRenderLoop() {
|
||||
const render = () => {
|
||||
if (!this.gl || !this.program || this.effects.size === 0) {
|
||||
this.animationFrame = requestAnimationFrame(render)
|
||||
return
|
||||
this.animationFrame = requestAnimationFrame(render);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTime = (performance.now() - this.startTime) / 1000
|
||||
const currentTime = (performance.now() - this.startTime) / 1000;
|
||||
|
||||
// Render each effect
|
||||
for (const [id, effect] of Array.from(this.effects)) {
|
||||
const mousePos = this.mousePositions.get(id) || { x: 0, y: 0 }
|
||||
const mousePos = this.mousePositions.get(id) || { x: 0, y: 0 };
|
||||
|
||||
// Skip rendering if dimensions are invalid
|
||||
if (effect.width <= 0 || effect.height <= 0) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set canvas size if needed
|
||||
|
|
@ -236,14 +236,14 @@ class GlassEffectManager {
|
|||
this.canvas!.width !== effect.width ||
|
||||
this.canvas!.height !== effect.height
|
||||
) {
|
||||
this.canvas!.width = effect.width
|
||||
this.canvas!.height = effect.height
|
||||
this.gl.viewport(0, 0, effect.width, effect.height)
|
||||
this.canvas!.width = effect.width;
|
||||
this.canvas!.height = effect.height;
|
||||
this.gl.viewport(0, 0, effect.width, effect.height);
|
||||
}
|
||||
|
||||
// Clear and render
|
||||
this.gl.clearColor(0, 0, 0, 0)
|
||||
this.gl.clear(this.gl.COLOR_BUFFER_BIT)
|
||||
this.gl.clearColor(0, 0, 0, 0);
|
||||
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
|
||||
|
||||
// Set uniforms
|
||||
if (this.uniforms.resolution) {
|
||||
|
|
@ -251,58 +251,58 @@ class GlassEffectManager {
|
|||
this.uniforms.resolution,
|
||||
effect.width,
|
||||
effect.height,
|
||||
)
|
||||
);
|
||||
}
|
||||
if (this.uniforms.time) {
|
||||
this.gl.uniform1f(this.uniforms.time, currentTime)
|
||||
this.gl.uniform1f(this.uniforms.time, currentTime);
|
||||
}
|
||||
if (this.uniforms.mouse) {
|
||||
this.gl.uniform2f(this.uniforms.mouse, mousePos.x, mousePos.y)
|
||||
this.gl.uniform2f(this.uniforms.mouse, mousePos.x, mousePos.y);
|
||||
}
|
||||
if (this.uniforms.expanded) {
|
||||
this.gl.uniform1f(
|
||||
this.uniforms.expanded,
|
||||
effect.isExpanded ? 1.0 : 0.0,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Draw
|
||||
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4)
|
||||
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
|
||||
|
||||
// Copy to target canvas
|
||||
const targetCtx = effect.targetCanvas.getContext("2d")
|
||||
const targetCtx = effect.targetCanvas.getContext("2d");
|
||||
if (targetCtx) {
|
||||
targetCtx.clearRect(0, 0, effect.width, effect.height)
|
||||
targetCtx.drawImage(this.canvas!, 0, 0)
|
||||
targetCtx.clearRect(0, 0, effect.width, effect.height);
|
||||
targetCtx.drawImage(this.canvas!, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
this.animationFrame = requestAnimationFrame(render)
|
||||
}
|
||||
this.animationFrame = requestAnimationFrame(render);
|
||||
};
|
||||
|
||||
render()
|
||||
render();
|
||||
}
|
||||
|
||||
// Clean up method (optional, for when the app unmounts)
|
||||
destroy() {
|
||||
if (this.animationFrame) {
|
||||
cancelAnimationFrame(this.animationFrame)
|
||||
cancelAnimationFrame(this.animationFrame);
|
||||
}
|
||||
if (this.gl && this.program) {
|
||||
this.gl.deleteProgram(this.program)
|
||||
this.gl.deleteProgram(this.program);
|
||||
}
|
||||
this.effects.clear()
|
||||
this.mousePositions.clear()
|
||||
GlassEffectManager.instance = null
|
||||
this.effects.clear();
|
||||
this.mousePositions.clear();
|
||||
GlassEffectManager.instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
interface EffectInstance {
|
||||
id: string
|
||||
targetCanvas: HTMLCanvasElement
|
||||
isExpanded: boolean
|
||||
width: number
|
||||
height: number
|
||||
id: string;
|
||||
targetCanvas: HTMLCanvasElement;
|
||||
isExpanded: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export default GlassEffectManager
|
||||
export default GlassEffectManager;
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { usePathname, useSearchParams } from "next/navigation"
|
||||
import posthog from "posthog-js"
|
||||
import { Suspense, useEffect } from "react"
|
||||
import { useSession } from "./auth"
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import posthog from "posthog-js";
|
||||
import { Suspense, useEffect } from "react";
|
||||
import { useSession } from "./auth";
|
||||
|
||||
function PostHogPageTracking() {
|
||||
const pathname = usePathname()
|
||||
const searchParams = useSearchParams()
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
// Page tracking
|
||||
useEffect(() => {
|
||||
if (pathname) {
|
||||
let url = window.origin + pathname
|
||||
let url = window.origin + pathname;
|
||||
if (searchParams.toString()) {
|
||||
url = `${url}?${searchParams.toString()}`
|
||||
url = `${url}?${searchParams.toString()}`;
|
||||
}
|
||||
|
||||
// Extract page context for better tracking
|
||||
|
|
@ -24,17 +24,17 @@ function PostHogPageTracking() {
|
|||
search_params: searchParams.toString(),
|
||||
page_type: getPageType(pathname),
|
||||
org_slug: getOrgSlug(pathname),
|
||||
}
|
||||
};
|
||||
|
||||
posthog.capture("$pageview", pageContext)
|
||||
posthog.capture("$pageview", pageContext);
|
||||
}
|
||||
}, [pathname, searchParams])
|
||||
}, [pathname, searchParams]);
|
||||
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
||||
const { data: session } = useSession()
|
||||
const { data: session } = useSession();
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
|
|
@ -44,9 +44,9 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
|||
person_profiles: "identified_only",
|
||||
capture_pageview: false,
|
||||
capture_pageleave: true,
|
||||
})
|
||||
});
|
||||
}
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
// User identification
|
||||
useEffect(() => {
|
||||
|
|
@ -56,9 +56,9 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
|||
name: session.user.name,
|
||||
userId: session.user.id,
|
||||
createdAt: session.user.createdAt,
|
||||
})
|
||||
});
|
||||
}
|
||||
}, [session?.user])
|
||||
}, [session?.user]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -67,18 +67,18 @@ export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
|||
</Suspense>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function getPageType(pathname: string): string {
|
||||
return "other"
|
||||
return "other";
|
||||
}
|
||||
|
||||
function getOrgSlug(pathname: string): string | null {
|
||||
const match = pathname.match(/^\/([^/]+)\//)
|
||||
return match ? (match[1] ?? null) : null
|
||||
const match = pathname.match(/^\/([^/]+)\//);
|
||||
return match ? (match[1] ?? null) : null;
|
||||
}
|
||||
|
||||
export function usePostHog() {
|
||||
return posthog
|
||||
return posthog;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import type { useCustomer } from "autumn-js/react"
|
||||
import { toast } from "sonner"
|
||||
import type { z } from "zod"
|
||||
import type { DocumentsWithMemoriesResponseSchema } from "../validation/api"
|
||||
import { $fetch } from "./api"
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { useCustomer } from "autumn-js/react";
|
||||
import { toast } from "sonner";
|
||||
import type { z } from "zod";
|
||||
import type { DocumentsWithMemoriesResponseSchema } from "../validation/api";
|
||||
import { $fetch } from "./api";
|
||||
|
||||
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
|
||||
type DocumentWithMemories = DocumentsResponse["documents"][0]
|
||||
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>;
|
||||
type DocumentWithMemories = DocumentsResponse["documents"][0];
|
||||
|
||||
export const fetchSubscriptionStatus = (
|
||||
autumn: ReturnType<typeof useCustomer>,
|
||||
|
|
@ -18,54 +18,54 @@ export const fetchSubscriptionStatus = (
|
|||
"memory_starter",
|
||||
"memory_growth",
|
||||
"consumer_pro",
|
||||
]
|
||||
const statusMap: Record<string, boolean | null> = {}
|
||||
];
|
||||
const statusMap: Record<string, boolean | null> = {};
|
||||
|
||||
await Promise.all(
|
||||
allPlans.map(async (plan) => {
|
||||
try {
|
||||
const res = await autumn.check({
|
||||
productId: plan,
|
||||
})
|
||||
statusMap[plan] = res.data?.allowed ?? false
|
||||
});
|
||||
statusMap[plan] = res.data?.allowed ?? false;
|
||||
} catch (error) {
|
||||
console.error(`Error checking status for ${plan}:`, error)
|
||||
statusMap[plan] = false
|
||||
console.error(`Error checking status for ${plan}:`, error);
|
||||
statusMap[plan] = false;
|
||||
}
|
||||
}),
|
||||
)
|
||||
);
|
||||
|
||||
return statusMap
|
||||
return statusMap;
|
||||
},
|
||||
queryKey: ["subscription-status"],
|
||||
refetchInterval: 5000, // Refetch every 5 seconds
|
||||
staleTime: 4000, // Consider data stale after 4 seconds
|
||||
})
|
||||
});
|
||||
|
||||
// Feature checks
|
||||
export const fetchMemoriesFeature = (autumn: ReturnType<typeof useCustomer>) =>
|
||||
useQuery({
|
||||
queryFn: async () => {
|
||||
const res = await autumn.check({ featureId: "memories" })
|
||||
return res.data
|
||||
const res = await autumn.check({ featureId: "memories" });
|
||||
return res.data;
|
||||
},
|
||||
queryKey: ["autumn-feature", "memories"],
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
gcTime: 5 * 60 * 1000, // 5 minutes
|
||||
})
|
||||
});
|
||||
|
||||
export const fetchConnectionsFeature = (
|
||||
autumn: ReturnType<typeof useCustomer>,
|
||||
) =>
|
||||
useQuery({
|
||||
queryFn: async () => {
|
||||
const res = await autumn.check({ featureId: "connections" })
|
||||
return res.data
|
||||
const res = await autumn.check({ featureId: "connections" });
|
||||
return res.data;
|
||||
},
|
||||
queryKey: ["autumn-feature", "connections"],
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
gcTime: 5 * 60 * 1000, // 5 minutes
|
||||
})
|
||||
});
|
||||
|
||||
// Product checks
|
||||
export const fetchConsumerProProduct = (
|
||||
|
|
@ -73,54 +73,54 @@ export const fetchConsumerProProduct = (
|
|||
) =>
|
||||
useQuery({
|
||||
queryFn: async () => {
|
||||
const res = await autumn.check({ productId: "consumer_pro" })
|
||||
return res.data
|
||||
const res = await autumn.check({ productId: "consumer_pro" });
|
||||
return res.data;
|
||||
},
|
||||
queryKey: ["autumn-product", "consumer_pro"],
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
gcTime: 5 * 60 * 1000, // 5 minutes
|
||||
})
|
||||
});
|
||||
|
||||
export const fetchProProduct = (autumn: ReturnType<typeof useCustomer>) =>
|
||||
useQuery({
|
||||
queryFn: async () => {
|
||||
const res = await autumn.check({ productId: "pro" })
|
||||
return res.data
|
||||
const res = await autumn.check({ productId: "pro" });
|
||||
return res.data;
|
||||
},
|
||||
queryKey: ["autumn-product", "pro"],
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
gcTime: 5 * 60 * 1000, // 5 minutes
|
||||
})
|
||||
});
|
||||
|
||||
export const useDeleteDocument = (selectedProject: string) => {
|
||||
const queryClient = useQueryClient()
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (documentId: string) => {
|
||||
// context for LLM: delete/memories/:documentId is documents delete endpoint not memories delete endpoint
|
||||
const response = await $fetch(`@delete/documents/${documentId}`)
|
||||
const response = await $fetch(`@delete/documents/${documentId}`);
|
||||
if (response.error) {
|
||||
throw new Error(response.error?.message || "Failed to delete document")
|
||||
throw new Error(response.error?.message || "Failed to delete document");
|
||||
}
|
||||
return response.data
|
||||
return response.data;
|
||||
},
|
||||
onMutate: async (documentId: string) => {
|
||||
await queryClient.cancelQueries({
|
||||
queryKey: ["documents-with-memories", selectedProject],
|
||||
})
|
||||
});
|
||||
|
||||
const previousData = queryClient.getQueryData([
|
||||
"documents-with-memories",
|
||||
selectedProject,
|
||||
])
|
||||
]);
|
||||
|
||||
queryClient.setQueryData(
|
||||
["documents-with-memories", selectedProject],
|
||||
(old: unknown) => {
|
||||
if (!old || typeof old !== "object") return old
|
||||
if (!old || typeof old !== "object") return old;
|
||||
const typedOld = old as {
|
||||
pages?: Array<{ documents?: DocumentWithMemories[] }>
|
||||
}
|
||||
pages?: Array<{ documents?: DocumentWithMemories[] }>;
|
||||
};
|
||||
return {
|
||||
...typedOld,
|
||||
pages: typedOld.pages?.map((page) => ({
|
||||
|
|
@ -129,30 +129,30 @@ export const useDeleteDocument = (selectedProject: string) => {
|
|||
(doc: DocumentWithMemories) => doc.id !== documentId,
|
||||
),
|
||||
})),
|
||||
}
|
||||
};
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
return { previousData }
|
||||
return { previousData };
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("Memory deleted successfully")
|
||||
toast.success("Memory deleted successfully");
|
||||
},
|
||||
onError: (error, _documentId, context) => {
|
||||
if (context?.previousData) {
|
||||
queryClient.setQueryData(
|
||||
["documents-with-memories", selectedProject],
|
||||
context.previousData,
|
||||
)
|
||||
);
|
||||
}
|
||||
toast.error("Failed to delete memory", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
});
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["documents-with-memories", selectedProject],
|
||||
})
|
||||
});
|
||||
},
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
|
||||
import { useState } from "react"
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { useState } from "react";
|
||||
|
||||
export const QueryProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [queryClient] = useState(
|
||||
|
|
@ -16,12 +16,12 @@ export const QueryProvider = ({ children }: { children: React.ReactNode }) => {
|
|||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}%)`;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import Supermemory from "supermemory"
|
||||
import { tool } from "ai"
|
||||
import { z } from "zod"
|
||||
import { tool } from "ai";
|
||||
import Supermemory from "supermemory";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
DEFAULT_VALUES,
|
||||
getContainerTags,
|
||||
PARAMETER_DESCRIPTIONS,
|
||||
TOOL_DESCRIPTIONS,
|
||||
getContainerTags,
|
||||
} from "./shared"
|
||||
import type { SupermemoryToolsConfig } from "./types"
|
||||
} from "./shared";
|
||||
import type { SupermemoryToolsConfig } from "./types";
|
||||
|
||||
// Export individual tool creators
|
||||
export const searchMemoriesTool = (
|
||||
|
|
@ -17,9 +17,9 @@ export const searchMemoriesTool = (
|
|||
const client = new Supermemory({
|
||||
apiKey,
|
||||
...(config?.baseUrl ? { baseURL: config.baseUrl } : {}),
|
||||
})
|
||||
});
|
||||
|
||||
const containerTags = getContainerTags(config)
|
||||
const containerTags = getContainerTags(config);
|
||||
|
||||
return tool({
|
||||
description: TOOL_DESCRIPTIONS.searchMemories,
|
||||
|
|
@ -50,22 +50,22 @@ export const searchMemoriesTool = (
|
|||
limit,
|
||||
chunkThreshold: DEFAULT_VALUES.chunkThreshold,
|
||||
includeFullDocs,
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
results: response.results,
|
||||
count: response.results?.length || 0,
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const addMemoryTool = (
|
||||
apiKey: string,
|
||||
|
|
@ -74,9 +74,9 @@ export const addMemoryTool = (
|
|||
const client = new Supermemory({
|
||||
apiKey,
|
||||
...(config?.baseUrl ? { baseURL: config.baseUrl } : {}),
|
||||
})
|
||||
});
|
||||
|
||||
const containerTags = getContainerTags(config)
|
||||
const containerTags = getContainerTags(config);
|
||||
|
||||
return tool({
|
||||
description: TOOL_DESCRIPTIONS.addMemory,
|
||||
|
|
@ -85,27 +85,27 @@ export const addMemoryTool = (
|
|||
}),
|
||||
execute: async ({ memory }) => {
|
||||
try {
|
||||
const metadata: Record<string, string | number | boolean> = {}
|
||||
const metadata: Record<string, string | number | boolean> = {};
|
||||
|
||||
const response = await client.memories.add({
|
||||
content: memory,
|
||||
containerTags,
|
||||
...(Object.keys(metadata).length > 0 && { metadata }),
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
memory: response,
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create Supermemory tools for AI SDK
|
||||
|
|
@ -117,5 +117,5 @@ export function supermemoryTools(
|
|||
return {
|
||||
searchMemories: searchMemoriesTool(apiKey, config),
|
||||
addMemory: addMemoryTool(apiKey, config),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
// Export shared types and utilities
|
||||
export type { SupermemoryToolsConfig } from "./types"
|
||||
export type { SupermemoryToolsConfig } from "./types";
|
||||
|
|
|
|||
|
|
@ -1,27 +1,27 @@
|
|||
import type OpenAI from "openai"
|
||||
import Supermemory from "supermemory"
|
||||
import type OpenAI from "openai";
|
||||
import Supermemory from "supermemory";
|
||||
import {
|
||||
DEFAULT_VALUES,
|
||||
getContainerTags,
|
||||
PARAMETER_DESCRIPTIONS,
|
||||
TOOL_DESCRIPTIONS,
|
||||
getContainerTags,
|
||||
} from "./shared"
|
||||
import type { SupermemoryToolsConfig } from "./types"
|
||||
} from "./shared";
|
||||
import type { SupermemoryToolsConfig } from "./types";
|
||||
|
||||
/**
|
||||
* Result types for memory operations
|
||||
*/
|
||||
export interface MemorySearchResult {
|
||||
success: boolean
|
||||
results?: Awaited<ReturnType<Supermemory["search"]["execute"]>>["results"]
|
||||
count?: number
|
||||
error?: string
|
||||
success: boolean;
|
||||
results?: Awaited<ReturnType<Supermemory["search"]["execute"]>>["results"];
|
||||
count?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface MemoryAddResult {
|
||||
success: boolean
|
||||
memory?: Awaited<ReturnType<Supermemory["memories"]["add"]>>
|
||||
error?: string
|
||||
success: boolean;
|
||||
memory?: Awaited<ReturnType<Supermemory["memories"]["add"]>>;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -67,7 +67,7 @@ export const memoryToolSchemas = {
|
|||
required: ["memory"],
|
||||
},
|
||||
} satisfies OpenAI.FunctionDefinition,
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Create a Supermemory client with configuration
|
||||
|
|
@ -76,11 +76,11 @@ function createClient(apiKey: string, config?: SupermemoryToolsConfig) {
|
|||
const client = new Supermemory({
|
||||
apiKey,
|
||||
...(config?.baseUrl && { baseURL: config.baseUrl }),
|
||||
})
|
||||
});
|
||||
|
||||
const containerTags = getContainerTags(config)
|
||||
const containerTags = getContainerTags(config);
|
||||
|
||||
return { client, containerTags }
|
||||
return { client, containerTags };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -90,16 +90,16 @@ export function createSearchMemoriesFunction(
|
|||
apiKey: string,
|
||||
config?: SupermemoryToolsConfig,
|
||||
) {
|
||||
const { client, containerTags } = createClient(apiKey, config)
|
||||
const { client, containerTags } = createClient(apiKey, config);
|
||||
|
||||
return async function searchMemories({
|
||||
informationToGet,
|
||||
includeFullDocs = DEFAULT_VALUES.includeFullDocs,
|
||||
limit = DEFAULT_VALUES.limit,
|
||||
}: {
|
||||
informationToGet: string
|
||||
includeFullDocs?: boolean
|
||||
limit?: number
|
||||
informationToGet: string;
|
||||
includeFullDocs?: boolean;
|
||||
limit?: number;
|
||||
}): Promise<MemorySearchResult> {
|
||||
try {
|
||||
const response = await client.search.execute({
|
||||
|
|
@ -108,20 +108,20 @@ export function createSearchMemoriesFunction(
|
|||
limit,
|
||||
chunkThreshold: DEFAULT_VALUES.chunkThreshold,
|
||||
includeFullDocs,
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
results: response.results,
|
||||
count: response.results?.length || 0,
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -131,33 +131,33 @@ export function createAddMemoryFunction(
|
|||
apiKey: string,
|
||||
config?: SupermemoryToolsConfig,
|
||||
) {
|
||||
const { client, containerTags } = createClient(apiKey, config)
|
||||
const { client, containerTags } = createClient(apiKey, config);
|
||||
|
||||
return async function addMemory({
|
||||
memory,
|
||||
}: {
|
||||
memory: string
|
||||
memory: string;
|
||||
}): Promise<MemoryAddResult> {
|
||||
try {
|
||||
const metadata: Record<string, string | number | boolean> = {}
|
||||
const metadata: Record<string, string | number | boolean> = {};
|
||||
|
||||
const response = await client.memories.add({
|
||||
content: memory,
|
||||
containerTags,
|
||||
...(Object.keys(metadata).length > 0 && { metadata }),
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
memory: response,
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -167,13 +167,13 @@ export function supermemoryTools(
|
|||
apiKey: string,
|
||||
config?: SupermemoryToolsConfig,
|
||||
) {
|
||||
const searchMemories = createSearchMemoriesFunction(apiKey, config)
|
||||
const addMemory = createAddMemoryFunction(apiKey, config)
|
||||
const searchMemories = createSearchMemoriesFunction(apiKey, config);
|
||||
const addMemory = createAddMemoryFunction(apiKey, config);
|
||||
|
||||
return {
|
||||
searchMemories,
|
||||
addMemory,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -183,7 +183,7 @@ export function getToolDefinitions(): OpenAI.Chat.Completions.ChatCompletionTool
|
|||
return [
|
||||
{ type: "function", function: memoryToolSchemas.searchMemories },
|
||||
{ type: "function", function: memoryToolSchemas.addMemory },
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -193,26 +193,26 @@ export function createToolCallExecutor(
|
|||
apiKey: string,
|
||||
config?: SupermemoryToolsConfig,
|
||||
) {
|
||||
const tools = supermemoryTools(apiKey, config)
|
||||
const tools = supermemoryTools(apiKey, config);
|
||||
|
||||
return async function executeToolCall(
|
||||
toolCall: OpenAI.Chat.Completions.ChatCompletionMessageToolCall,
|
||||
): Promise<string> {
|
||||
const functionName = toolCall.function.name
|
||||
const args = JSON.parse(toolCall.function.arguments)
|
||||
const functionName = toolCall.function.name;
|
||||
const args = JSON.parse(toolCall.function.arguments);
|
||||
|
||||
switch (functionName) {
|
||||
case "searchMemories":
|
||||
return JSON.stringify(await tools.searchMemories(args))
|
||||
return JSON.stringify(await tools.searchMemories(args));
|
||||
case "addMemory":
|
||||
return JSON.stringify(await tools.addMemory(args))
|
||||
return JSON.stringify(await tools.addMemory(args));
|
||||
default:
|
||||
return JSON.stringify({
|
||||
success: false,
|
||||
error: `Unknown function: ${functionName}`,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -222,24 +222,24 @@ export function createToolCallsExecutor(
|
|||
apiKey: string,
|
||||
config?: SupermemoryToolsConfig,
|
||||
) {
|
||||
const executeToolCall = createToolCallExecutor(apiKey, config)
|
||||
const executeToolCall = createToolCallExecutor(apiKey, config);
|
||||
|
||||
return async function executeToolCalls(
|
||||
toolCalls: OpenAI.Chat.Completions.ChatCompletionMessageToolCall[],
|
||||
): Promise<OpenAI.Chat.Completions.ChatCompletionToolMessageParam[]> {
|
||||
const results = await Promise.all(
|
||||
toolCalls.map(async (toolCall) => {
|
||||
const result = await executeToolCall(toolCall)
|
||||
const result = await executeToolCall(toolCall);
|
||||
return {
|
||||
tool_call_id: toolCall.id,
|
||||
role: "tool" as const,
|
||||
content: result,
|
||||
}
|
||||
};
|
||||
}),
|
||||
)
|
||||
);
|
||||
|
||||
return results
|
||||
}
|
||||
return results;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -249,7 +249,7 @@ export function createSearchMemoriesTool(
|
|||
apiKey: string,
|
||||
config?: SupermemoryToolsConfig,
|
||||
) {
|
||||
const searchMemories = createSearchMemoriesFunction(apiKey, config)
|
||||
const searchMemories = createSearchMemoriesFunction(apiKey, config);
|
||||
|
||||
return {
|
||||
definition: {
|
||||
|
|
@ -257,14 +257,14 @@ export function createSearchMemoriesTool(
|
|||
function: memoryToolSchemas.searchMemories,
|
||||
},
|
||||
execute: searchMemories,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createAddMemoryTool(
|
||||
apiKey: string,
|
||||
config?: SupermemoryToolsConfig,
|
||||
) {
|
||||
const addMemory = createAddMemoryFunction(apiKey, config)
|
||||
const addMemory = createAddMemoryFunction(apiKey, config);
|
||||
|
||||
return {
|
||||
definition: {
|
||||
|
|
@ -272,5 +272,5 @@ export function createAddMemoryTool(
|
|||
function: memoryToolSchemas.addMemory,
|
||||
},
|
||||
execute: addMemory,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -29,26 +29,26 @@ export function useGraphData(
|
|||
const allEdges: GraphEdge[] = [];
|
||||
|
||||
// Filter documents that have memories in selected space
|
||||
const filteredDocuments = data.documents
|
||||
const filteredDocuments = (data.documents || [])
|
||||
.map((doc) => ({
|
||||
...doc,
|
||||
memoryEntries:
|
||||
selectedSpace === "all"
|
||||
? doc.memoryEntries
|
||||
: doc.memoryEntries.filter(
|
||||
? doc.memoryEntries || []
|
||||
: (doc.memoryEntries || []).filter(
|
||||
(memory) =>
|
||||
(memory.spaceContainerTag ?? memory.spaceId ?? "default") ===
|
||||
selectedSpace,
|
||||
),
|
||||
}))
|
||||
.filter((doc) => doc.memoryEntries.length > 0);
|
||||
.filter((doc) => (doc.memoryEntries || []).length > 0);
|
||||
|
||||
// Group documents by space for better clustering
|
||||
const documentsBySpace = new Map<string, typeof filteredDocuments>();
|
||||
filteredDocuments.forEach((doc) => {
|
||||
const docSpace =
|
||||
doc.memoryEntries[0]?.spaceContainerTag ??
|
||||
doc.memoryEntries[0]?.spaceId ??
|
||||
(doc.memoryEntries || [])[0]?.spaceContainerTag ??
|
||||
(doc.memoryEntries || [])[0]?.spaceId ??
|
||||
"default";
|
||||
if (!documentsBySpace.has(docSpace)) {
|
||||
documentsBySpace.set(docSpace, []);
|
||||
|
|
@ -171,7 +171,7 @@ export function useGraphData(
|
|||
const memoryNodeMap = new Map<string, GraphNode>();
|
||||
const doc = docNode.data as DocumentWithMemories;
|
||||
|
||||
doc.memoryEntries.forEach((memory, memIndex) => {
|
||||
(doc.memoryEntries || []).forEach((memory, memIndex) => {
|
||||
const memoryId = `${memory.id}`;
|
||||
const customMemPos = nodePositions.get(memoryId);
|
||||
|
||||
|
|
@ -231,8 +231,8 @@ export function useGraphData(
|
|||
});
|
||||
|
||||
// Add version-chain edges (old -> new)
|
||||
data.documents.forEach((doc) => {
|
||||
doc.memoryEntries.forEach((mem: MemoryEntry) => {
|
||||
(data.documents || []).forEach((doc) => {
|
||||
(doc.memoryEntries || []).forEach((mem: MemoryEntry) => {
|
||||
// Support both new object structure and legacy array/single parent fields
|
||||
let parentRelations: Record<string, MemoryRelation> = {};
|
||||
|
||||
|
|
|
|||
|
|
@ -157,8 +157,8 @@ export const MemoryGraph = ({
|
|||
const spaceSet = new Set<string>();
|
||||
const counts: Record<string, number> = {};
|
||||
|
||||
data.documents.forEach((doc) => {
|
||||
doc.memoryEntries.forEach((memory) => {
|
||||
(data.documents || []).forEach((doc) => {
|
||||
(doc.memoryEntries || []).forEach((memory) => {
|
||||
const spaceId = memory.spaceContainerTag || memory.spaceId || "default";
|
||||
spaceSet.add(spaceId);
|
||||
counts[spaceId] = (counts[spaceId] || 0) + 1;
|
||||
|
|
@ -199,32 +199,47 @@ export const MemoryGraph = ({
|
|||
const handleCenter = useCallback(() => {
|
||||
if (nodes.length > 0) {
|
||||
// Calculate center of all nodes
|
||||
let sumX = 0
|
||||
let sumY = 0
|
||||
let count = 0
|
||||
|
||||
let sumX = 0;
|
||||
let sumY = 0;
|
||||
let count = 0;
|
||||
|
||||
nodes.forEach((node) => {
|
||||
sumX += node.x
|
||||
sumY += node.y
|
||||
count++
|
||||
})
|
||||
|
||||
sumX += node.x;
|
||||
sumY += node.y;
|
||||
count++;
|
||||
});
|
||||
|
||||
if (count > 0) {
|
||||
const centerX = sumX / count
|
||||
const centerY = sumY / count
|
||||
centerViewportOn(centerX, centerY, containerSize.width, containerSize.height)
|
||||
const centerX = sumX / count;
|
||||
const centerY = sumY / count;
|
||||
centerViewportOn(
|
||||
centerX,
|
||||
centerY,
|
||||
containerSize.width,
|
||||
containerSize.height,
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [nodes, centerViewportOn, containerSize.width, containerSize.height])
|
||||
}, [nodes, centerViewportOn, containerSize.width, containerSize.height]);
|
||||
|
||||
const handleAutoFit = useCallback(() => {
|
||||
if (nodes.length > 0 && containerSize.width > 0 && containerSize.height > 0) {
|
||||
if (
|
||||
nodes.length > 0 &&
|
||||
containerSize.width > 0 &&
|
||||
containerSize.height > 0
|
||||
) {
|
||||
autoFitToViewport(nodes, containerSize.width, containerSize.height, {
|
||||
occludedRightPx,
|
||||
animate: true,
|
||||
})
|
||||
});
|
||||
}
|
||||
}, [nodes, containerSize.width, containerSize.height, occludedRightPx, autoFitToViewport])
|
||||
}, [
|
||||
nodes,
|
||||
containerSize.width,
|
||||
containerSize.height,
|
||||
occludedRightPx,
|
||||
autoFitToViewport,
|
||||
]);
|
||||
|
||||
// Get selected node data
|
||||
const selectedNodeData = useMemo(() => {
|
||||
|
|
@ -251,7 +266,7 @@ export const MemoryGraph = ({
|
|||
};
|
||||
|
||||
// Count visible documents
|
||||
const visibleDocuments = data.documents.filter((doc) => {
|
||||
const visibleDocuments = (data.documents || []).filter((doc) => {
|
||||
const docNodes = nodes.filter(
|
||||
(node) => node.type === "document" && node.data.id === doc.id,
|
||||
);
|
||||
|
|
@ -265,7 +280,8 @@ export const MemoryGraph = ({
|
|||
});
|
||||
|
||||
// If 80% or more of documents are visible, load more
|
||||
const visibilityRatio = visibleDocuments.length / data.documents.length;
|
||||
const visibilityRatio =
|
||||
visibleDocuments.length / (data.documents || []).length;
|
||||
if (visibilityRatio >= 0.8) {
|
||||
loadMoreDocuments();
|
||||
}
|
||||
|
|
@ -421,8 +437,12 @@ export const MemoryGraph = ({
|
|||
{containerSize.width > 0 && (
|
||||
<NavigationControls
|
||||
onCenter={handleCenter}
|
||||
onZoomIn={() => zoomIn(containerSize.width / 2, containerSize.height / 2)}
|
||||
onZoomOut={() => zoomOut(containerSize.width / 2, containerSize.height / 2)}
|
||||
onZoomIn={() =>
|
||||
zoomIn(containerSize.width / 2, containerSize.height / 2)
|
||||
}
|
||||
onZoomOut={() =>
|
||||
zoomOut(containerSize.width / 2, containerSize.height / 2)
|
||||
}
|
||||
onAutoFit={handleAutoFit}
|
||||
nodes={nodes}
|
||||
className="absolute bottom-4 left-4"
|
||||
|
|
|
|||
|
|
@ -1,67 +1,62 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { memo } from "react"
|
||||
import type { GraphNode } from "./types"
|
||||
import { memo } from "react";
|
||||
import type { GraphNode } from "./types";
|
||||
|
||||
interface NavigationControlsProps {
|
||||
onCenter: () => void
|
||||
onZoomIn: () => void
|
||||
onZoomOut: () => void
|
||||
onAutoFit: () => void
|
||||
nodes: GraphNode[]
|
||||
className?: string
|
||||
onCenter: () => void;
|
||||
onZoomIn: () => void;
|
||||
onZoomOut: () => void;
|
||||
onAutoFit: () => void;
|
||||
nodes: GraphNode[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const NavigationControls = memo<NavigationControlsProps>(({
|
||||
onCenter,
|
||||
onZoomIn,
|
||||
onZoomOut,
|
||||
onAutoFit,
|
||||
nodes,
|
||||
className = "",
|
||||
}) => {
|
||||
if (nodes.length === 0) {
|
||||
return null
|
||||
}
|
||||
export const NavigationControls = memo<NavigationControlsProps>(
|
||||
({ onCenter, onZoomIn, onZoomOut, onAutoFit, nodes, className = "" }) => {
|
||||
if (nodes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col gap-1 ${className}`}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAutoFit}
|
||||
className="bg-black/20 backdrop-blur-sm hover:bg-black/30 border border-white/10 hover:border-white/20 rounded-lg p-2 text-white/70 hover:text-white transition-colors text-xs font-medium min-w-16"
|
||||
title="Auto-fit graph to viewport"
|
||||
>
|
||||
Fit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCenter}
|
||||
className="bg-black/20 backdrop-blur-sm hover:bg-black/30 border border-white/10 hover:border-white/20 rounded-lg p-2 text-white/70 hover:text-white transition-colors text-xs font-medium min-w-16"
|
||||
title="Center view on graph"
|
||||
>
|
||||
Center
|
||||
</button>
|
||||
<div className="flex flex-col">
|
||||
return (
|
||||
<div className={`flex flex-col gap-1 ${className}`}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onZoomIn}
|
||||
className="bg-black/20 backdrop-blur-sm hover:bg-black/30 border border-white/10 hover:border-white/20 rounded-t-lg p-2 text-white/70 hover:text-white transition-colors text-xs font-medium min-w-16 border-b-0"
|
||||
title="Zoom in"
|
||||
onClick={onAutoFit}
|
||||
className="bg-black/20 backdrop-blur-sm hover:bg-black/30 border border-white/10 hover:border-white/20 rounded-lg p-2 text-white/70 hover:text-white transition-colors text-xs font-medium min-w-16"
|
||||
title="Auto-fit graph to viewport"
|
||||
>
|
||||
+
|
||||
Fit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onZoomOut}
|
||||
className="bg-black/20 backdrop-blur-sm hover:bg-black/30 border border-white/10 hover:border-white/20 rounded-b-lg p-2 text-white/70 hover:text-white transition-colors text-xs font-medium min-w-16"
|
||||
title="Zoom out"
|
||||
onClick={onCenter}
|
||||
className="bg-black/20 backdrop-blur-sm hover:bg-black/30 border border-white/10 hover:border-white/20 rounded-lg p-2 text-white/70 hover:text-white transition-colors text-xs font-medium min-w-16"
|
||||
title="Center view on graph"
|
||||
>
|
||||
−
|
||||
Center
|
||||
</button>
|
||||
<div className="flex flex-col">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onZoomIn}
|
||||
className="bg-black/20 backdrop-blur-sm hover:bg-black/30 border border-white/10 hover:border-white/20 rounded-t-lg p-2 text-white/70 hover:text-white transition-colors text-xs font-medium min-w-16 border-b-0"
|
||||
title="Zoom in"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onZoomOut}
|
||||
className="bg-black/20 backdrop-blur-sm hover:bg-black/30 border border-white/10 hover:border-white/20 rounded-b-lg p-2 text-white/70 hover:text-white transition-colors text-xs font-medium min-w-16"
|
||||
title="Zoom out"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
NavigationControls.displayName = "NavigationControls"
|
||||
NavigationControls.displayName = "NavigationControls";
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
import { z } from "zod"
|
||||
import "zod-openapi/extend"
|
||||
import { z } from "zod";
|
||||
import "zod-openapi/extend";
|
||||
import {
|
||||
MetadataSchema as BaseMetadataSchema,
|
||||
DocumentSchema,
|
||||
MemoryEntrySchema,
|
||||
OrganizationSettingsSchema,
|
||||
RequestTypeEnum,
|
||||
} from "./schemas"
|
||||
} from "./schemas";
|
||||
|
||||
export const MetadataSchema = BaseMetadataSchema
|
||||
export const MetadataSchema = BaseMetadataSchema;
|
||||
|
||||
export const SearchFiltersSchema = z
|
||||
.object({
|
||||
AND: z.array(z.unknown()).optional(),
|
||||
OR: z.array(z.unknown()).optional(),
|
||||
})
|
||||
.or(z.record(z.unknown()))
|
||||
.or(z.record(z.unknown()));
|
||||
|
||||
const exampleMetadata: Record<string, string | number | boolean> = {
|
||||
category: "technology",
|
||||
|
|
@ -24,7 +24,7 @@ const exampleMetadata: Record<string, string | number | boolean> = {
|
|||
source: "web",
|
||||
tag_1: "ai",
|
||||
tag_2: "machine-learning",
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
const exampleMemory = {
|
||||
connectionId: "conn_123",
|
||||
|
|
@ -45,7 +45,7 @@ const exampleMemory = {
|
|||
type: "text",
|
||||
updatedAt: new Date().toISOString(),
|
||||
url: "https://example.com/article",
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
export const MemorySchema = z
|
||||
.object({
|
||||
|
|
@ -136,7 +136,7 @@ export const MemorySchema = z
|
|||
.openapi({
|
||||
description: "Memory object",
|
||||
example: exampleMemory,
|
||||
})
|
||||
});
|
||||
|
||||
export const MemoryUpdateSchema = z.object({
|
||||
containerTags: z
|
||||
|
|
@ -162,9 +162,9 @@ export const MemoryUpdateSchema = z.object({
|
|||
"Optional metadata for the memory. This is used to store additional information about the memory. You can use this to store any additional information you need about the memory. Metadata can be filtered through. Keys must be strings and are case sensitive. Values can be strings, numbers, or booleans. You cannot nest objects.",
|
||||
example: exampleMetadata,
|
||||
}),
|
||||
})
|
||||
});
|
||||
|
||||
export const MemoryAddSchema = MemoryUpdateSchema
|
||||
export const MemoryAddSchema = MemoryUpdateSchema;
|
||||
|
||||
export const PaginationSchema = z
|
||||
.object({
|
||||
|
|
@ -181,9 +181,9 @@ export const PaginationSchema = z
|
|||
totalItems: 100,
|
||||
totalPages: 10,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
export const GetMemoryResponseSchema = MemorySchema
|
||||
export const GetMemoryResponseSchema = MemorySchema;
|
||||
|
||||
export const ListMemoriesResponseSchema = z
|
||||
.object({
|
||||
|
|
@ -229,7 +229,7 @@ export const ListMemoriesResponseSchema = z
|
|||
totalPages: 10,
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
export const ListMemoriesQuerySchema = z
|
||||
.object({
|
||||
|
|
@ -317,12 +317,12 @@ export const ListMemoriesQuerySchema = z
|
|||
page: 1,
|
||||
sort: "createdAt",
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
export const MemoryResponseSchema = z.object({
|
||||
id: z.string(),
|
||||
status: z.string(),
|
||||
})
|
||||
});
|
||||
|
||||
export const SearchRequestSchema = z.object({
|
||||
categoriesFilter: z
|
||||
|
|
@ -455,7 +455,7 @@ export const SearchRequestSchema = z.object({
|
|||
"If true, rewrites the query to make it easier to find documents. This increases the latency by about 400ms",
|
||||
example: false,
|
||||
}),
|
||||
})
|
||||
});
|
||||
|
||||
export const Searchv4RequestSchema = z.object({
|
||||
containerTag: z.string().optional().openapi({
|
||||
|
|
@ -546,7 +546,7 @@ export const Searchv4RequestSchema = z.object({
|
|||
"If true, rewrites the query to make it easier to find documents. This increases the latency by about 400ms",
|
||||
example: false,
|
||||
}),
|
||||
})
|
||||
});
|
||||
|
||||
export const SearchResultSchema = z.object({
|
||||
chunks: z
|
||||
|
|
@ -632,13 +632,13 @@ export const SearchResultSchema = z.object({
|
|||
description: "Document type",
|
||||
example: "web",
|
||||
}),
|
||||
})
|
||||
});
|
||||
|
||||
export const SearchResponseSchema = z.object({
|
||||
results: z.array(SearchResultSchema),
|
||||
timing: z.number(),
|
||||
total: z.number(),
|
||||
})
|
||||
});
|
||||
|
||||
// V4 Memory Search Schemas
|
||||
export const MemorySearchDocumentSchema = z.object({
|
||||
|
|
@ -666,7 +666,7 @@ export const MemorySearchDocumentSchema = z.object({
|
|||
description: "Document last update date",
|
||||
format: "date-time",
|
||||
}),
|
||||
})
|
||||
});
|
||||
|
||||
export const MemorySearchResult = z.object({
|
||||
id: z.string().openapi({
|
||||
|
|
@ -765,7 +765,7 @@ export const MemorySearchResult = z.object({
|
|||
documents: z.array(MemorySearchDocumentSchema).optional().openapi({
|
||||
description: "Associated documents for this memory entry",
|
||||
}),
|
||||
})
|
||||
});
|
||||
|
||||
export const MemorySearchResponseSchema = z.object({
|
||||
results: z.array(MemorySearchResult).openapi({
|
||||
|
|
@ -779,7 +779,7 @@ export const MemorySearchResponseSchema = z.object({
|
|||
description: "Total number of results returned",
|
||||
example: 5,
|
||||
}),
|
||||
})
|
||||
});
|
||||
|
||||
export const ErrorResponseSchema = z.object({
|
||||
details: z.string().optional().openapi({
|
||||
|
|
@ -790,15 +790,15 @@ export const ErrorResponseSchema = z.object({
|
|||
description: "Error message",
|
||||
example: "Invalid request parameters",
|
||||
}),
|
||||
})
|
||||
});
|
||||
|
||||
export type SearchResult = z.infer<typeof SearchResultSchema>
|
||||
export type SearchResult = z.infer<typeof SearchResultSchema>;
|
||||
|
||||
export const SettingsRequestSchema = OrganizationSettingsSchema.omit({
|
||||
id: true,
|
||||
orgId: true,
|
||||
updatedAt: true,
|
||||
})
|
||||
});
|
||||
|
||||
export const ConnectionResponseSchema = z.object({
|
||||
createdAt: z.string().datetime(),
|
||||
|
|
@ -808,21 +808,21 @@ export const ConnectionResponseSchema = z.object({
|
|||
id: z.string(),
|
||||
metadata: z.record(z.any()).optional(),
|
||||
provider: z.string(),
|
||||
})
|
||||
});
|
||||
|
||||
export const RequestTypeSchema = RequestTypeEnum
|
||||
export const RequestTypeSchema = RequestTypeEnum;
|
||||
|
||||
export const HourlyAnalyticsSchema = z.object({
|
||||
count: z.number(),
|
||||
hour: z.union([z.date(), z.string()]),
|
||||
})
|
||||
});
|
||||
|
||||
export const ApiKeyAnalyticsBaseSchema = z.object({
|
||||
count: z.number(),
|
||||
keyId: z.string(),
|
||||
keyName: z.string().nullable(),
|
||||
lastUsed: z.union([z.date(), z.string()]).nullable(),
|
||||
})
|
||||
});
|
||||
|
||||
export const AnalyticsUsageResponseSchema = z.object({
|
||||
byKey: z.array(
|
||||
|
|
@ -845,7 +845,7 @@ export const AnalyticsUsageResponseSchema = z.object({
|
|||
type: RequestTypeSchema,
|
||||
}),
|
||||
),
|
||||
})
|
||||
});
|
||||
|
||||
export const AnalyticsErrorResponseSchema = z.object({
|
||||
byKey: z.array(
|
||||
|
|
@ -878,7 +878,7 @@ export const AnalyticsErrorResponseSchema = z.object({
|
|||
type: RequestTypeSchema,
|
||||
}),
|
||||
),
|
||||
})
|
||||
});
|
||||
|
||||
export const AnalyticsLogSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
|
|
@ -917,12 +917,12 @@ export const AnalyticsLogSchema = z.object({
|
|||
]),
|
||||
statusCode: z.number(),
|
||||
type: RequestTypeSchema,
|
||||
})
|
||||
});
|
||||
|
||||
export const AnalyticsLogsResponseSchema = z.object({
|
||||
logs: z.array(z.unknown()),
|
||||
pagination: PaginationSchema,
|
||||
})
|
||||
});
|
||||
|
||||
export const AnalyticsChatResponseSchema = z.object({
|
||||
analytics: z.object({
|
||||
|
|
@ -1022,7 +1022,7 @@ export const AnalyticsChatResponseSchema = z.object({
|
|||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
});
|
||||
|
||||
export const AnalyticsMemoryResponseSchema = z.object({
|
||||
connectionsGrowth: z.number(),
|
||||
|
|
@ -1033,7 +1033,7 @@ export const AnalyticsMemoryResponseSchema = z.object({
|
|||
tokensProcessed: z.number(),
|
||||
totalConnections: z.number(),
|
||||
totalMemories: z.number(),
|
||||
})
|
||||
});
|
||||
|
||||
export const MemoryEntryAPISchema = MemoryEntrySchema.extend({
|
||||
sourceAddedAt: z.date().nullable(), // From join relationship
|
||||
|
|
@ -1042,7 +1042,7 @@ export const MemoryEntryAPISchema = MemoryEntrySchema.extend({
|
|||
spaceContainerTag: z.string().nullable(), // From join relationship
|
||||
}).openapi({
|
||||
description: "Memory entry with source relationship data",
|
||||
})
|
||||
});
|
||||
|
||||
// Extended document schema with memory entries
|
||||
export const DocumentWithMemoriesSchema = z
|
||||
|
|
@ -1075,7 +1075,7 @@ export const DocumentWithMemoriesSchema = z
|
|||
})
|
||||
.openapi({
|
||||
description: "Document with associated memory entries",
|
||||
})
|
||||
});
|
||||
|
||||
export const DocumentsWithMemoriesResponseSchema = z
|
||||
.object({
|
||||
|
|
@ -1084,7 +1084,7 @@ export const DocumentsWithMemoriesResponseSchema = z
|
|||
})
|
||||
.openapi({
|
||||
description: "List of documents with their memory entries",
|
||||
})
|
||||
});
|
||||
|
||||
export const DocumentsWithMemoriesQuerySchema = z
|
||||
.object({
|
||||
|
|
@ -1114,7 +1114,7 @@ export const DocumentsWithMemoriesQuerySchema = z
|
|||
})
|
||||
.openapi({
|
||||
description: "Query parameters for listing documents with memory entries",
|
||||
})
|
||||
});
|
||||
|
||||
export const MigrateMCPRequestSchema = z
|
||||
.object({
|
||||
|
|
@ -1129,7 +1129,7 @@ export const MigrateMCPRequestSchema = z
|
|||
})
|
||||
.openapi({
|
||||
description: "Request body for migrating MCP documents",
|
||||
})
|
||||
});
|
||||
|
||||
export const MigrateMCPResponseSchema = z
|
||||
.object({
|
||||
|
|
@ -1155,7 +1155,7 @@ export const MigrateMCPResponseSchema = z
|
|||
})
|
||||
.openapi({
|
||||
description: "Response for MCP document migration",
|
||||
})
|
||||
});
|
||||
|
||||
// Processing documents schema
|
||||
export const ProcessingDocumentsResponseSchema = z
|
||||
|
|
@ -1196,7 +1196,7 @@ export const ProcessingDocumentsResponseSchema = z
|
|||
],
|
||||
totalCount: 5,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
// Project schemas
|
||||
export const ProjectSchema = z
|
||||
|
|
@ -1235,7 +1235,7 @@ export const ProjectSchema = z
|
|||
})
|
||||
.openapi({
|
||||
description: "Project object for organizing memories",
|
||||
})
|
||||
});
|
||||
|
||||
export const CreateProjectSchema = z
|
||||
.object({
|
||||
|
|
@ -1248,7 +1248,7 @@ export const CreateProjectSchema = z
|
|||
})
|
||||
.openapi({
|
||||
description: "Request body for creating a new project",
|
||||
})
|
||||
});
|
||||
|
||||
export const ListProjectsResponseSchema = z
|
||||
.object({
|
||||
|
|
@ -1258,7 +1258,7 @@ export const ListProjectsResponseSchema = z
|
|||
})
|
||||
.openapi({
|
||||
description: "Response containing list of projects",
|
||||
})
|
||||
});
|
||||
|
||||
export const DeleteProjectSchema = z
|
||||
.object({
|
||||
|
|
@ -1275,9 +1275,9 @@ export const DeleteProjectSchema = z
|
|||
(data) => {
|
||||
// If action is "move", targetProjectId is required
|
||||
if (data.action === "move") {
|
||||
return !!data.targetProjectId
|
||||
return !!data.targetProjectId;
|
||||
}
|
||||
return true
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "targetProjectId is required when action is 'move'",
|
||||
|
|
@ -1286,7 +1286,7 @@ export const DeleteProjectSchema = z
|
|||
)
|
||||
.openapi({
|
||||
description: "Request body for deleting a project",
|
||||
})
|
||||
});
|
||||
|
||||
export const DeleteProjectResponseSchema = z
|
||||
.object({
|
||||
|
|
@ -1309,7 +1309,7 @@ export const DeleteProjectResponseSchema = z
|
|||
})
|
||||
.openapi({
|
||||
description: "Response for project deletion",
|
||||
})
|
||||
});
|
||||
|
||||
// Bulk delete schema - supports both IDs and container tags
|
||||
export const BulkDeleteMemoriesSchema = z
|
||||
|
|
@ -1336,7 +1336,7 @@ export const BulkDeleteMemoriesSchema = z
|
|||
.refine(
|
||||
(data) => {
|
||||
// At least one of ids or containerTags must be provided
|
||||
return !!data.ids?.length || !!data.containerTags?.length
|
||||
return !!data.ids?.length || !!data.containerTags?.length;
|
||||
},
|
||||
{
|
||||
message: "Either 'ids' or 'containerTags' must be provided",
|
||||
|
|
@ -1348,7 +1348,7 @@ export const BulkDeleteMemoriesSchema = z
|
|||
example: {
|
||||
ids: ["acxV5LHMEsG2hMSNb4umbn", "bxcV5LHMEsG2hMSNb4umbn"],
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
export const BulkDeleteMemoriesResponseSchema = z
|
||||
.object({
|
||||
|
|
@ -1383,4 +1383,4 @@ export const BulkDeleteMemoriesResponseSchema = z
|
|||
})
|
||||
.openapi({
|
||||
description: "Response for bulk memory deletion",
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { z } from "zod"
|
||||
import { ConnectionProviderEnum } from "./schemas"
|
||||
import { z } from "zod";
|
||||
import { ConnectionProviderEnum } from "./schemas";
|
||||
|
||||
export const providers = ConnectionProviderEnum
|
||||
export type Provider = z.infer<typeof providers>
|
||||
export const providers = ConnectionProviderEnum;
|
||||
export type Provider = z.infer<typeof providers>;
|
||||
|
||||
const BaseMetadataSchema = <T extends z.ZodTypeAny>(provider: T) =>
|
||||
z.object({
|
||||
provider,
|
||||
})
|
||||
});
|
||||
|
||||
export const NotionMetadataSchema = BaseMetadataSchema(
|
||||
z.literal("notion"),
|
||||
|
|
@ -17,8 +17,8 @@ export const NotionMetadataSchema = BaseMetadataSchema(
|
|||
workspaceIcon: z.string().optional(),
|
||||
workspaceId: z.string(),
|
||||
workspaceName: z.string(),
|
||||
})
|
||||
export type NotionMetadata = z.infer<typeof NotionMetadataSchema>
|
||||
});
|
||||
export type NotionMetadata = z.infer<typeof NotionMetadataSchema>;
|
||||
|
||||
export const GoogleDriveMetadataSchema = BaseMetadataSchema(
|
||||
z.literal("google-drive"),
|
||||
|
|
@ -27,8 +27,8 @@ export const GoogleDriveMetadataSchema = BaseMetadataSchema(
|
|||
webhookChannelId: z.string().optional(),
|
||||
webhookExpiration: z.number().optional(),
|
||||
webhookResourceId: z.string().optional(),
|
||||
})
|
||||
export type GoogleDriveMetadata = z.infer<typeof GoogleDriveMetadataSchema>
|
||||
});
|
||||
export type GoogleDriveMetadata = z.infer<typeof GoogleDriveMetadataSchema>;
|
||||
|
||||
export const OneDriveMetadataSchema = BaseMetadataSchema(
|
||||
z.literal("onedrive"),
|
||||
|
|
@ -38,38 +38,38 @@ export const OneDriveMetadataSchema = BaseMetadataSchema(
|
|||
webhookClientState: z.string().optional(),
|
||||
webhookExpiration: z.number().optional(),
|
||||
webhookSubscriptionId: z.string().optional(),
|
||||
})
|
||||
export type OneDriveMetadata = z.infer<typeof OneDriveMetadataSchema>
|
||||
});
|
||||
export type OneDriveMetadata = z.infer<typeof OneDriveMetadataSchema>;
|
||||
|
||||
export const ConnectionMetadataSchema = z.discriminatedUnion("provider", [
|
||||
NotionMetadataSchema,
|
||||
GoogleDriveMetadataSchema,
|
||||
OneDriveMetadataSchema,
|
||||
])
|
||||
]);
|
||||
export type ConnectionMetadata<T extends Provider> = T extends "notion"
|
||||
? NotionMetadata
|
||||
: T extends "google-drive"
|
||||
? GoogleDriveMetadata
|
||||
: T extends "onedrive"
|
||||
? OneDriveMetadata
|
||||
: never
|
||||
: never;
|
||||
|
||||
export function isNotionMetadata(
|
||||
metadata: unknown,
|
||||
): metadata is NotionMetadata {
|
||||
return NotionMetadataSchema.safeParse(metadata).success
|
||||
return NotionMetadataSchema.safeParse(metadata).success;
|
||||
}
|
||||
|
||||
export function isGoogleDriveMetadata(
|
||||
metadata: unknown,
|
||||
): metadata is GoogleDriveMetadata {
|
||||
return GoogleDriveMetadataSchema.safeParse(metadata).success
|
||||
return GoogleDriveMetadataSchema.safeParse(metadata).success;
|
||||
}
|
||||
|
||||
export function isOneDriveMetadata(
|
||||
metadata: unknown,
|
||||
): metadata is OneDriveMetadata {
|
||||
return OneDriveMetadataSchema.safeParse(metadata).success
|
||||
return OneDriveMetadataSchema.safeParse(metadata).success;
|
||||
}
|
||||
|
||||
export const ConnectionStateSchema = z.object({
|
||||
|
|
@ -77,8 +77,8 @@ export const ConnectionStateSchema = z.object({
|
|||
org: z.string(),
|
||||
provider: providers,
|
||||
userId: z.string(),
|
||||
})
|
||||
export type ConnectionState = z.infer<typeof ConnectionStateSchema>
|
||||
});
|
||||
export type ConnectionState = z.infer<typeof ConnectionStateSchema>;
|
||||
|
||||
export const TokenDataSchema = z.object({
|
||||
// Only used for Notion connections since they don't support refresh tokens
|
||||
|
|
@ -90,8 +90,8 @@ export const TokenDataSchema = z.object({
|
|||
metadata: ConnectionMetadataSchema.optional(),
|
||||
refreshToken: z.string().optional(),
|
||||
userId: z.string().optional(),
|
||||
})
|
||||
export type TokenData = z.infer<typeof TokenDataSchema>
|
||||
});
|
||||
export type TokenData = z.infer<typeof TokenDataSchema>;
|
||||
|
||||
export const NotionTokenResponseSchema = z.object({
|
||||
access_token: z.string(),
|
||||
|
|
@ -123,8 +123,8 @@ export const NotionTokenResponseSchema = z.object({
|
|||
workspace_icon: z.string().optional(),
|
||||
workspace_id: z.string(),
|
||||
workspace_name: z.string(),
|
||||
})
|
||||
export type NotionTokenResponse = z.infer<typeof NotionTokenResponseSchema>
|
||||
});
|
||||
export type NotionTokenResponse = z.infer<typeof NotionTokenResponseSchema>;
|
||||
|
||||
export const GoogleDriveTokenResponseSchema = z.object({
|
||||
access_token: z.string(),
|
||||
|
|
@ -132,10 +132,10 @@ export const GoogleDriveTokenResponseSchema = z.object({
|
|||
refresh_token: z.string().optional(),
|
||||
scope: z.string(),
|
||||
token_type: z.literal("Bearer"),
|
||||
})
|
||||
});
|
||||
export type GoogleDriveTokenResponse = z.infer<
|
||||
typeof GoogleDriveTokenResponseSchema
|
||||
>
|
||||
>;
|
||||
|
||||
export const OneDriveTokenResponseSchema = z.object({
|
||||
access_token: z.string(),
|
||||
|
|
@ -143,8 +143,8 @@ export const OneDriveTokenResponseSchema = z.object({
|
|||
refresh_token: z.string().optional(),
|
||||
scope: z.string(),
|
||||
token_type: z.literal("Bearer"),
|
||||
})
|
||||
export type OneDriveTokenResponse = z.infer<typeof OneDriveTokenResponseSchema>
|
||||
});
|
||||
export type OneDriveTokenResponse = z.infer<typeof OneDriveTokenResponseSchema>;
|
||||
|
||||
export const NotionConfigSchema = z.object({
|
||||
clientId: z.string(),
|
||||
|
|
@ -154,23 +154,23 @@ export const NotionConfigSchema = z.object({
|
|||
token: z.string().url(),
|
||||
}),
|
||||
scopes: z.array(z.string()),
|
||||
})
|
||||
export type NotionConfig = z.infer<typeof NotionConfigSchema>
|
||||
});
|
||||
export type NotionConfig = z.infer<typeof NotionConfigSchema>;
|
||||
|
||||
export const ConnectionQuerySchema = z.object({
|
||||
id: z.string(),
|
||||
redirectUrl: z.string().optional(),
|
||||
})
|
||||
});
|
||||
|
||||
export const GoogleDrivePageTokenResponseSchema = z.object({
|
||||
startPageToken: z.union([z.string(), z.number()]),
|
||||
})
|
||||
});
|
||||
|
||||
export const GoogleDriveWatchResponseSchema = z.object({
|
||||
expiration: z.string(),
|
||||
id: z.string(),
|
||||
resourceId: z.string(),
|
||||
})
|
||||
});
|
||||
|
||||
export const OneDriveSubscriptionResponseSchema = z.object({
|
||||
changeType: z.string(),
|
||||
|
|
@ -179,13 +179,13 @@ export const OneDriveSubscriptionResponseSchema = z.object({
|
|||
id: z.string(),
|
||||
notificationUrl: z.string(),
|
||||
resource: z.string(),
|
||||
})
|
||||
});
|
||||
|
||||
export const GoogleUserInfoResponseSchema = z.object({
|
||||
email: z.string().email(),
|
||||
})
|
||||
});
|
||||
|
||||
export const MicrosoftUserInfoResponseSchema = z.object({
|
||||
mail: z.string().optional(),
|
||||
userPrincipalName: z.string().optional(),
|
||||
})
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue