diff --git a/apps/web/app/(navigation)/chat/[id]/page.tsx b/apps/web/app/(navigation)/chat/[id]/page.tsx new file mode 100644 index 00000000..9bb2038b --- /dev/null +++ b/apps/web/app/(navigation)/chat/[id]/page.tsx @@ -0,0 +1,39 @@ +"use client" + +import { useEffect } from "react" +import { useParams } from "next/navigation" +import { usePersistentChat } from "@/stores" +import { ChatMessages } from "@/components/views/chat/chat-messages" + +export default function ChatPage() { + const params = useParams() + const { setCurrentChatId, getCurrentChat } = usePersistentChat() + + const chatId = params.id as string + + useEffect(() => { + if (chatId) { + setCurrentChatId(chatId) + } + }, [chatId, setCurrentChatId]) + + const currentChat = getCurrentChat() + + return ( +
+
+
+

+ {currentChat?.title || "New Chat"} +

+
+ +
+
+ +
+
+
+
+ ) +} diff --git a/apps/web/app/(navigation)/layout.tsx b/apps/web/app/(navigation)/layout.tsx new file mode 100644 index 00000000..da5a87fb --- /dev/null +++ b/apps/web/app/(navigation)/layout.tsx @@ -0,0 +1,57 @@ +"use client" + +import { Header } from "@/components/header" +import { AddMemoryView } from "@/components/views/add-memory" +import { useEffect, useState } from "react" + +export default function NavigationLayout({ + children, +}: { + children: React.ReactNode +}) { + const [showAddMemoryView, setShowAddMemoryView] = useState(false) + useEffect(() => { + const handleKeydown = (event: KeyboardEvent) => { + const target = event.target as HTMLElement + const isInputField = + target.tagName === "INPUT" || + target.tagName === "TEXTAREA" || + target.isContentEditable || + target.closest('[contenteditable="true"]') + + if (isInputField) return + + // add memory shortcut + if ( + event.key === "c" && + !event.ctrlKey && + !event.metaKey && + !event.altKey && + !event.shiftKey + ) { + event.preventDefault() + setShowAddMemoryView(true) + } + } + + document.addEventListener("keydown", handleKeydown) + + return () => { + document.removeEventListener("keydown", handleKeydown) + } + }, []) + return ( +
+
+
setShowAddMemoryView(true)} /> +
+ {children} + {showAddMemoryView && ( + setShowAddMemoryView(false)} + /> + )} +
+ ) +} diff --git a/apps/web/app/(navigation)/page.tsx b/apps/web/app/(navigation)/page.tsx new file mode 100644 index 00000000..b8c81985 --- /dev/null +++ b/apps/web/app/(navigation)/page.tsx @@ -0,0 +1,81 @@ +"use client" + +import { useOnboardingStorage } from "@hooks/use-onboarding-storage" +import { useAuth } from "@lib/auth-context" +import { ChevronsDown, LoaderIcon } from "lucide-react" +import { useRouter } from "next/navigation" +import { useEffect } from "react" +import { InstallPrompt } from "@/components/install-prompt" +import { ChatInput } from "@/components/chat-input" +import { BackgroundPlus } from "@ui/components/grid-plus" +import { Memories } from "@/components/memories" + +export default function Page() { + const { user, session } = useAuth() + const { shouldShowOnboarding, isLoading: onboardingLoading } = + useOnboardingStorage() + const router = useRouter() + + 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]) + + useEffect(() => { + if (user && !onboardingLoading && shouldShowOnboarding()) { + router.push("/onboarding") + } + }, [user, shouldShowOnboarding, onboardingLoading, router]) + + if (!user || onboardingLoading) { + return ( +
+
+ +

Loading...

+
+
+ ) + } + + if (shouldShowOnboarding()) { + return null + } + + return ( +
+
+ +
+ +
+ +
+ +

Scroll down to see memories

+
+
+ + + +
+ ) +} diff --git a/apps/web/app/(navigation)/settings/billing/page.tsx b/apps/web/app/(navigation)/settings/billing/page.tsx new file mode 100644 index 00000000..2b8e6ba0 --- /dev/null +++ b/apps/web/app/(navigation)/settings/billing/page.tsx @@ -0,0 +1,12 @@ +"use client" +import { BillingView } from "@/components/views/billing" +export default function BillingPage() { + return ( +
+

+ Billing & Subscription +

+ +
+ ) +} \ No newline at end of file diff --git a/apps/web/app/(navigation)/settings/integrations/page.tsx b/apps/web/app/(navigation)/settings/integrations/page.tsx new file mode 100644 index 00000000..7fedd143 --- /dev/null +++ b/apps/web/app/(navigation)/settings/integrations/page.tsx @@ -0,0 +1,10 @@ +"use client" +import { IntegrationsView } from "@/components/views/integrations" +export default function IntegrationsPage() { + return ( +
+

Integrations

+ +
+ ) +} \ No newline at end of file diff --git a/apps/web/app/(navigation)/settings/layout.tsx b/apps/web/app/(navigation)/settings/layout.tsx new file mode 100644 index 00000000..342e640c --- /dev/null +++ b/apps/web/app/(navigation)/settings/layout.tsx @@ -0,0 +1,52 @@ +"use client" + +import { Button } from "@ui/components/button" +import { useRouter, usePathname } from "next/navigation" +import { cn } from "@repo/lib/utils" + +export default function SettingsPageLayout({ + children, +}: { + children: React.ReactNode +}) { + const router = useRouter() + const pathname = usePathname() + + const navItems = [ + { label: "Profile", path: "/settings" }, + { label: "Integrations", path: "/settings/integrations" }, + { label: "Billing", path: "/settings/billing" }, + { label: "Support", path: "/settings/support" }, + ] + + return ( +
+
+
+ + {children} +
+
+
+ ) +} diff --git a/apps/web/app/(navigation)/settings/page.tsx b/apps/web/app/(navigation)/settings/page.tsx new file mode 100644 index 00000000..c9046fba --- /dev/null +++ b/apps/web/app/(navigation)/settings/page.tsx @@ -0,0 +1,12 @@ +"use client" +import { ProfileView } from "@/components/views/profile" +export default function ProfilePage() { + return ( +
+

+ Profile Settings +

+ +
+ ) +} \ No newline at end of file diff --git a/apps/web/app/(navigation)/settings/support/page.tsx b/apps/web/app/(navigation)/settings/support/page.tsx new file mode 100644 index 00000000..de4b3c78 --- /dev/null +++ b/apps/web/app/(navigation)/settings/support/page.tsx @@ -0,0 +1,125 @@ +"use client" + +import { Button } from "@repo/ui/components/button" +import { HeadingH3Bold } from "@repo/ui/text/heading/heading-h3-bold" +import { ExternalLink, Mail, MessageCircle } from "lucide-react" + +export default function SupportPage() { + return ( +
+

+ Support & Help +

+ +
+ {/* Contact Options */} +
+ Get Help +

+ Need assistance? We're here to help! Choose the best way to reach + us. +

+ +
+ + + +
+
+ + {/* FAQ Section */} +
+ + Frequently Asked Questions + + +
+
+

+ How do I upgrade to Pro? +

+

+ Go to the Billing tab in settings and click "Upgrade to Pro". + You'll be redirected to our secure payment processor. +

+
+ +
+

+ What's included in the Pro plan? +

+

+ Pro includes 5,000 memories (vs 200 in free), 10 connections to + external services like Google Drive and Notion, advanced search + features, and priority support. +

+
+ +
+

+ How do connections work? +

+

+ Connections let you sync documents from Google Drive, Notion, + and OneDrive automatically. supermemory will index and make them + searchable. +

+
+ +
+

+ Can I cancel my subscription anytime? +

+

+ Yes! You can cancel anytime from the Billing tab. Your Pro + features will remain active until the end of your billing + period. +

+
+
+
+ + {/* Feedback Section */} +
+ + Feedback & Feature Requests + +

+ Have ideas for new features or improvements? We'd love to hear from + you! +

+ + +
+
+
+ ) +} \ No newline at end of file diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 70d4916f..622eb2c1 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,5 +1,5 @@ import type { Metadata } from "next" -import { Inter, JetBrains_Mono, Instrument_Serif } from "next/font/google" +import { Space_Grotesk } from "next/font/google" import "../globals.css" import "@ui/globals.css" import { AuthProvider } from "@lib/auth-context" @@ -11,25 +11,15 @@ import { Suspense } from "react" import { Toaster } from "sonner" import { MobilePanelProvider } from "@/lib/mobile-panel-context" import { NuqsAdapter } from "nuqs/adapters/next/app" +import { ThemeProvider } from "@/lib/theme-provider" import { ViewModeProvider } from "@/lib/view-mode-context" -const sans = Inter({ +const font = Space_Grotesk({ subsets: ["latin"], variable: "--font-sans", }) -const mono = JetBrains_Mono({ - subsets: ["latin"], - variable: "--font-mono", -}) - -const serif = Instrument_Serif({ - subsets: ["latin"], - variable: "--font-serif", - weight: ["400"], -}) - export const metadata: Metadata = { metadataBase: new URL("https://app.supermemory.ai"), description: "Your memories, wherever you are", @@ -42,33 +32,39 @@ export default function RootLayout({ children: React.ReactNode }>) { return ( - - - + + - - - - - - - - {children} - - - - - - - - - + + + + + + + + + {children} + + + + + + + + + + ) diff --git a/apps/web/app/onboarding/animated-text.tsx b/apps/web/app/onboarding/animated-text.tsx index c3616482..c32abb10 100644 --- a/apps/web/app/onboarding/animated-text.tsx +++ b/apps/web/app/onboarding/animated-text.tsx @@ -1,53 +1,62 @@ -'use client'; -import { useEffect } from 'react'; -import { TextEffect } from '@/components/text-effect'; +"use client" +import { useEffect } from "react" +import { TextEffect } from "@/components/text-effect" -export function AnimatedText({ children, trigger, delay }: { children: string, trigger: boolean, delay: number }) { - const blurSlideVariants = { - container: { - hidden: { opacity: 0 }, - visible: { - opacity: 1, - transition: { staggerChildren: 0.01 }, - }, - exit: { - transition: { staggerChildren: 0.01, staggerDirection: 1 }, - }, - }, - item: { - hidden: { - opacity: 0, - filter: 'blur(10px) brightness(0%)', - y: 0, - }, - visible: { - opacity: 1, - y: 0, - filter: 'blur(0px) brightness(100%)', - transition: { - duration: 0.4, - }, - }, - exit: { - opacity: 0, - y: -30, - filter: 'blur(10px) brightness(0%)', - transition: { - duration: 0.3, - }, - }, - }, - }; +export function AnimatedText({ + children, + trigger, + delay, +}: { + children: string + trigger: boolean + delay: number +}) { + const blurSlideVariants = { + container: { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { staggerChildren: 0.01 }, + }, + exit: { + transition: { staggerChildren: 0.01, staggerDirection: 1 }, + }, + }, + item: { + hidden: { + opacity: 0, + filter: "blur(10px) brightness(0%)", + y: 0, + }, + visible: { + opacity: 1, + y: 0, + filter: "blur(0px) brightness(100%)", + transition: { + duration: 0.4, + }, + }, + exit: { + opacity: 0, + y: -30, + filter: "blur(10px) brightness(0%)", + transition: { + duration: 0.3, + }, + }, + }, + } - return ( - - {children} - - ); + return ( + + {children} + + ) } diff --git a/apps/web/app/onboarding/connections-form.tsx b/apps/web/app/onboarding/connections-form.tsx deleted file mode 100644 index f71f17ce..00000000 --- a/apps/web/app/onboarding/connections-form.tsx +++ /dev/null @@ -1,213 +0,0 @@ -"use client"; - -import { motion, type Transition } from "framer-motion"; -import { Button } from "@ui/components/button"; -import { useOnboarding } from "./onboarding-context"; -import { $fetch } from "@lib/api"; -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import type { ConnectionResponseSchema } from "@repo/validation/api"; -import type { z } from "zod"; -import { Check } from "lucide-react"; -import { toast } from "sonner"; -import { analytics } from "@/lib/analytics"; -import { useProject } from "@/stores"; -import { NavMenu } from "./nav-menu"; - -type Connection = z.infer; - -const CONNECTORS = { - "google-drive": { - title: "Google Drive", - description: "Supermemory can use the documents and files in your Google Drive to better understand and assist you.", - iconSrc: "/images/gdrive.svg", - }, - notion: { - title: "Notion", - description: "Help Supermemory understand how you organize your life and what you have going on by connecting your Notion account.", - iconSrc: "/images/notion.svg", - }, - onedrive: { - title: "OneDrive", - description: "By integrating with OneDrive, Supermemory can better understand both your previous and your current work.", - iconSrc: "/images/onedrive.svg", - }, -} as const; - -type ConnectorProvider = keyof typeof CONNECTORS; - -const containerVariants = { - hidden: { opacity: 0 }, - visible: { - opacity: 1, - transition: { staggerChildren: 0.15, delayChildren: 0.1 } satisfies Transition, - }, -}; - -const itemVariants = { - hidden: { opacity: 0, y: 16 }, - visible: { - opacity: 1, - y: 0, - transition: { type: "spring", stiffness: 500, damping: 35, mass: 0.8 } satisfies Transition, - }, -}; - -function ConnectionCard({ - title, - description, - iconSrc, - isConnected = false, - onConnect, - isConnecting = false -}: { - title: string; - description: string; - iconSrc: string; - isConnected?: boolean; - onConnect?: () => void; - isConnecting?: boolean; -}) { - return ( -
- {title} -
-
- {title} -

{title}

-
-

{description}

-
-
- {isConnected ? ( - - ) : ( - - )} -
-
- ); -} - -export function ConnectionsForm() { - const { totalSteps, nextStep, getStepNumberFor } = useOnboarding(); - const { selectedProject } = useProject(); - - const { data: connections = [] } = useQuery({ - queryKey: ["connections"], - queryFn: async () => { - const response = await $fetch("@post/connections/list", { - body: { - containerTags: [], - }, - }); - - if (response.error) { - throw new Error( - response.error?.message || "Failed to load connections", - ); - } - - return response.data as Connection[]; - }, - staleTime: 30 * 1000, - refetchInterval: 60 * 1000, - }); - - const addConnectionMutation = useMutation({ - mutationFn: async (provider: ConnectorProvider) => { - const response = await $fetch("@post/connections/:provider", { - params: { provider }, - body: { - redirectUrl: window.location.href, - containerTags: [selectedProject], - }, - }); - - // biome-ignore lint/style/noNonNullAssertion: its fine - if ("data" in response && !("error" in response.data!)) { - return response.data; - } - - throw new Error(response.error?.message || "Failed to connect"); - }, - onSuccess: (data, provider) => { - analytics.connectionAdded(provider); - analytics.connectionAuthStarted(); - if (data?.authLink) { - window.location.href = data.authLink; - } - }, - onError: (error, provider) => { - analytics.connectionAuthFailed(); - toast.error(`Failed to connect ${provider}`, { - description: error instanceof Error ? error.message : "Unknown error", - }); - }, - }); - - function isConnectorConnected(provider: ConnectorProvider): boolean { - return connections.some(connection => connection.provider === provider); - } - - function handleConnect(provider: ConnectorProvider) { - addConnectionMutation.mutate(provider); - } - - return ( -
-
- -

- Step {getStepNumberFor("connections")} of {totalSteps} -

-
-

Connect your accounts

-

- Help Supermemory get to know you and your documents better - {/* The more context you provide, the better Supermemory becomes */} - {/* Supermemory understands your needs and goals better with more context */} - {/* Supermemory understands you better when it integrates with your apps */} -

-
- - {Object.entries(CONNECTORS).map(([provider, config]) => { - const providerKey = provider as ConnectorProvider; - const isConnected = isConnectorConnected(providerKey); - const isConnecting = addConnectionMutation.isPending && addConnectionMutation.variables === providerKey; - - return ( - - handleConnect(providerKey)} - isConnecting={isConnecting} - /> - - ); - })} - -
- -
-
- ); -} \ No newline at end of file diff --git a/apps/web/app/onboarding/extension-form.tsx b/apps/web/app/onboarding/extension-form.tsx index c94512e0..63e7dff1 100644 --- a/apps/web/app/onboarding/extension-form.tsx +++ b/apps/web/app/onboarding/extension-form.tsx @@ -636,7 +636,9 @@ export function ExtensionForm() { Step {getStepNumberFor("extension")} of {totalSteps}

-

Install the Chrome extension

+

+ Install the Chrome extension +

{/* Install the Supermemory extension to start saving and organizing everything that matters. */} Bring Supermemory everywhere @@ -706,7 +708,7 @@ export function ExtensionForm() { - - - ) -} \ No newline at end of file + return ( + + +

Hey there!

+ {triggers.first && ( + + + Intelligence without memory + + + )} + {triggers.second && ( + + + is just sophisticated randomness. + + + )} + + + + + + ) +} diff --git a/apps/web/app/onboarding/name-form.tsx b/apps/web/app/onboarding/name-form.tsx index 2b9520c9..df734601 100644 --- a/apps/web/app/onboarding/name-form.tsx +++ b/apps/web/app/onboarding/name-form.tsx @@ -63,7 +63,6 @@ export function NameForm() {
cleanups.forEach((cleanup) => clearTimeout(cleanup)) + return () => cleanups.forEach(clearTimeout) }, [currentStep]) // Set orbs as revealed once the fourth trigger is activated OR if we're on any non-intro step diff --git a/apps/web/app/onboarding/welcome.tsx b/apps/web/app/onboarding/welcome.tsx index 3f73e43a..93313f7a 100644 --- a/apps/web/app/onboarding/welcome.tsx +++ b/apps/web/app/onboarding/welcome.tsx @@ -15,7 +15,7 @@ export function Welcome() { return (
-

Welcome to Supermemory

+

Welcome to Supermemory

We're excited to have you on board.

diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx deleted file mode 100644 index 8886067d..00000000 --- a/apps/web/app/page.tsx +++ /dev/null @@ -1,622 +0,0 @@ -"use client" - -import { useIsMobile } from "@hooks/use-mobile" -import { useOnboardingStorage } from "@hooks/use-onboarding-storage" -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 { 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 { AddMemoryView } from "@/components/views/add-memory" -import { ChatRewrite } from "@/components/views/chat" -import { useViewMode } from "@/lib/view-mode-context" -import { useChatOpen, useProject } from "@/stores" -import { useGraphHighlights } from "@/stores/highlights" - -type DocumentsResponse = z.infer -type DocumentWithMemories = DocumentsResponse["documents"][0] - -const MemoryGraphPage = () => { - const { documentIds: allHighlightDocumentIds } = useGraphHighlights() - const isMobile = useIsMobile() - const { viewMode, setViewMode } = useViewMode() - const { selectedProject } = useProject() - const { isOpen, setIsOpen } = useChatOpen() - const [injectedDocs, setInjectedDocs] = useState([]) - const [showAddMemoryView, setShowAddMemoryView] = useState(false) - const [showReferralModal, setShowReferralModal] = useState(false) - const [showConnectAIModal, setShowConnectAIModal] = useState(false) - const [isHelpHovered, setIsHelpHovered] = useState(false) - - // Fetch projects meta to detect experimental flag - const { data: projectsMeta = [] } = useQuery({ - queryKey: ["projects"], - queryFn: async () => { - const response = await $fetch("@get/projects") - return response.data?.projects ?? [] - }, - staleTime: 5 * 60 * 1000, - }) - - const isCurrentProjectExperimental = !!projectsMeta.find( - (p: any) => p.containerTag === selectedProject, - )?.isExperimental - - // 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/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() - 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/documents/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(() => { - if (allDocuments.length === 0) { - setShowConnectAIModal(true) - } - }, [allDocuments.length]) - - // 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)} - /> - )} - - {/* Referral/Upgrade Modal */} - setShowReferralModal(false)} - /> -
- ) -} - -// Wrapper component to handle auth and waitlist checks -export default function Page() { - const { user, session } = useAuth() - const { shouldShowOnboarding, isLoading: onboardingLoading } = - useOnboardingStorage() - const router = useRouter() - - 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]) - - useEffect(() => { - if (user && !onboardingLoading && shouldShowOnboarding()) { - router.push("/onboarding") - } - }, [user, shouldShowOnboarding, onboardingLoading, router]) - - if (!user || onboardingLoading) { - return ( -
-
- -

Loading...

-
-
- ) - } - - if (shouldShowOnboarding()) { - return null - } - - return ( - <> - - - - ) -} diff --git a/apps/web/button.tsx b/apps/web/button.tsx deleted file mode 100644 index 3a671e03..00000000 --- a/apps/web/button.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { cn } from "@lib/utils"; -import { Slot } from "@radix-ui/react-slot"; -import { cva, type VariantProps } from "class-variance-authority"; -import type * as React from "react"; - -const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-2 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", - { - variants: { - variant: { - default: - "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", - destructive: - "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", - secondary: - "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", - ghost: - "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - }, -); - -function Button({ - className, - variant, - size, - asChild = false, - ...props -}: React.ComponentProps<"button"> & - VariantProps & { - asChild?: boolean; - }) { - const Comp = asChild ? Slot : "button"; - - return ( - - ); -} - -export { Button, buttonVariants }; diff --git a/apps/web/components/chat-input.tsx b/apps/web/components/chat-input.tsx new file mode 100644 index 00000000..cf7409fc --- /dev/null +++ b/apps/web/components/chat-input.tsx @@ -0,0 +1,80 @@ +"use client" + +import { useState } from "react" +import { useRouter } from "next/navigation" +import { generateId } from "@lib/generate-id" +import { usePersistentChat } from "@/stores/chat" +import { ArrowUp } from "lucide-react" +import { Button } from "@ui/components/button" +import { ProjectSelector } from "./project-selector" + +export function ChatInput() { + const [message, setMessage] = useState("") + const router = useRouter() + const { setCurrentChatId } = usePersistentChat() + + const handleSend = () => { + if (!message.trim()) return + + const newChatId = generateId() + + setCurrentChatId(newChatId) + + // Store the initial message in sessionStorage for the chat page to pick up + sessionStorage.setItem(`chat-initial-${newChatId}`, message.trim()) + + router.push(`/chat/${newChatId}`) + + setMessage("") + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault() + handleSend() + } + } + + return ( +
+
+
+

+ Good evening, Mahesh +

+
+
+
{ + e.preventDefault() + if (!message.trim()) return + handleSend() + }} + > +