"use client" import { useState, useCallback, useEffect } from "react" import { useQueryState } from "nuqs" import { Header } from "@/components/header" import { ChatSidebar } from "@/components/chat" import { MemoriesGrid } from "@/components/memories-grid" import { GraphLayoutView } from "@/components/graph-layout-view" import { IntegrationsView } from "@/components/integrations-view" import { AnimatedGradientBackground } from "@/components/animated-gradient-background" import { AddDocumentModal } from "@/components/add-document" import { DocumentModal } from "@/components/document-modal" import { DocumentsCommandPalette } from "@/components/documents-command-palette" import { FullscreenNoteModal } from "@/components/fullscreen-note-modal" import type { HighlightItem } from "@/components/highlights-card" import { HotkeysProvider } from "react-hotkeys-hook" import { useHotkeys } from "react-hotkeys-hook" import { AnimatePresence } from "motion/react" import { useIsMobile } from "@hooks/use-mobile" import { useAuth } from "@lib/auth-context" import { useProject } from "@/stores" import { useContainerTags } from "@/hooks/use-container-tags" import { DEFAULT_PROJECT_ID } from "@lib/constants" import { useQuickNoteDraftReset, useQuickNoteDraft, } from "@/stores/quick-note-draft" import { analytics } from "@/lib/analytics" import { useDocumentMutations } from "@/hooks/use-document-mutations" 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 { ErrorBoundary } from "@/components/error-boundary" import { cn } from "@lib/utils" import { addDocumentParam, searchParam, qParam, docParam, fullscreenParam, chatParam, integrationParam, pluginsPanelParam, type IntegrationParamValue, } from "@/lib/search-params" type DocumentsResponse = z.infer type DocumentWithMemories = DocumentsResponse["documents"][0] function ViewErrorFallback() { return (

Something went wrong.{" "}

) } export default function NewPage() { const isMobile = useIsMobile() const { user, session } = useAuth() const { selectedProject, isNovaSpaces, novaContainerTags, selectedProjects, setSelectedProjects, } = useProject() const selectedProjectTag = selectedProjects[0] const isNovaContext = isNovaSpaces || (selectedProjectTag !== undefined && novaContainerTags.includes(selectedProjectTag)) const { allProjects } = useContainerTags() const emptyStateSpaceName = !isNovaSpaces && selectedProjectTag ? selectedProjectTag === DEFAULT_PROJECT_ID ? "My Space" : (allProjects.find((p) => p.containerTag === selectedProjectTag) ?.name ?? selectedProjectTag) : undefined const handleSwitchToAllSpacesFromEmptyState = useCallback(() => { analytics.spaceSwitched({ space_id: "nova_spaces" }) setSelectedProjects([]) }, [setSelectedProjects]) const { viewMode, setViewMode } = useViewMode() const queryClient = useQueryClient() // Chrome extension auth: send session token via postMessage so the content script can store it useEffect(() => { const url = new URL(window.location.href) if (!url.searchParams.get("extension-auth-success")) return const sessionToken = session?.token const userData = { email: user?.email, name: user?.name, userId: user?.id } if (sessionToken && userData.email) { window.postMessage( { token: encodeURIComponent(sessionToken), userData }, window.location.origin, ) url.searchParams.delete("extension-auth-success") window.history.replaceState({}, "", url.toString()) } }, [user, session]) // URL-driven modal states const [addDoc, setAddDoc] = useQueryState("add", addDocumentParam) 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) const [integrationFromUrl, setIntegration] = useQueryState( "integration", integrationParam, ) const [pluginsPanelFromUrl] = useQueryState("plugins", pluginsPanelParam) useEffect(() => { if (integrationFromUrl || pluginsPanelFromUrl === true) { void setViewMode("integrations") } }, [integrationFromUrl, pluginsPanelFromUrl, setViewMode]) // Ephemeral local state (not worth URL-encoding) const [fullscreenInitialContent, setFullscreenInitialContent] = useState("") const [queuedChatSeed, setQueuedChatSeed] = useState(null) 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 || "") const { noteMutation, bulkDeleteMutation } = useDocumentMutations({ onClose: () => { resetDraft() setIsFullscreen(false) }, }) const [selectedDocumentIds, setSelectedDocumentIds] = useState>( new Set(), ) const [isSelectionMode, setIsSelectionMode] = useState(false) const handleToggleSelection = useCallback((documentId: string) => { setSelectedDocumentIds((prev) => { const next = new Set(prev) if (next.has(documentId)) { next.delete(documentId) } else { next.add(documentId) } return next }) }, []) const handleClearSelection = useCallback(() => { setSelectedDocumentIds(new Set()) setIsSelectionMode(false) }, []) const handleEnterSelectionMode = useCallback(() => { setIsSelectionMode(true) }, []) const handleSelectAllVisible = useCallback((visibleIds: string[]) => { setSelectedDocumentIds((prev) => { const next = new Set(prev) for (const id of visibleIds) { next.add(id) } return next }) }, []) const handleBulkDelete = useCallback(() => { const ids = Array.from(selectedDocumentIds) if (ids.length === 0) return bulkDeleteMutation.mutate( { documentIds: ids }, { onSuccess: () => { setSelectedDocumentIds(new Set()) setIsSelectionMode(false) if (selectedDocument && ids.includes(selectedDocument.id ?? "")) { setDocId(null) } }, }, ) }, [selectedDocumentIds, bulkDeleteMutation, selectedDocument, setDocId]) type SpaceHighlightsResponse = { highlights: HighlightItem[] questions: string[] generatedAt: string } const HIGHLIGHTS_CACHE_NAME = "space-highlights-v1" const HIGHLIGHTS_MAX_AGE = 4 * 60 * 60 * 1000 // 4 hours const { data: highlightsData, isLoading: isLoadingHighlights } = useQuery({ queryKey: ["space-highlights", selectedProject], queryFn: async (): Promise => { const spaceId = selectedProject || "sm_project_default" const cacheKey = `${process.env.NEXT_PUBLIC_BACKEND_URL}/v3/space-highlights?spaceId=${spaceId}` const cache = await caches.open(HIGHLIGHTS_CACHE_NAME) const cached = await cache.match(cacheKey) if (cached) { const age = Date.now() - Number(cached.headers.get("x-cached-at") || 0) if (age < HIGHLIGHTS_MAX_AGE) { return cached.json() } } const response = await fetch( `${process.env.NEXT_PUBLIC_BACKEND_URL}/v3/space-highlights`, { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ spaceId, highlightsCount: 3, questionsCount: 4, includeHighlights: true, includeQuestions: true, }), }, ) if (!response.ok) { throw new Error("Failed to fetch space highlights") } const data = await response.json() const cacheResponse = new Response(JSON.stringify(data), { headers: { "Content-Type": "application/json", "x-cached-at": String(Date.now()), }, }) await cache.put(cacheKey, cacheResponse) return data }, staleTime: HIGHLIGHTS_MAX_AGE, refetchOnWindowFocus: false, }) useHotkeys("c", () => { analytics.addDocumentModalOpened() setAddDoc("note") }) useHotkeys("mod+k", (e) => { e.preventDefault() analytics.searchOpened({ source: "hotkey" }) setIsSearchOpen(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) => { if (content.trim()) { const hadPreviousContent = quickNoteDraft.trim().length > 0 noteMutation.mutate( { content, project: selectedProject }, { onSuccess: () => { if (hadPreviousContent) { analytics.quickNoteEdited() } else { analytics.quickNoteCreated() } }, }, ) } }, [selectedProject, noteMutation, quickNoteDraft], ) const handleFullScreenSave = useCallback( (content: string) => { if (content.trim()) { const hadInitialContent = fullscreenInitialContent.trim().length > 0 noteMutation.mutate( { content, project: selectedProject }, { onSuccess: () => { if (hadInitialContent) { analytics.quickNoteEdited() } else { analytics.quickNoteCreated() } }, }, ) } }, [selectedProject, noteMutation, fullscreenInitialContent], ) const handleMaximize = useCallback( (content: string) => { analytics.fullscreenNoteModalOpened() setFullscreenInitialContent(content) setIsFullscreen(true) }, [setIsFullscreen], ) const handleHighlightsChat = useCallback( (seed: string) => { setQueuedChatSeed(seed) setIsChatOpen(true) }, [setIsChatOpen], ) const handleHighlightsShowRelated = useCallback( (query: string) => { analytics.searchOpened({ source: "highlight_related" }) setSearchPrefill(query) setIsSearchOpen(true) }, [setSearchPrefill, setIsSearchOpen], ) const handleOpenIntegrations = useCallback( (integration?: IntegrationParamValue) => { setViewMode("integrations") if (integration) { setIntegration(integration) } else { setIntegration(null) } }, [setViewMode, setIntegration], ) const handleAddMemory = useCallback( (tab: "note" | "link") => { analytics.addDocumentModalOpened() setAddDoc(tab) }, [setAddDoc], ) const chatOpen = isChatOpen !== null ? isChatOpen : !isMobile const isGraphMode = viewMode === "graph" && !isMobile return (
{isGraphMode && (
)}
{ analytics.addDocumentModalOpened() setAddDoc("note") }} onOpenChat={() => setIsChatOpen(true)} onOpenSearch={() => { analytics.searchOpened({ source: "header" }) setIsSearchOpen(true) }} />
}> {viewMode === "integrations" ? (
) : viewMode === "graph" && !isMobile ? (
) : (
)}
setIsChatOpen(open)} queuedMessage={queuedChatSeed} onConsumeQueuedMessage={() => setQueuedChatSeed(null)} emptyStateSuggestions={highlightsData?.questions} />
{isMobile && ( setIsChatOpen(open)} queuedMessage={queuedChatSeed} onConsumeQueuedMessage={() => setQueuedChatSeed(null)} emptyStateSuggestions={highlightsData?.questions} /> )} setAddDoc(null)} /> { setIsSearchOpen(open) if (!open) setSearchPrefill("") }} projectId={selectedProject} novaContainerTags={isNovaSpaces ? novaContainerTags : undefined} onOpenDocument={handleOpenDocument} onAddMemory={() => { analytics.addDocumentModalOpened() setAddDoc("note") }} onOpenIntegrations={() => setViewMode("integrations")} initialSearch={searchPrefill} /> setDocId(null)} /> setIsFullscreen(false)} initialContent={fullscreenInitialContent} onSave={handleFullScreenSave} isSaving={noteMutation.isPending} />
) }