mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-07 02:58:11 +00:00
787 lines
23 KiB
TypeScript
787 lines
23 KiB
TypeScript
"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 { useCallback, useEffect, useMemo, useState } from "react"
|
|
import type { z } from "zod"
|
|
import { ConnectAIModal } from "@/components/connect-ai-modal"
|
|
import { InstallPrompt } from "@/components/install-prompt"
|
|
import { MemoryListView } from "@/components/memory-list-view"
|
|
import Menu from "@/components/menu"
|
|
import { ProjectSelector } from "@/components/project-selector"
|
|
import { ReferralUpgradeModal } from "@/components/referral-upgrade-modal"
|
|
import type { TourStep } from "@/components/tour"
|
|
import { TourAlertDialog, useTour } from "@/components/tour"
|
|
import { AddMemoryView } from "@/components/views/add-memory"
|
|
import { ChatRewrite } from "@/components/views/chat"
|
|
import { TOUR_STEP_IDS, TOUR_STORAGE_KEY } from "@/lib/tour-constants"
|
|
import { useViewMode } from "@/lib/view-mode-context"
|
|
import { useChatOpen, useProject } from "@/stores"
|
|
import { useGraphHighlights } from "@/stores/highlights"
|
|
|
|
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
|
|
type DocumentWithMemories = DocumentsResponse["documents"][0]
|
|
|
|
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)
|
|
|
|
// 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 ?? []
|
|
},
|
|
staleTime: 5 * 60 * 1000,
|
|
})
|
|
|
|
const isCurrentProjectExperimental = !!projectsMeta.find(
|
|
(p: any) => 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
|
|
const MAX_TOTAL = 1000
|
|
|
|
const {
|
|
data,
|
|
error,
|
|
isPending,
|
|
isFetchingNextPage,
|
|
hasNextPage,
|
|
fetchNextPage,
|
|
} = useInfiniteQuery<DocumentsResponse, Error>({
|
|
queryKey: ["documents-with-memories", selectedProject],
|
|
initialPageParam: 1,
|
|
queryFn: async ({ pageParam }) => {
|
|
const response = await $fetch("@post/documents/documents", {
|
|
body: {
|
|
page: pageParam as number,
|
|
limit: (pageParam as number) === 1 ? (IS_DEV ? 500 : 500) : PAGE_SIZE,
|
|
sort: "createdAt",
|
|
order: "desc",
|
|
containerTags: selectedProject ? [selectedProject] : undefined,
|
|
},
|
|
disableValidation: true,
|
|
})
|
|
|
|
if (response.error) {
|
|
throw new Error(response.error?.message || "Failed to fetch documents")
|
|
}
|
|
|
|
return response.data
|
|
},
|
|
getNextPageParam: (lastPage, allPages) => {
|
|
const loaded = allPages.reduce(
|
|
(acc, p) => acc + (p.documents?.length ?? 0),
|
|
0,
|
|
)
|
|
if (loaded >= MAX_TOTAL) return undefined
|
|
|
|
const { currentPage, totalPages } = lastPage.pagination
|
|
if (currentPage < totalPages) {
|
|
return currentPage + 1
|
|
}
|
|
return undefined
|
|
},
|
|
staleTime: 5 * 60 * 1000,
|
|
})
|
|
|
|
const baseDocuments = useMemo(() => {
|
|
return (
|
|
data?.pages.flatMap((p: DocumentsResponse) => p.documents ?? []) ?? []
|
|
)
|
|
}, [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])
|
|
|
|
const totalLoaded = allDocuments.length
|
|
const hasMore = hasNextPage
|
|
const isLoadingMore = isFetchingNextPage
|
|
|
|
const loadMoreDocuments = useCallback(async (): Promise<void> => {
|
|
if (hasNextPage && !isFetchingNextPage) {
|
|
await fetchNextPage()
|
|
return
|
|
}
|
|
return
|
|
}, [hasNextPage, isFetchingNextPage, fetchNextPage])
|
|
|
|
// Reset injected docs when project changes
|
|
useEffect(() => {
|
|
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 {}
|
|
}
|
|
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],
|
|
)
|
|
|
|
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"
|
|
|
|
return () => {
|
|
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">
|
|
{/* 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>
|
|
|
|
<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 && (
|
|
<AddMemoryView
|
|
initialTab="note"
|
|
onClose={() => setShowAddMemoryView(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
|
|
export default function Page() {
|
|
const { user, session } = useAuth()
|
|
|
|
useEffect(() => {
|
|
const url = new URL(window.location.href)
|
|
const authenticateChromeExtension = url.searchParams.get(
|
|
"extension-auth-success",
|
|
)
|
|
|
|
if (authenticateChromeExtension) {
|
|
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())
|
|
}
|
|
}
|
|
}, [user, session])
|
|
|
|
// Show loading state while checking authentication and waitlist status
|
|
if (!user) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-[#0f1419]">
|
|
<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>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// If we have a user and they have access, show the main component
|
|
return (
|
|
<>
|
|
<MemoryGraphPage />
|
|
<InstallPrompt />
|
|
</>
|
|
)
|
|
}
|