Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apps/web/app/(app)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use client"

import { MobileBanner } from "@/components/new/mobile-banner"

export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
<>
<MobileBanner />
{children}
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
)
Expand All @@ -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),
)
Expand All @@ -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],
)
Expand Down
176 changes: 120 additions & 56 deletions apps/web/app/new/page.tsx → apps/web/app/(app)/page.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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<typeof DocumentsWithMemoriesResponseSchema>
type DocumentWithMemories = DocumentsResponse["documents"][0]
Expand All @@ -36,25 +46,64 @@ 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<DocumentWithMemories | null>(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<string | null>(null)
const [searchPrefill, setSearchPrefill] = useState("")
const [selectedDocument, setSelectedDocument] =
useState<DocumentWithMemories | null>(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=<id> (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 || "")

const { noteMutation } = useDocumentMutations({
onClose: () => {
resetDraft()
setIsFullScreenNoteOpen(false)
setIsFullscreen(false)
},
})

Expand All @@ -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) {
Expand Down Expand Up @@ -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",
Expand All @@ -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) => {
Expand Down Expand Up @@ -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 (
Expand All @@ -227,11 +282,11 @@ export default function NewPage() {
<Header
onAddMemory={() => {
analytics.addDocumentModalOpened()
setIsAddDocumentOpen(true)
setAddDoc("note")
}}
onOpenMCP={() => {
analytics.mcpModalOpened()
setIsMCPModalOpen(true)
setIsMCPOpen(true)
}}
onOpenChat={() => setIsChatOpen(true)}
onOpenSearch={() => {
Expand All @@ -240,7 +295,7 @@ export default function NewPage() {
}}
/>
<main
key={`main-container-${isChatOpen}-${viewMode}`}
key={`main-container-${chatOpen}-${viewMode}`}
className={cn(
"z-10 relative",
isGraphMode && "h-[calc(100vh-86px)] overflow-hidden",
Expand All @@ -249,12 +304,12 @@ export default function NewPage() {
<div className={cn("relative z-10 flex flex-col md:flex-row h-full")}>
{viewMode === "graph" && !isMobile ? (
<div className="flex-1">
<GraphLayoutView isChatOpen={isChatOpen} />
<GraphLayoutView isChatOpen={chatOpen} />
</div>
) : (
<div className="flex-1 p-4 md:p-6 md:pr-0 pt-2!">
<MemoriesGrid
isChatOpen={isChatOpen}
isChatOpen={chatOpen}
onOpenDocument={handleOpenDocument}
quickNoteProps={{
onSave: handleQuickNoteSave,
Expand All @@ -273,8 +328,8 @@ export default function NewPage() {
<div className="hidden md:block md:sticky md:top-0 md:h-screen">
<AnimatePresence mode="popLayout">
<ChatSidebar
isChatOpen={isChatOpen}
setIsChatOpen={setIsChatOpen}
isChatOpen={chatOpen}
setIsChatOpen={(open) => setIsChatOpen(open)}
queuedMessage={queuedChatSeed}
onConsumeQueuedMessage={() => setQueuedChatSeed(null)}
emptyStateSuggestions={highlightsData?.questions}
Expand All @@ -286,21 +341,22 @@ export default function NewPage() {

{isMobile && (
<ChatSidebar
isChatOpen={isChatOpen}
setIsChatOpen={setIsChatOpen}
isChatOpen={chatOpen}
setIsChatOpen={(open) => setIsChatOpen(open)}
queuedMessage={queuedChatSeed}
onConsumeQueuedMessage={() => setQueuedChatSeed(null)}
emptyStateSuggestions={highlightsData?.questions}
/>
)}

<AddDocumentModal
isOpen={isAddDocumentOpen}
onClose={() => setIsAddDocumentOpen(false)}
isOpen={addDoc !== null}
onClose={() => setAddDoc(null)}
defaultTab={addDoc ?? undefined}
/>
<MCPModal
isOpen={isMCPModalOpen}
onClose={() => setIsMCPModalOpen(false)}
isOpen={isMCPOpen}
onClose={() => setIsMCPOpen(false)}
/>
<DocumentsCommandPalette
open={isSearchOpen}
Expand All @@ -310,16 +366,24 @@ export default function NewPage() {
}}
projectId={selectedProject}
onOpenDocument={handleOpenDocument}
onAddMemory={() => {
analytics.addDocumentModalOpened()
setAddDoc("note")
}}
onOpenMCP={() => {
analytics.mcpModalOpened()
setIsMCPOpen(true)
}}
initialSearch={searchPrefill}
/>
<DocumentModal
document={selectedDocument}
isOpen={isDocumentModalOpen}
onClose={() => setIsDocumentModalOpen(false)}
isOpen={docId !== null}
onClose={() => setDocId(null)}
/>
<FullscreenNoteModal
isOpen={isFullScreenNoteOpen}
onClose={() => setIsFullScreenNoteOpen(false)}
isOpen={isFullscreen}
onClose={() => setIsFullscreen(false)}
initialContent={fullscreenInitialContent}
onSave={handleFullScreenSave}
isSaving={noteMutation.isPending}
Expand Down
Loading
Loading