From 85d4786f61e8d68a4bcdbaa8d25de0d6d560bad1 Mon Sep 17 00:00:00 2001 From: Mahesh Sanikommmu Date: Wed, 3 Sep 2025 12:52:38 -0700 Subject: [PATCH] remove scripting for extension --- apps/web/app/chat/[id]/page.tsx | 33 ++ apps/web/app/page-old.tsx | 748 ++++++++++++++++++++++++++ apps/web/components/chat-inline.tsx | 77 +++ apps/web/components/graph-debug.tsx | 1 + apps/web/components/graph-preview.tsx | 124 +++++ apps/web/components/header.tsx | 115 ++++ apps/web/components/theme-toggle.tsx | 42 ++ 7 files changed, 1140 insertions(+) create mode 100644 apps/web/app/chat/[id]/page.tsx create mode 100644 apps/web/app/page-old.tsx create mode 100644 apps/web/components/chat-inline.tsx create mode 100644 apps/web/components/graph-debug.tsx create mode 100644 apps/web/components/graph-preview.tsx create mode 100644 apps/web/components/header.tsx create mode 100644 apps/web/components/theme-toggle.tsx diff --git a/apps/web/app/chat/[id]/page.tsx b/apps/web/app/chat/[id]/page.tsx new file mode 100644 index 00000000..7076823d --- /dev/null +++ b/apps/web/app/chat/[id]/page.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { usePersistentChat } from "@/stores/chat"; +import { ChatMessages } from "@/components/views/chat/chat-messages"; +import { AppHeader } from "@/components/header"; + +export default function Page(props: { params: Promise<{ id: string }> }) { + const [id, setId] = useState(null); + const { setCurrentChatId } = usePersistentChat(); + + useEffect(() => { + async function getParams() { + const params = await props.params; + setId(params.id); + setCurrentChatId(params.id); + } + getParams(); + }, [props.params, setCurrentChatId]); + + if (!id) { + return
Loading...
; + } + + return ( +
+ +
+ +
+
+ ); +} diff --git a/apps/web/app/page-old.tsx b/apps/web/app/page-old.tsx new file mode 100644 index 00000000..ee57c82d --- /dev/null +++ b/apps/web/app/page-old.tsx @@ -0,0 +1,748 @@ +"use client"; + +import { useAuth } from "@lib/auth-context"; +import { $fetch } from "@repo/lib/api"; +import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"; +import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; +import { LoaderIcon } from "lucide-react"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import type { z } from "zod"; +import { AppHeader } from "@/components/header"; +import { ChatInline } from "@/components/chat-inline"; +import { GraphPreview } from "@/components/graph-preview"; +import { MemoriesBottomList } from "@/components/memories-bottom-list"; +import { InstallPrompt } from "@/components/install-prompt"; +import { useProject } from "@/stores"; + +type DocumentsResponse = z.infer; +type DocumentWithMemories = DocumentsResponse["documents"][0]; + +const MemoryAppPage = () => { + const { selectedProject } = useProject(); + const [injectedDocs, setInjectedDocs] = useState([]); + + // 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: ( +
+

+ Memories Overview +

+

+ This is your memory graph. Each node represents a memory, and + connections show relationships between them. +

