mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-07 02:58:11 +00:00
layout first
This commit is contained in:
parent
85d4786f61
commit
6e93cac9b5
17 changed files with 319 additions and 852 deletions
|
|
@ -15,6 +15,7 @@ export default defineConfig({
|
|||
permissions: [
|
||||
"contextMenus",
|
||||
"storage",
|
||||
"scripting",
|
||||
"activeTab",
|
||||
"webRequest",
|
||||
"tabs",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { ErrorTrackingProvider } from "@lib/error-tracking";
|
|||
import { PostHogProvider } from "@lib/posthog";
|
||||
import { QueryProvider } from "@lib/query-client";
|
||||
import { AutumnProvider } from "autumn-js/react";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { Suspense } from "react";
|
||||
import { Toaster } from "sonner";
|
||||
import { TourProvider } from "@/components/tour";
|
||||
|
|
@ -36,33 +37,35 @@ export default function RootLayout({
|
|||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html className="dark bg-sm-black" lang="en">
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body
|
||||
className={`${sans.variable} ${mono.variable} antialiased bg-[#0f1419]`}
|
||||
className={`${sans.variable} ${mono.variable} antialiased`}
|
||||
>
|
||||
<AutumnProvider
|
||||
backendUrl={
|
||||
process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
|
||||
}
|
||||
includeCredentials={true}
|
||||
>
|
||||
<QueryProvider>
|
||||
<AuthProvider>
|
||||
<ViewModeProvider>
|
||||
<MobilePanelProvider>
|
||||
<PostHogProvider>
|
||||
<ErrorTrackingProvider>
|
||||
<TourProvider>
|
||||
<Suspense>{children}</Suspense>
|
||||
<Toaster richColors theme="dark" />
|
||||
</TourProvider>
|
||||
</ErrorTrackingProvider>
|
||||
</PostHogProvider>
|
||||
</MobilePanelProvider>
|
||||
</ViewModeProvider>
|
||||
</AuthProvider>
|
||||
</QueryProvider>
|
||||
</AutumnProvider>
|
||||
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
|
||||
<AutumnProvider
|
||||
backendUrl={
|
||||
process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
|
||||
}
|
||||
includeCredentials={true}
|
||||
>
|
||||
<QueryProvider>
|
||||
<AuthProvider>
|
||||
<ViewModeProvider>
|
||||
<MobilePanelProvider>
|
||||
<PostHogProvider>
|
||||
<ErrorTrackingProvider>
|
||||
<TourProvider>
|
||||
<Suspense>{children}</Suspense>
|
||||
<Toaster richColors />
|
||||
</TourProvider>
|
||||
</ErrorTrackingProvider>
|
||||
</PostHogProvider>
|
||||
</MobilePanelProvider>
|
||||
</ViewModeProvider>
|
||||
</AuthProvider>
|
||||
</QueryProvider>
|
||||
</AutumnProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,57 +1,25 @@
|
|||
"use client";
|
||||
|
||||
import { useIsMobile } from "@hooks/use-mobile";
|
||||
import { useAuth } from "@lib/auth-context";
|
||||
import { $fetch } from "@repo/lib/api";
|
||||
import { MemoryGraph } from "@repo/ui/memory-graph";
|
||||
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api";
|
||||
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
||||
import { Logo, LogoFull } from "@ui/assets/Logo";
|
||||
import { Button } from "@ui/components/button";
|
||||
import { GlassMenuEffect } from "@ui/other/glass-effect";
|
||||
import {
|
||||
HelpCircle,
|
||||
LayoutGrid,
|
||||
List,
|
||||
LoaderIcon,
|
||||
MessageSquare,
|
||||
Unplug,
|
||||
} from "lucide-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { LoaderIcon } from "lucide-react";
|
||||
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 { AppHeader } from "@/components/header";
|
||||
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 { InstallPrompt } from "@/components/install-prompt";
|
||||
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";
|
||||
import { useProject } from "@/stores";
|
||||
|
||||
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 MemoryAppPage = () => {
|
||||
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 [showAddMemoryModal, setShowAddMemoryModal] = useState(false);
|
||||
|
||||
// Fetch projects meta to detect experimental flag
|
||||
const { data: projectsMeta = [] } = useQuery({
|
||||
|
|
@ -64,170 +32,9 @@ const MemoryGraphPage = () => {
|
|||
});
|
||||
|
||||
const isCurrentProjectExperimental = !!projectsMeta.find(
|
||||
(p: any) => p.containerTag === selectedProject,
|
||||
(p: { containerTag: string; isExperimental?: boolean }) => p.containerTag === selectedProject,
|
||||
)?.isExperimental;
|
||||
|
||||
// Tour state
|
||||
const [showTourDialog, setShowTourDialog] = useState(false);
|
||||
|
||||
// Define tour steps with useMemo to prevent recreation
|
||||
const tourSteps: TourStep[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
content: (
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2 text-white">
|
||||
Memories Overview
|
||||
</h3>
|
||||
<p className="text-gray-200">
|
||||
This is your memory graph. Each node represents a memory, and
|
||||
connections show relationships between them.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
selectorId: TOUR_STEP_IDS.MEMORY_GRAPH,
|
||||
position: "center",
|
||||
},
|
||||
{
|
||||
content: (
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2 text-white">
|
||||
Add Memories
|
||||
</h3>
|
||||
<p className="text-gray-200">
|
||||
Click here to add new memories to your knowledge base. You can add
|
||||
text, links, or connect external sources.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
selectorId: TOUR_STEP_IDS.MENU_ADD_MEMORY,
|
||||
position: "right",
|
||||
},
|
||||
{
|
||||
content: (
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2 text-white">
|
||||
Connections
|
||||
</h3>
|
||||
<p className="text-gray-200">
|
||||
Connect your external accounts like Google Drive, Notion, or
|
||||
OneDrive to automatically sync and organize your content.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
selectorId: TOUR_STEP_IDS.MENU_CONNECTIONS,
|
||||
position: "right",
|
||||
},
|
||||
{
|
||||
content: (
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2 text-white">Projects</h3>
|
||||
<p className="text-gray-200">
|
||||
Organize your memories into projects. Switch between different
|
||||
contexts easily.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
selectorId: TOUR_STEP_IDS.MENU_PROJECTS,
|
||||
position: "right",
|
||||
},
|
||||
{
|
||||
content: (
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2 text-white">
|
||||
MCP Servers
|
||||
</h3>
|
||||
<p className="text-gray-200">
|
||||
Access Model Context Protocol servers to give AI tools access to
|
||||
your memories securely.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
selectorId: TOUR_STEP_IDS.MENU_MCP,
|
||||
position: "right",
|
||||
},
|
||||
{
|
||||
content: (
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2 text-white">Billing</h3>
|
||||
<p className="text-gray-200">
|
||||
Manage your subscription and billing information.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
selectorId: TOUR_STEP_IDS.MENU_BILLING,
|
||||
position: "right",
|
||||
},
|
||||
{
|
||||
content: (
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2 text-white">
|
||||
View Toggle
|
||||
</h3>
|
||||
<p className="text-gray-200">
|
||||
Switch between graph view and list view to see your memories in
|
||||
different ways.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
selectorId: TOUR_STEP_IDS.VIEW_TOGGLE,
|
||||
position: "left",
|
||||
},
|
||||
{
|
||||
content: (
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2 text-white">Legend</h3>
|
||||
<p className="text-gray-200">
|
||||
Understand the different types of nodes and connections in your
|
||||
memory graph.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
selectorId: TOUR_STEP_IDS.LEGEND,
|
||||
position: "left",
|
||||
},
|
||||
{
|
||||
content: (
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg mb-2 text-white">
|
||||
Chat Assistant
|
||||
</h3>
|
||||
<p className="text-gray-200">
|
||||
Ask questions or add new memories using our AI-powered chat
|
||||
interface.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
selectorId: TOUR_STEP_IDS.FLOATING_CHAT,
|
||||
position: "left",
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
|
||||
// Check if tour has been completed before
|
||||
useEffect(() => {
|
||||
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);
|
||||
}
|
||||
}, [isTourCompleted]);
|
||||
|
||||
// Set up tour steps
|
||||
useEffect(() => {
|
||||
setSteps(tourSteps);
|
||||
}, [setSteps, tourSteps]);
|
||||
|
||||
// Save tour completion to localStorage
|
||||
useEffect(() => {
|
||||
if (isTourCompleted) {
|
||||
localStorage.setItem(TOUR_STORAGE_KEY, "true");
|
||||
}
|
||||
}, [isTourCompleted]);
|
||||
|
||||
// Progressive loading via useInfiniteQuery
|
||||
const IS_DEV = process.env.NODE_ENV === "development";
|
||||
const PAGE_SIZE = IS_DEV ? 100 : 100;
|
||||
|
|
@ -308,88 +115,23 @@ const MemoryGraphPage = () => {
|
|||
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>();
|
||||
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);
|
||||
}
|
||||
const missing = allHighlightDocumentIds.filter(
|
||||
(id: string) => !present.has(id),
|
||||
);
|
||||
if (missing.length === 0) return;
|
||||
let cancelled = false;
|
||||
const run = async () => {
|
||||
try {
|
||||
const resp = await $fetch("@post/memories/documents/by-ids", {
|
||||
body: {
|
||||
ids: missing,
|
||||
by: "customId",
|
||||
containerTags: selectedProject ? [selectedProject] : undefined,
|
||||
},
|
||||
disableValidation: true,
|
||||
});
|
||||
if (cancelled || (resp as any)?.error) return;
|
||||
const extraDocs = (resp as any)?.data?.documents as
|
||||
| DocumentWithMemories[]
|
||||
| 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];
|
||||
for (const doc of extraDocs) {
|
||||
if (!seen.has(doc.id)) {
|
||||
merged.push(doc);
|
||||
seen.add(doc.id);
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
});
|
||||
} catch {}
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'c' && !['INPUT', 'TEXTAREA'].includes((event.target as Element)?.tagName)) {
|
||||
event.preventDefault();
|
||||
setShowAddMemoryModal(true);
|
||||
}
|
||||
};
|
||||
void run();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [
|
||||
isOpen,
|
||||
allHighlightDocumentIds.join("|"),
|
||||
baseDocuments,
|
||||
injectedDocs,
|
||||
selectedProject,
|
||||
$fetch,
|
||||
]);
|
||||
|
||||
// Handle view mode change
|
||||
const handleViewModeChange = useCallback(
|
||||
(mode: "graph" | "list") => {
|
||||
setViewMode(mode);
|
||||
},
|
||||
[setViewMode],
|
||||
);
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === "true";
|
||||
if (hasCompletedTour && allDocuments.length === 0 && !showTourDialog) {
|
||||
setShowConnectAIModal(true);
|
||||
} else if (showTourDialog) {
|
||||
setShowConnectAIModal(false);
|
||||
}
|
||||
}, [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 = "auto";
|
||||
document.body.style.height = "auto";
|
||||
document.documentElement.style.overflow = "auto";
|
||||
document.documentElement.style.height = "auto";
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = "";
|
||||
|
|
@ -400,380 +142,49 @@ const MemoryGraphPage = () => {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative h-screen bg-[#0f1419] overflow-hidden touch-none">
|
||||
{/* Main content area */}
|
||||
<motion.div
|
||||
animate={{
|
||||
marginRight: isOpen && !isMobile ? 600 : 0,
|
||||
}}
|
||||
className="h-full relative"
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
ease: [0.4, 0, 0.2, 1], // Material Design easing - snappy but smooth
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="absolute md:top-4 md:right-4 md:bottom-auto md:left-auto bottom-8 left-6 z-20 rounded-xl overflow-hidden"
|
||||
id={TOUR_STEP_IDS.VIEW_TOGGLE}
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
transition={{ type: "spring", stiffness: 300, damping: 25 }}
|
||||
>
|
||||
<GlassMenuEffect rounded="rounded-xl" />
|
||||
<div className="relative z-10 p-2 flex gap-1">
|
||||
<motion.button
|
||||
animate={{
|
||||
color: viewMode === "graph" ? "#93c5fd" : "#cbd5e1",
|
||||
}}
|
||||
className="relative h-8 px-3 flex items-center gap-2 text-sm font-medium rounded-md transition-colors"
|
||||
onClick={() => handleViewModeChange("graph")}
|
||||
transition={{ duration: 0.2 }}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
{viewMode === "graph" && (
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-blue-500/20 rounded-md"
|
||||
layoutId="activeBackground"
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 400,
|
||||
damping: 30,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className="relative z-10 flex items-center gap-2">
|
||||
<LayoutGrid className="w-4 h-4" />
|
||||
<span className="hidden md:inline">Graph</span>
|
||||
</span>
|
||||
</motion.button>
|
||||
<div>
|
||||
<AppHeader />
|
||||
|
||||
<MemoryListView
|
||||
documents={allDocuments}
|
||||
error={error}
|
||||
hasMore={hasMore}
|
||||
isLoading={isPending}
|
||||
isLoadingMore={isLoadingMore}
|
||||
totalLoaded={totalLoaded}
|
||||
loadMoreDocuments={loadMoreDocuments}
|
||||
isCurrentProjectExperimental={isCurrentProjectExperimental}
|
||||
/>
|
||||
|
||||
<motion.button
|
||||
animate={{
|
||||
color: viewMode === "list" ? "#93c5fd" : "#cbd5e1",
|
||||
}}
|
||||
className="relative h-8 px-3 flex items-center gap-2 text-sm font-medium rounded-md transition-colors"
|
||||
onClick={() => handleViewModeChange("list")}
|
||||
transition={{ duration: 0.2 }}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
{viewMode === "list" && (
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-blue-500/20 rounded-md"
|
||||
layoutId="activeBackground"
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 400,
|
||||
damping: 30,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className="relative z-10 flex items-center gap-2">
|
||||
<List className="w-4 h-4" />
|
||||
<span className="hidden md:inline">List</span>
|
||||
</span>
|
||||
</motion.button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Animated content switching */}
|
||||
<AnimatePresence mode="wait">
|
||||
{viewMode === "graph" ? (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="absolute inset-0"
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
id={TOUR_STEP_IDS.MEMORY_GRAPH}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
key="graph"
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 500,
|
||||
damping: 30,
|
||||
}}
|
||||
>
|
||||
<MemoryGraph
|
||||
autoLoadOnViewport={false}
|
||||
documents={allDocuments}
|
||||
error={error}
|
||||
hasMore={hasMore}
|
||||
highlightDocumentIds={allHighlightDocumentIds}
|
||||
highlightsVisible={isOpen}
|
||||
isExperimental={isCurrentProjectExperimental}
|
||||
isLoading={isPending}
|
||||
isLoadingMore={isLoadingMore}
|
||||
legendId={TOUR_STEP_IDS.LEGEND}
|
||||
loadMoreDocuments={loadMoreDocuments}
|
||||
occludedRightPx={isOpen && !isMobile ? 600 : 0}
|
||||
showSpacesSelector={false}
|
||||
totalLoaded={totalLoaded}
|
||||
variant="consumer"
|
||||
>
|
||||
<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>
|
||||
</MemoryGraph>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="absolute inset-0 md:ml-18"
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
id={TOUR_STEP_IDS.MEMORY_LIST}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
key="list"
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 500,
|
||||
damping: 30,
|
||||
}}
|
||||
>
|
||||
<MemoryListView
|
||||
documents={allDocuments}
|
||||
error={error}
|
||||
hasMore={hasMore}
|
||||
isLoading={isPending}
|
||||
isLoadingMore={isLoadingMore}
|
||||
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>
|
||||
</MemoryListView>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Top Bar */}
|
||||
<div className="absolute top-2 left-0 right-0 z-10 p-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3 justify-between w-full md:w-fit md:justify-start">
|
||||
<Link
|
||||
className="pointer-events-auto"
|
||||
href="https://supermemory.ai"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<LogoFull
|
||||
className="h-8 hidden md:block"
|
||||
id={TOUR_STEP_IDS.LOGO}
|
||||
/>
|
||||
<Logo className="h-8 md:hidden" id={TOUR_STEP_IDS.LOGO} />
|
||||
</Link>
|
||||
|
||||
<div className="hidden sm:block">
|
||||
<ProjectSelector />
|
||||
</div>
|
||||
|
||||
<ConnectAIModal>
|
||||
<Button
|
||||
className="bg-white/5 hover:bg-white/10 border-white/20 text-white hover:text-white px-2 sm:px-3"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
<Unplug className="h-4 w-4" />
|
||||
<span className="hidden sm:inline ml-2">
|
||||
Connect to your AI
|
||||
</span>
|
||||
<span className="sm:hidden ml-1">Connect AI</span>
|
||||
</Button>
|
||||
</ConnectAIModal>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Menu />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating Open Chat Button */}
|
||||
{!isOpen && !isMobile && (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="fixed bottom-6 right-6 z-50"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 25,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
className="px-4 bg-white hover:bg-white/80 text-[#001A39] shadow-lg hover:shadow-xl transition-all duration-200 rounded-full flex items-center gap-2 cursor-pointer"
|
||||
onClick={() => setIsOpen(true)}
|
||||
size="lg"
|
||||
>
|
||||
<MessageSquare className="h-5 w-5" />
|
||||
<span className="font-medium">Open Chat</span>
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="fixed bottom-6 left-6 z-50 flex items-center overflow-hidden rounded-full shadow-2xl bg-transparent border-none cursor-pointer"
|
||||
onMouseEnter={() => setIsHelpHovered(true)}
|
||||
onMouseLeave={() => setIsHelpHovered(false)}
|
||||
type="button"
|
||||
>
|
||||
<div className="absolute inset-0 rounded-full">
|
||||
<GlassMenuEffect rounded="rounded-full" />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 p-3 text-white">
|
||||
<HelpCircle className="h-5 w-5" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`relative z-10 flex items-center text-white transition-all duration-300 overflow-hidden ${
|
||||
isHelpHovered
|
||||
? "opacity-100 max-w-32 pr-4 pl-0 py-3"
|
||||
: "opacity-0 max-w-0 px-0 py-3"
|
||||
}`}
|
||||
>
|
||||
<a
|
||||
className="flex items-center gap-2 text-sm font-medium hover:text-white/80 transition-colors whitespace-nowrap"
|
||||
href="mailto:dhravya@supermemory.com"
|
||||
>
|
||||
<span>Need Help?</span>
|
||||
</a>
|
||||
</div>
|
||||
</button>
|
||||
</motion.div>
|
||||
|
||||
{/* Chat panel - positioned absolutely */}
|
||||
<motion.div
|
||||
className="fixed top-0 right-0 h-full z-50 md:z-auto"
|
||||
id={TOUR_STEP_IDS.FLOATING_CHAT}
|
||||
style={{
|
||||
width: isOpen ? (isMobile ? "100vw" : "600px") : 0,
|
||||
pointerEvents: isOpen ? "auto" : "none",
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
animate={{ x: isOpen ? 0 : isMobile ? "100%" : 600 }}
|
||||
className="absolute inset-0"
|
||||
exit={{ x: isMobile ? "100%" : 600 }}
|
||||
initial={{ x: isMobile ? "100%" : 600 }}
|
||||
key="chat"
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 500,
|
||||
damping: 40,
|
||||
}}
|
||||
>
|
||||
<ChatRewrite />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{showAddMemoryView && (
|
||||
{showAddMemoryModal && (
|
||||
<AddMemoryView
|
||||
initialTab="note"
|
||||
onClose={() => setShowAddMemoryView(false)}
|
||||
onClose={() => setShowAddMemoryModal(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Tour Alert Dialog */}
|
||||
<TourAlertDialog onOpenChange={setShowTourDialog} open={showTourDialog} />
|
||||
|
||||
{/* Referral/Upgrade Modal */}
|
||||
<ReferralUpgradeModal
|
||||
isOpen={showReferralModal}
|
||||
onClose={() => setShowReferralModal(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Wrapper component to handle auth and waitlist checks
|
||||
// Wrapper component to handle auth
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
// save the token for chrome extension
|
||||
const url = new URL(window.location.href);
|
||||
const rawToken = url.searchParams.get("token");
|
||||
|
||||
if (rawToken) {
|
||||
const encodedToken = encodeURIComponent(rawToken);
|
||||
window.postMessage({ token: encodedToken }, "*");
|
||||
url.searchParams.delete("token");
|
||||
window.history.replaceState({}, "", url.toString());
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Show loading state while checking authentication and waitlist status
|
||||
// Show loading state while checking authentication
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-[#0f1419]">
|
||||
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<LoaderIcon className="w-8 h-8 text-orange-500 animate-spin" />
|
||||
<p className="text-white/60">Loading...</p>
|
||||
<p className="text-muted-foreground">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If we have a user and they have access, show the main component
|
||||
return (
|
||||
<>
|
||||
<MemoryGraphPage />
|
||||
<MemoryAppPage />
|
||||
<InstallPrompt />
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ 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 { colors } from "@repo/ui/memory-graph/constants";
|
||||
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import {
|
||||
|
|
@ -20,6 +19,8 @@ import { analytics } from "@/lib/analytics";
|
|||
import { MemoryDetail } from "./memories/memory-detail";
|
||||
import { getDocumentIcon } from "@/lib/document-icon";
|
||||
import { formatDate, getSourceUrl } from "./memories";
|
||||
import { ChatInline } from "./chat-inline";
|
||||
import { GraphPreview } from "./graph-preview";
|
||||
|
||||
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>;
|
||||
type DocumentWithMemories = DocumentsResponse["documents"][0];
|
||||
|
|
@ -33,33 +34,9 @@ interface MemoryListViewProps {
|
|||
totalLoaded: number;
|
||||
hasMore: boolean;
|
||||
loadMoreDocuments: () => Promise<void>;
|
||||
isCurrentProjectExperimental?: boolean;
|
||||
}
|
||||
|
||||
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";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 mb-3 px-4 md:mb-6 md:mt-3">
|
||||
<div>
|
||||
<h1
|
||||
className="text-lg md:text-xl font-semibold"
|
||||
style={{ color: colors.text.primary }}
|
||||
>
|
||||
{getGreeting()}!
|
||||
</h1>
|
||||
<p className="text-xs md:text-sm" style={{ color: colors.text.muted }}>
|
||||
Welcome back to your memory collection
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const DocumentCard = memo(
|
||||
({
|
||||
document,
|
||||
|
|
@ -75,14 +52,11 @@ const DocumentCard = memo(
|
|||
|
||||
return (
|
||||
<Card
|
||||
className="h-full mx-4 p-4 transition-all cursor-pointer group relative overflow-hidden border-0 gap-2 md:w-full"
|
||||
className="h-full mx-4 p-4 transition-all cursor-pointer group relative overflow-hidden bg-card gap-2 md:w-full"
|
||||
onClick={() => {
|
||||
analytics.documentCardClicked();
|
||||
onOpenDetails(document);
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: colors.document.primary,
|
||||
}}
|
||||
>
|
||||
<CardHeader className="relative z-10 px-0">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
|
|
@ -99,16 +73,12 @@ const DocumentCard = memo(
|
|||
</div>
|
||||
{document.url && (
|
||||
<button
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded"
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded bg-muted/50 text-muted-foreground hover:bg-muted/80"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const sourceUrl = getSourceUrl(document);
|
||||
window.open(sourceUrl ?? undefined, "_blank");
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: "rgba(255, 255, 255, 0.05)",
|
||||
color: colors.text.secondary,
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
|
|
@ -122,8 +92,7 @@ const DocumentCard = memo(
|
|||
<CardContent className="relative z-10 px-0">
|
||||
{document.content && (
|
||||
<p
|
||||
className="text-xs line-clamp-2 mb-3"
|
||||
style={{ color: colors.text.muted }}
|
||||
className="text-xs line-clamp-2 mb-3 text-muted-foreground"
|
||||
>
|
||||
{document.content}
|
||||
</p>
|
||||
|
|
@ -131,10 +100,7 @@ const DocumentCard = memo(
|
|||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{activeMemories.length > 0 && (
|
||||
<Badge
|
||||
className="text-xs text-accent-foreground"
|
||||
style={{
|
||||
backgroundColor: colors.memory.secondary,
|
||||
}}
|
||||
className="text-xs"
|
||||
variant="secondary"
|
||||
>
|
||||
<Brain className="w-3 h-3 mr-1" />
|
||||
|
|
@ -144,11 +110,7 @@ const DocumentCard = memo(
|
|||
)}
|
||||
{forgottenMemories.length > 0 && (
|
||||
<Badge
|
||||
className="text-xs"
|
||||
style={{
|
||||
borderColor: "rgba(255, 255, 255, 0.2)",
|
||||
color: colors.text.muted,
|
||||
}}
|
||||
className="text-xs text-muted-foreground border-muted"
|
||||
variant="outline"
|
||||
>
|
||||
{forgottenMemories.length} forgotten
|
||||
|
|
@ -169,13 +131,14 @@ export const MemoryListView = ({
|
|||
error,
|
||||
hasMore,
|
||||
loadMoreDocuments,
|
||||
isCurrentProjectExperimental = false,
|
||||
}: MemoryListViewProps) => {
|
||||
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 parentRef = useRef<HTMLDivElement>(null);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const gap = 14;
|
||||
|
|
@ -232,6 +195,7 @@ export const MemoryListView = ({
|
|||
estimateSize: () => 200,
|
||||
});
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const [lastItem] = [...virtualizer.getVirtualItems()].reverse();
|
||||
|
||||
|
|
@ -254,27 +218,24 @@ export const MemoryListView = ({
|
|||
return (
|
||||
<>
|
||||
<div
|
||||
className="h-full overflow-hidden relative pb-20"
|
||||
style={{ backgroundColor: colors.background.primary }}
|
||||
className="min-h-screen flex flex-col"
|
||||
ref={containerRef}
|
||||
>
|
||||
{error ? (
|
||||
<div className="h-full flex items-center justify-center p-4">
|
||||
<div className="flex items-center justify-center p-4 min-h-screen">
|
||||
<div className="rounded-xl overflow-hidden">
|
||||
<div
|
||||
className="relative z-10 px-6 py-4"
|
||||
style={{ color: colors.text.primary }}
|
||||
className="relative z-10 px-6 py-4 text-foreground"
|
||||
>
|
||||
Error loading documents: {error.message}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : isLoading ? (
|
||||
<div className="h-full flex items-center justify-center p-4">
|
||||
<div className="flex items-center justify-center p-4 min-h-screen">
|
||||
<div className="rounded-xl overflow-hidden">
|
||||
<div
|
||||
className="relative z-10 px-6 py-4"
|
||||
style={{ color: colors.text.primary }}
|
||||
className="relative z-10 px-6 py-4 text-foreground"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4 animate-spin text-blue-400" />
|
||||
|
|
@ -284,67 +245,157 @@ export const MemoryListView = ({
|
|||
</div>
|
||||
</div>
|
||||
) : filteredDocuments.length === 0 && !isLoading ? (
|
||||
<div className="h-full flex items-center justify-center p-4">
|
||||
{children}
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<div className="p-6 pb-8">
|
||||
{isMobile ? (
|
||||
<>
|
||||
<div className="mb-6">
|
||||
<ChatInline />
|
||||
</div>
|
||||
<div className="mb-6 hidden sm:block">
|
||||
<GraphPreview
|
||||
documents={documents}
|
||||
error={error}
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
isLoadingMore={isLoadingMore}
|
||||
totalLoaded={documents.length}
|
||||
loadMoreDocuments={loadMoreDocuments}
|
||||
isCurrentProjectExperimental={
|
||||
isCurrentProjectExperimental
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="mb-6 flex gap-6">
|
||||
<div className="w-2/3">
|
||||
<ChatInline />
|
||||
</div>
|
||||
<div className="w-1/3">
|
||||
<GraphPreview
|
||||
documents={documents}
|
||||
error={error}
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
isLoadingMore={isLoadingMore}
|
||||
totalLoaded={documents.length}
|
||||
loadMoreDocuments={loadMoreDocuments}
|
||||
isCurrentProjectExperimental={
|
||||
isCurrentProjectExperimental
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center p-4">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
ref={parentRef}
|
||||
className="h-full overflow-auto mt-20 custom-scrollbar"
|
||||
>
|
||||
<GreetingMessage />
|
||||
|
||||
<div
|
||||
className="w-full relative"
|
||||
style={{
|
||||
height: `${virtualizer.getTotalSize() + virtualItems.length * gap}px`,
|
||||
}}
|
||||
>
|
||||
{virtualizer.getVirtualItems().map((virtualRow) => {
|
||||
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"
|
||||
style={{
|
||||
transform: `translateY(${virtualRow.start + virtualRow.index * gap}px)`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="grid justify-start"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${columns}, ${columnWidth}px)`,
|
||||
gap: `${gap}px`,
|
||||
}}
|
||||
>
|
||||
{rowItems.map((document, columnIndex) => (
|
||||
<DocumentCard
|
||||
key={`${document.id}-${virtualRow.index}-${columnIndex}`}
|
||||
document={document}
|
||||
onOpenDetails={handleOpenDetails}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<>
|
||||
<div className="p-6 pb-8 min-h-[48vh]">
|
||||
{isMobile ? (
|
||||
<>
|
||||
<div className="mb-6">
|
||||
<ChatInline />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="mb-6 hidden sm:block">
|
||||
<GraphPreview
|
||||
documents={documents}
|
||||
error={error}
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
isLoadingMore={isLoadingMore}
|
||||
totalLoaded={documents.length}
|
||||
loadMoreDocuments={loadMoreDocuments}
|
||||
isCurrentProjectExperimental={
|
||||
isCurrentProjectExperimental
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex gap-6 h-[48vh]">
|
||||
<div className="w-2/3">
|
||||
<ChatInline />
|
||||
</div>
|
||||
<div className="w-1/3">
|
||||
<GraphPreview
|
||||
documents={documents}
|
||||
error={error}
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
isLoadingMore={isLoadingMore}
|
||||
totalLoaded={documents.length}
|
||||
loadMoreDocuments={loadMoreDocuments}
|
||||
isCurrentProjectExperimental={
|
||||
isCurrentProjectExperimental
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLoadingMore && (
|
||||
<div className="py-8 flex items-center justify-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4 animate-spin text-blue-400" />
|
||||
<span style={{ color: colors.text.primary }}>
|
||||
Loading more memories...
|
||||
</span>
|
||||
</div>
|
||||
<div className="px-10 py-2">
|
||||
<p className="font-medium text-lg">All Memories</p>
|
||||
</div>
|
||||
|
||||
<div ref={parentRef} className="flex-1 overflow-auto pt-2">
|
||||
<div
|
||||
className="w-full relative"
|
||||
style={{
|
||||
height: `${virtualizer.getTotalSize() + virtualItems.length * gap}px`,
|
||||
}}
|
||||
>
|
||||
{virtualizer.getVirtualItems().map((virtualRow) => {
|
||||
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"
|
||||
style={{
|
||||
transform: `translateY(${virtualRow.start + virtualRow.index * gap}px)`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="grid justify-start px-4"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${columns}, ${columnWidth}px)`,
|
||||
gap: `${gap}px`,
|
||||
}}
|
||||
>
|
||||
{rowItems.map((document, columnIndex) => (
|
||||
<DocumentCard
|
||||
key={`${document.id}-${virtualRow.index}-${columnIndex}`}
|
||||
document={document}
|
||||
onOpenDetails={handleOpenDetails}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLoadingMore && (
|
||||
<div className="py-8 flex items-center justify-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4 animate-spin text-blue-400" />
|
||||
<span className="text-foreground">
|
||||
Loading more memories...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
ChevronRight,
|
||||
Copy,
|
||||
RotateCcw,
|
||||
UserIcon,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
|
@ -327,22 +328,24 @@ export function ChatMessages() {
|
|||
} = useStickyAutoScroll([messages, status]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative grow">
|
||||
<div className="h-full mx-auto flex flex-col max-w-3xl">
|
||||
<div className="flex-1 relative">
|
||||
<div
|
||||
className="flex flex-col gap-2 absolute inset-0 overflow-y-auto px-4 pt-4 pb-7 scroll-pb-7"
|
||||
onScroll={onScroll}
|
||||
ref={scrollContainerRef}
|
||||
>
|
||||
{messages.map((message) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col",
|
||||
message.role === "user" ? "items-end" : "items-start",
|
||||
<div className={cn("flex", message.role === "user" ? "items-center gap-2" : "flex-col")} key={message.id}>
|
||||
{message.role === "user" && (
|
||||
<UserIcon className="size-4 text-muted-foreground" />
|
||||
)}
|
||||
key={message.id}
|
||||
>
|
||||
<div className="flex flex-col gap-2 max-w-4/5 bg-white/10 py-3 px-4 rounded-lg">
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-2 max-w-4/5 rounded-full",
|
||||
message.role === "user" ? "bg-white/10 px-3 py-1.5" : "",
|
||||
)}
|
||||
>
|
||||
{message.parts
|
||||
.filter((part) =>
|
||||
["text", "tool-searchMemories", "tool-addMemory"].includes(
|
||||
|
|
@ -515,7 +518,7 @@ export function ChatMessages() {
|
|||
</div>
|
||||
|
||||
<form
|
||||
className="flex gap-2 px-4 pb-4 pt-1 relative"
|
||||
className="flex gap-2 px-4 pb-4 pt-1 relative flex-shrink-0"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (status === "submitted") return;
|
||||
|
|
@ -531,7 +534,6 @@ export function ChatMessages() {
|
|||
}
|
||||
}}
|
||||
>
|
||||
<div className="absolute top-0 left-0 -mt-7 w-full h-7 bg-gradient-to-t from-background to-transparent" />
|
||||
<Input
|
||||
className="w-full"
|
||||
disabled={status === "submitted"}
|
||||
|
|
@ -549,6 +551,6 @@ export function ChatMessages() {
|
|||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { DEFAULT_PROJECT_ID } from "@repo/lib/constants";
|
||||
|
||||
interface ProjectState {
|
||||
selectedProject: string;
|
||||
|
|
@ -9,7 +10,7 @@ interface ProjectState {
|
|||
export const useProjectStore = create<ProjectState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
selectedProject: "sm_project_default",
|
||||
selectedProject: DEFAULT_PROJECT_ID,
|
||||
setSelectedProject: (projectId) => set({ selectedProject: projectId }),
|
||||
}),
|
||||
{
|
||||
|
|
@ -51,7 +52,14 @@ export function useProject() {
|
|||
const setSelectedProject = useProjectStore(
|
||||
(state) => state.setSelectedProject,
|
||||
);
|
||||
return { selectedProject, setSelectedProject };
|
||||
|
||||
const finalProject = selectedProject || DEFAULT_PROJECT_ID;
|
||||
|
||||
return {
|
||||
selectedProject: finalProject,
|
||||
setSelectedProject,
|
||||
resetToDefault: () => setSelectedProject(DEFAULT_PROJECT_ID)
|
||||
};
|
||||
}
|
||||
|
||||
export function useMemoryGraphPosition() {
|
||||
|
|
|
|||
10
biome.json
10
biome.json
|
|
@ -3,8 +3,8 @@
|
|||
"assist": {
|
||||
"actions": {
|
||||
"source": {
|
||||
"organizeImports": "on",
|
||||
"useSortedAttributes": "on",
|
||||
"organizeImports": "off",
|
||||
"useSortedAttributes": "off",
|
||||
"useSortedKeys": "off"
|
||||
}
|
||||
},
|
||||
|
|
@ -95,6 +95,12 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["**/*.css"],
|
||||
"linter": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"vcs": {
|
||||
|
|
|
|||
1
bun.lock
1
bun.lock
|
|
@ -29,6 +29,7 @@
|
|||
"llm-bridge": "1.0.8",
|
||||
"nanoid": "^5.1.5",
|
||||
"neverthrow": "^8.2.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"pg": "^8.16.3",
|
||||
"pino": "^9.7.0",
|
||||
"postgres": "^3.4.7",
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@
|
|||
"llm-bridge": "1.0.8",
|
||||
"nanoid": "^5.1.5",
|
||||
"neverthrow": "^8.2.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"pg": "^8.16.3",
|
||||
"pino": "^9.7.0",
|
||||
"postgres": "^3.4.7",
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"root": false,
|
||||
"extends": "//",
|
||||
"$schema": "https://biomejs.dev/schemas/2.2.2/schema.json"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
|
|||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl py-6 shadow-sm",
|
||||
className,
|
||||
)}
|
||||
data-slot="card"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
--color-background: var(--color-sm-shark-alt);
|
||||
--color-background: var(--color-sm-shark-alt);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-dodger-blue: var(--color-sm-dodger-blue);
|
||||
--font-sans:
|
||||
"Space Grotesk", "Inter", ui-sans-serif, system-ui, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
|
|
@ -84,7 +85,7 @@
|
|||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--background: oklch(0.98 0.002 286);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
|
|
@ -151,15 +152,6 @@
|
|||
--sidebar-ring: oklch(0.552 0.016 285.938);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom Scrollbar Styles */
|
||||
.custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export const GRAPH_SETTINGS = {
|
|||
initialPanY: 0,
|
||||
},
|
||||
consumer: {
|
||||
initialZoom: 0.5, // Changed from 0.1 to 0.5 for better initial visibility
|
||||
initialZoom: 0.1, // Changed from 0.1 to 0.5 for better initial visibility
|
||||
initialPanX: 400, // Pan towards center to compensate for larger layout
|
||||
initialPanY: 300, // Pan towards center to compensate for larger layout
|
||||
},
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export const MemoryGraph = ({
|
|||
showSpacesSelector = true,
|
||||
variant = "console",
|
||||
legendId,
|
||||
showLegend = true,
|
||||
highlightDocumentIds = [],
|
||||
highlightsVisible = true,
|
||||
occludedRightPx = 0,
|
||||
|
|
@ -199,32 +200,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(() => {
|
||||
|
|
@ -354,14 +370,16 @@ export const MemoryGraph = ({
|
|||
/>
|
||||
|
||||
{/* Legend */}
|
||||
<Legend
|
||||
edges={edges}
|
||||
id={legendId}
|
||||
isLoading={isLoading}
|
||||
nodes={nodes}
|
||||
variant={variant}
|
||||
isExperimental={isExperimental}
|
||||
/>
|
||||
{showLegend && (
|
||||
<Legend
|
||||
edges={edges}
|
||||
id={legendId}
|
||||
isLoading={isLoading}
|
||||
nodes={nodes}
|
||||
variant={variant}
|
||||
isExperimental={isExperimental}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Node detail panel */}
|
||||
<AnimatePresence>
|
||||
|
|
@ -421,8 +439,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"
|
||||
|
|
|
|||
|
|
@ -34,39 +34,6 @@ export const SpacesDropdown = memo<SpacesDropdownProps>(
|
|||
|
||||
return (
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
className={cn(
|
||||
"flex items-center gap-3 px-4 py-3 rounded-xl border-2 border-solid border-transparent",
|
||||
"[background-image:linear-gradient(#1a1f29,_#1a1f29),_linear-gradient(150.262deg,_#A4E8F5_0%,_#267FFA_26%,_#464646_49%,_#747474_70%,_#A4E8F5_100%)]",
|
||||
"[background-origin:border-box] [background-clip:padding-box,_border-box]",
|
||||
"shadow-[inset_0px_2px_1px_rgba(84,84,84,0.15)] backdrop-blur-md",
|
||||
"transition-all duration-200 hover:shadow-[inset_0px_2px_1px_rgba(84,84,84,0.25)]",
|
||||
"cursor-pointer min-w-60",
|
||||
)}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
type="button"
|
||||
>
|
||||
<Eye className="w-4 h-4 text-slate-300" />
|
||||
<div className="flex-1 text-left">
|
||||
<span className="text-sm text-slate-200 font-medium">
|
||||
{selectedSpace === "all"
|
||||
? "All Spaces"
|
||||
: selectedSpace || "Select space"}
|
||||
</span>
|
||||
<div className="text-xs text-slate-400">
|
||||
{selectedSpace === "all"
|
||||
? `${totalMemories} total memories`
|
||||
: `${spaceMemoryCounts[selectedSpace] || 0} memories`}
|
||||
</div>
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"w-4 h-4 text-slate-300 transition-transform duration-200",
|
||||
isOpen && "rotate-180",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute top-full left-0 right-0 mt-2 bg-slate-900/95 backdrop-blur-md border border-slate-700/40 rounded-xl shadow-xl z-20 overflow-hidden">
|
||||
<div className="p-1">
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ export interface MemoryGraphProps {
|
|||
showSpacesSelector?: boolean; // true for console, false for consumer
|
||||
variant?: "console" | "consumer"; // for different positioning and styling
|
||||
legendId?: string; // Optional ID for the legend component
|
||||
showLegend?: boolean;
|
||||
// Optional document highlight list (document custom IDs)
|
||||
highlightDocumentIds?: string[];
|
||||
// Whether highlights are currently visible (e.g., chat open)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue