diff --git a/apps/web/app/(app)/layout.tsx b/apps/web/app/(app)/layout.tsx new file mode 100644 index 000000000..765abcf63 --- /dev/null +++ b/apps/web/app/(app)/layout.tsx @@ -0,0 +1,12 @@ +"use client" + +import { MobileBanner } from "@/components/new/mobile-banner" + +export default function AppLayout({ children }: { children: React.ReactNode }) { + return ( + <> + + {children} + + ) +} diff --git a/apps/web/app/new/onboarding/layout.tsx b/apps/web/app/(app)/onboarding/layout.tsx similarity index 100% rename from apps/web/app/new/onboarding/layout.tsx rename to apps/web/app/(app)/onboarding/layout.tsx diff --git a/apps/web/app/new/onboarding/page.tsx b/apps/web/app/(app)/onboarding/page.tsx similarity index 87% rename from apps/web/app/new/onboarding/page.tsx rename to apps/web/app/(app)/onboarding/page.tsx index 19e9c62a8..460fe92fb 100644 --- a/apps/web/app/new/onboarding/page.tsx +++ b/apps/web/app/(app)/onboarding/page.tsx @@ -7,7 +7,7 @@ export default function OnboardingPage() { const router = useRouter() useEffect(() => { - router.replace("/new/onboarding/welcome?step=input") + router.replace("/onboarding/welcome?step=input") }, [router]) return ( diff --git a/apps/web/app/new/onboarding/setup/layout.tsx b/apps/web/app/(app)/onboarding/setup/layout.tsx similarity index 93% rename from apps/web/app/new/onboarding/setup/layout.tsx rename to apps/web/app/(app)/onboarding/setup/layout.tsx index 6694f5fd3..2ef7d4866 100644 --- a/apps/web/app/new/onboarding/setup/layout.tsx +++ b/apps/web/app/(app)/onboarding/setup/layout.tsx @@ -47,21 +47,21 @@ export default function SetupLayout({ children }: { children: ReactNode }) { const goToStep = useCallback( (step: SetupStep) => { analytics.onboardingStepViewed({ step, trigger: "user" }) - router.push(`/new/onboarding/setup?step=${step}`) + router.push(`/onboarding/setup?step=${step}`) }, [router], ) const goToWelcome = useCallback( (step = "input") => { - router.push(`/new/onboarding/welcome?step=${step}`) + router.push(`/onboarding/welcome?step=${step}`) }, [router], ) const finishOnboarding = useCallback(() => { resetOnboarding() - router.push("/new") + router.push("/") }, [router, resetOnboarding]) useEffect(() => { diff --git a/apps/web/app/new/onboarding/setup/page.tsx b/apps/web/app/(app)/onboarding/setup/page.tsx similarity index 100% rename from apps/web/app/new/onboarding/setup/page.tsx rename to apps/web/app/(app)/onboarding/setup/page.tsx diff --git a/apps/web/app/new/onboarding/welcome/layout.tsx b/apps/web/app/(app)/onboarding/welcome/layout.tsx similarity index 93% rename from apps/web/app/new/onboarding/welcome/layout.tsx rename to apps/web/app/(app)/onboarding/welcome/layout.tsx index 07d3c4b39..2b889d385 100644 --- a/apps/web/app/new/onboarding/welcome/layout.tsx +++ b/apps/web/app/(app)/onboarding/welcome/layout.tsx @@ -92,7 +92,7 @@ export default function WelcomeLayout({ children }: { children: ReactNode }) { setTimeout(() => { if (isMountedRef.current) { analytics.onboardingStepViewed({ step: "welcome", trigger: "auto" }) - router.replace("/new/onboarding/welcome?step=welcome") + router.replace("/onboarding/welcome?step=welcome") } }, 2000), ) @@ -104,7 +104,7 @@ export default function WelcomeLayout({ children }: { children: ReactNode }) { step: "username", trigger: "auto", }) - router.replace("/new/onboarding/welcome?step=username") + router.replace("/onboarding/welcome?step=username") } }, 2000), ) @@ -128,14 +128,14 @@ export default function WelcomeLayout({ children }: { children: ReactNode }) { const goToStep = useCallback( (step: WelcomeStep) => { analytics.onboardingStepViewed({ step, trigger: "user" }) - router.push(`/new/onboarding/welcome?step=${step}`) + router.push(`/onboarding/welcome?step=${step}`) }, [router], ) const goToSetup = useCallback( (step = "relatable") => { - router.push(`/new/onboarding/setup?step=${step}`) + router.push(`/onboarding/setup?step=${step}`) }, [router], ) diff --git a/apps/web/app/new/onboarding/welcome/page.tsx b/apps/web/app/(app)/onboarding/welcome/page.tsx similarity index 100% rename from apps/web/app/new/onboarding/welcome/page.tsx rename to apps/web/app/(app)/onboarding/welcome/page.tsx diff --git a/apps/web/app/new/page.tsx b/apps/web/app/(app)/page.tsx similarity index 69% rename from apps/web/app/new/page.tsx rename to apps/web/app/(app)/page.tsx index 7115dff66..96091f181 100644 --- a/apps/web/app/new/page.tsx +++ b/apps/web/app/(app)/page.tsx @@ -1,6 +1,7 @@ "use client" import { useState, useCallback, useEffect } from "react" +import { useQueryState } from "nuqs" import { Header } from "@/components/new/header" import { ChatSidebar } from "@/components/new/chat" import { MemoriesGrid } from "@/components/new/memories-grid" @@ -23,11 +24,20 @@ import { } from "@/stores/quick-note-draft" import { analytics } from "@/lib/analytics" import { useDocumentMutations } from "@/hooks/use-document-mutations" -import { useQuery } from "@tanstack/react-query" +import { useQuery, useQueryClient } from "@tanstack/react-query" import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api" import type { z } from "zod" import { useViewMode } from "@/lib/view-mode-context" import { cn } from "@lib/utils" +import { + addDocumentParam, + mcpParam, + searchParam, + qParam, + docParam, + fullscreenParam, + chatParam, +} from "@/lib/search-params" type DocumentsResponse = z.infer type DocumentWithMemories = DocumentsResponse["documents"][0] @@ -36,17 +46,56 @@ export default function NewPage() { const isMobile = useIsMobile() const { selectedProject } = useProject() const { viewMode } = useViewMode() - const [isAddDocumentOpen, setIsAddDocumentOpen] = useState(false) - const [isMCPModalOpen, setIsMCPModalOpen] = useState(false) - const [isSearchOpen, setIsSearchOpen] = useState(false) - const [selectedDocument, setSelectedDocument] = - useState(null) - const [isDocumentModalOpen, setIsDocumentModalOpen] = useState(false) + const queryClient = useQueryClient() - const [isFullScreenNoteOpen, setIsFullScreenNoteOpen] = useState(false) + // URL-driven modal states + const [addDoc, setAddDoc] = useQueryState("add", addDocumentParam) + const [isMCPOpen, setIsMCPOpen] = useQueryState("mcp", mcpParam) + const [isSearchOpen, setIsSearchOpen] = useQueryState("search", searchParam) + const [searchPrefill, setSearchPrefill] = useQueryState("q", qParam) + const [docId, setDocId] = useQueryState("doc", docParam) + const [isFullscreen, setIsFullscreen] = useQueryState("fullscreen", fullscreenParam) + const [isChatOpen, setIsChatOpen] = useQueryState("chat", chatParam) + + // Ephemeral local state (not worth URL-encoding) const [fullscreenInitialContent, setFullscreenInitialContent] = useState("") const [queuedChatSeed, setQueuedChatSeed] = useState(null) - const [searchPrefill, setSearchPrefill] = useState("") + const [selectedDocument, setSelectedDocument] = + useState(null) + + // Clear document when docId is removed (e.g. back button) + useEffect(() => { + if (!docId) setSelectedDocument(null) + }, [docId]) + + // Resolve document from cache when loading with ?doc= (deep link / refresh) + useEffect(() => { + if (!docId || selectedDocument) return + + const tryResolve = () => { + const queries = queryClient.getQueriesData<{ + pages: DocumentsResponse[] + }>({ queryKey: ["documents-with-memories"] }) + for (const [, data] of queries) { + if (!data?.pages) continue + for (const page of data.pages) { + const doc = page.documents?.find((d) => d.id === docId) + if (doc) { + setSelectedDocument(doc) + return true + } + } + } + return false + } + + if (tryResolve()) return + + const unsubscribe = queryClient.getQueryCache().subscribe(() => { + if (tryResolve()) unsubscribe() + }) + return unsubscribe + }, [docId, selectedDocument, queryClient]) const resetDraft = useQuickNoteDraftReset(selectedProject) const { draft: quickNoteDraft } = useQuickNoteDraft(selectedProject || "") @@ -54,7 +103,7 @@ export default function NewPage() { const { noteMutation } = useDocumentMutations({ onClose: () => { resetDraft() - setIsFullScreenNoteOpen(false) + setIsFullscreen(false) }, }) @@ -74,7 +123,6 @@ export default function NewPage() { const spaceId = selectedProject || "sm_project_default" const cacheKey = `${process.env.NEXT_PUBLIC_BACKEND_URL}/v3/space-highlights?spaceId=${spaceId}` - // Check Cache API for a fresh response const cache = await caches.open(HIGHLIGHTS_CACHE_NAME) const cached = await cache.match(cacheKey) if (cached) { @@ -107,7 +155,6 @@ export default function NewPage() { const data = await response.json() - // Store in Cache API with timestamp const cacheResponse = new Response(JSON.stringify(data), { headers: { "Content-Type": "application/json", @@ -124,26 +171,24 @@ export default function NewPage() { useHotkeys("c", () => { analytics.addDocumentModalOpened() - setIsAddDocumentOpen(true) + setAddDoc("note") }) useHotkeys("mod+k", (e) => { e.preventDefault() analytics.searchOpened({ source: "hotkey" }) setIsSearchOpen(true) }) - const [isChatOpen, setIsChatOpen] = useState(!isMobile) - - useEffect(() => { - setIsChatOpen(!isMobile) - }, [isMobile]) - const handleOpenDocument = useCallback((document: DocumentWithMemories) => { - if (document.id) { - analytics.documentModalOpened({ document_id: document.id }) - } - setSelectedDocument(document) - setIsDocumentModalOpen(true) - }, []) + const handleOpenDocument = useCallback( + (document: DocumentWithMemories) => { + if (document.id) { + analytics.documentModalOpened({ document_id: document.id }) + setSelectedDocument(document) + setDocId(document.id) + } + }, + [setDocId], + ) const handleQuickNoteSave = useCallback( (content: string) => { @@ -187,23 +232,33 @@ export default function NewPage() { [selectedProject, noteMutation, fullscreenInitialContent], ) - const handleMaximize = useCallback((content: string) => { - analytics.fullscreenNoteModalOpened() - setFullscreenInitialContent(content) - setIsFullScreenNoteOpen(true) - }, []) + const handleMaximize = useCallback( + (content: string) => { + analytics.fullscreenNoteModalOpened() + setFullscreenInitialContent(content) + setIsFullscreen(true) + }, + [setIsFullscreen], + ) - const handleHighlightsChat = useCallback((seed: string) => { - setQueuedChatSeed(seed) - setIsChatOpen(true) - }, []) + const handleHighlightsChat = useCallback( + (seed: string) => { + setQueuedChatSeed(seed) + setIsChatOpen(true) + }, + [setIsChatOpen], + ) - const handleHighlightsShowRelated = useCallback((query: string) => { - analytics.searchOpened({ source: "highlight_related" }) - setSearchPrefill(query) - setIsSearchOpen(true) - }, []) + const handleHighlightsShowRelated = useCallback( + (query: string) => { + analytics.searchOpened({ source: "highlight_related" }) + setSearchPrefill(query) + setIsSearchOpen(true) + }, + [setSearchPrefill, setIsSearchOpen], + ) + const chatOpen = isChatOpen !== null ? isChatOpen : !isMobile const isGraphMode = viewMode === "graph" && !isMobile return ( @@ -227,11 +282,11 @@ export default function NewPage() {
{ analytics.addDocumentModalOpened() - setIsAddDocumentOpen(true) + setAddDoc("note") }} onOpenMCP={() => { analytics.mcpModalOpened() - setIsMCPModalOpen(true) + setIsMCPOpen(true) }} onOpenChat={() => setIsChatOpen(true)} onOpenSearch={() => { @@ -240,7 +295,7 @@ export default function NewPage() { }} />
{viewMode === "graph" && !isMobile ? (
- +
) : (
setIsChatOpen(open)} queuedMessage={queuedChatSeed} onConsumeQueuedMessage={() => setQueuedChatSeed(null)} emptyStateSuggestions={highlightsData?.questions} @@ -286,8 +341,8 @@ export default function NewPage() { {isMobile && ( setIsChatOpen(open)} queuedMessage={queuedChatSeed} onConsumeQueuedMessage={() => setQueuedChatSeed(null)} emptyStateSuggestions={highlightsData?.questions} @@ -295,12 +350,13 @@ export default function NewPage() { )} setIsAddDocumentOpen(false)} + isOpen={addDoc !== null} + onClose={() => setAddDoc(null)} + defaultTab={addDoc ?? undefined} /> setIsMCPModalOpen(false)} + isOpen={isMCPOpen} + onClose={() => setIsMCPOpen(false)} /> { + analytics.addDocumentModalOpened() + setAddDoc("note") + }} + onOpenMCP={() => { + analytics.mcpModalOpened() + setIsMCPOpen(true) + }} initialSearch={searchPrefill} /> setIsDocumentModalOpen(false)} + isOpen={docId !== null} + onClose={() => setDocId(null)} /> setIsFullScreenNoteOpen(false)} + isOpen={isFullscreen} + onClose={() => setIsFullscreen(false)} initialContent={fullscreenInitialContent} onSave={handleFullScreenSave} isSaving={noteMutation.isPending} diff --git a/apps/web/app/new/settings/page.tsx b/apps/web/app/(app)/settings/page.tsx similarity index 99% rename from apps/web/app/new/settings/page.tsx rename to apps/web/app/(app)/settings/page.tsx index b2e35b7f7..35f4fb609 100644 --- a/apps/web/app/new/settings/page.tsx +++ b/apps/web/app/(app)/settings/page.tsx @@ -182,7 +182,7 @@ export default function SettingsPage() {
- ) - })} - - {children} -
- - - ) -} diff --git a/apps/web/app/(navigation)/settings/page.tsx b/apps/web/app/(navigation)/settings/page.tsx deleted file mode 100644 index 1f806e716..000000000 --- a/apps/web/app/(navigation)/settings/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -"use client" -import { ProfileView } from "@/components/views/profile" -export default function ProfilePage() { - return ( -
-

- Profile Settings -

- -
- ) -} diff --git a/apps/web/app/(navigation)/settings/support/page.tsx b/apps/web/app/(navigation)/settings/support/page.tsx deleted file mode 100644 index 30e96c37a..000000000 --- a/apps/web/app/(navigation)/settings/support/page.tsx +++ /dev/null @@ -1,123 +0,0 @@ -"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 unlimited 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! -