+
+ ), + selectorId: TOUR_STEP_IDS.MEMORY_GRAPH, + position: "center", + }, + { + content: ( +
+

+ Add Memories +

+

+ Click here to add new memories to your knowledge base. You can add + text, links, or connect external sources. +

+
+ ), + selectorId: TOUR_STEP_IDS.MENU_ADD_MEMORY, + position: "right", + }, + { + content: ( +
+

+ Connections +

+

+ Connect your external accounts like Google Drive, Notion, or + OneDrive to automatically sync and organize your content. +

+
+ ), + selectorId: TOUR_STEP_IDS.MENU_CONNECTIONS, + position: "right", + }, + { + content: ( +
+

Projects

+

+ Organize your memories into projects. Switch between different + contexts easily. +

+
+ ), + selectorId: TOUR_STEP_IDS.MENU_PROJECTS, + position: "right", + }, + { + content: ( +
+

+ MCP Servers +

+

+ Access Model Context Protocol servers to give AI tools access to + your memories securely. +

+
+ ), + selectorId: TOUR_STEP_IDS.MENU_MCP, + position: "right", + }, + { + content: ( +
+

Billing

+

+ Manage your subscription and billing information. +

+
+ ), + selectorId: TOUR_STEP_IDS.MENU_BILLING, + position: "right", + }, + { + content: ( +
+

+ View Toggle +

+

+ Switch between graph view and list view to see your memories in + different ways. +

+
+ ), + selectorId: TOUR_STEP_IDS.VIEW_TOGGLE, + position: "left", + }, + { + content: ( +
+

Legend

+

+ Understand the different types of nodes and connections in your + memory graph. +

+
+ ), + selectorId: TOUR_STEP_IDS.LEGEND, + position: "left", + }, + { + content: ( +
+

+ Chat Assistant +

+

+ Ask questions or add new memories using our AI-powered chat + interface. +

+
+ ), + 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({ + queryKey: ["documents-with-memories", selectedProject], + initialPageParam: 1, + queryFn: async ({ pageParam }) => { + const response = await $fetch("@post/memories/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(); + 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 => { + 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(); + 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([ + ...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 ( +
+ {/* Main content area */} + + + +
+ handleViewModeChange("graph")} + transition={{ duration: 0.2 }} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > + {viewMode === "graph" && ( + + )} + + + Graph + + + + handleViewModeChange("list")} + transition={{ duration: 0.2 }} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > + {viewMode === "list" && ( + + )} + + + List + + +
+
+ + {/* Animated content switching */} + + {viewMode === "graph" ? ( + + +
+ +
+
+

+ Get Started with supermemory +

+
+

+ Click here to set up your AI connection +

+

or

+ +
+
+
+
+
+
+
+ ) : ( + + +
+ +
+
+

+ Get Started with supermemory +

+
+

+ Click here to set up your AI connection +

+

or

+ +
+
+
+
+
+
+
+ )} +
+ + {/* Top Bar */} +
+
+ + + + + +
+ +
+ + + + +
+ +
+ +
+
+ + {/* Floating Open Chat Button */} + {!isOpen && !isMobile && ( + + + + )} + + +
+ + {/* Chat panel - positioned absolutely */} + + + + + + + {showAddMemoryView && ( + setShowAddMemoryView(false)} + /> + )} + + {/* Tour Alert Dialog */} + + + {/* Referral/Upgrade Modal */} + setShowReferralModal(false)} + /> +
+ ); +}; + +// Wrapper component to handle auth and waitlist checks +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 + if (!user) { + return ( +
+
+ +

Loading...

+
+
+ ); + } + + // If we have a user and they have access, show the main component + return ( + <> + + + + ); +} diff --git a/apps/web/components/chat-inline.tsx b/apps/web/components/chat-inline.tsx new file mode 100644 index 00000000..fe18f5d2 --- /dev/null +++ b/apps/web/components/chat-inline.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { ArrowUp } from "lucide-react"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { generateId } from "@lib/generate-id"; +import { usePersistentChat } from "@/stores/chat"; + +export function ChatInline() { + const [message, setMessage] = useState(""); + const router = useRouter(); + const { setCurrentChatId, setConversation } = usePersistentChat(); + + const handleSend = () => { + if (!message.trim()) return; + + const newChatId = generateId(); + + const userMessage = { + id: generateId(), + role: "user" as const, + content: message.trim(), + parts: [{ type: "text" as const, text: message.trim() }], + }; + + setCurrentChatId(newChatId); + setConversation(newChatId, [userMessage]); + + router.push(`/chat/${newChatId}`); + + setMessage(""); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }; + + return ( +
+
+
+
+

+ Night owl, Mahesh +

+
+
+
+