- - -
-
-
- ) -} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 88ce99882..dfc12a0c5 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -9,12 +9,9 @@ import { QueryProvider } from "../components/query-client" import { AutumnProvider } from "autumn-js/react" import { Suspense } from "react" import { Toaster } from "@ui/components/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 font = Space_Grotesk({ subsets: ["latin"], variable: "--font-sans", @@ -60,18 +57,14 @@ export default function RootLayout({ > - - - - - - {children} - - - - - - + + + + {children} + + + + diff --git a/apps/web/app/new/layout.tsx b/apps/web/app/new/layout.tsx deleted file mode 100644 index ae9990e82..000000000 --- a/apps/web/app/new/layout.tsx +++ /dev/null @@ -1,28 +0,0 @@ -"use client" - -import { useEffect } from "react" -import { useFeatureFlagEnabled } from "posthog-js/react" -import { useRouter } from "next/navigation" -import { MobileBanner } from "@/components/new/mobile-banner" - -export default function NewLayout({ children }: { children: React.ReactNode }) { - const router = useRouter() - const flagEnabled = true - - useEffect(() => { - if (!flagEnabled) { - router.push("/") - } - }, [flagEnabled, router]) - - if (!flagEnabled) { - return null - } - - return ( - <> - - {children} - - ) -} diff --git a/apps/web/app/onboarding/animated-text.tsx b/apps/web/app/onboarding/animated-text.tsx deleted file mode 100644 index c32abb10d..000000000 --- a/apps/web/app/onboarding/animated-text.tsx +++ /dev/null @@ -1,62 +0,0 @@ -"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, - }, - }, - }, - } - - return ( - - {children} - - ) -} diff --git a/apps/web/app/onboarding/bio-form.tsx b/apps/web/app/onboarding/bio-form.tsx deleted file mode 100644 index b90825351..000000000 --- a/apps/web/app/onboarding/bio-form.tsx +++ /dev/null @@ -1,98 +0,0 @@ -"use client" - -import { Textarea } from "@ui/components/textarea" -import { useOnboarding } from "./onboarding-context" -import { useState } from "react" -import { Button } from "@ui/components/button" -import { AnimatePresence, motion } from "motion/react" -import { NavMenu } from "./nav-menu" -import { $fetch } from "@lib/api" - -export function BioForm() { - const [bio, setBio] = useState("") - const { totalSteps, nextStep, getStepNumberFor } = useOnboarding() - - function handleNext() { - const trimmed = bio.trim() - if (!trimmed) { - nextStep() - return - } - - nextStep() - void $fetch("@post/documents", { - body: { - content: trimmed, - containerTags: ["sm_project_default"], - metadata: { sm_source: "consumer" }, - }, - }).catch((error) => { - console.error("Failed to save onboarding bio memory:", error) - }) - } - return ( -
-
-
- - {bio ? ( - - - - ) : ( - - - - )} - -
- -

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

-
-

- Tell Supermemory about yourself -

-

- share with Supermemory what you do, who you are, and what you're - interested in -

-
-