aboutsummaryrefslogtreecommitdiff
path: root/apps/web/app
diff options
context:
space:
mode:
authorDhravya Shah <[email protected]>2026-01-23 17:42:47 -0700
committerDhravya Shah <[email protected]>2026-01-23 17:42:47 -0700
commit4ca0f593a5d89695e101569f09debda5617c0ec6 (patch)
tree60517a8e898965cf8120cc01c56f69baaff0d06e /apps/web/app
parentextract metadata ourselves (diff)
parentfix: cf build (#700) (diff)
downloadarchived-supermemory-4ca0f593a5d89695e101569f09debda5617c0ec6.tar.xz
archived-supermemory-4ca0f593a5d89695e101569f09debda5617c0ec6.zip
fix: merge conflicts
Diffstat (limited to 'apps/web/app')
-rw-r--r--apps/web/app/(navigation)/layout.tsx12
-rw-r--r--apps/web/app/api/og/route.ts77
-rw-r--r--apps/web/app/new/page.tsx128
3 files changed, 214 insertions, 3 deletions
diff --git a/apps/web/app/(navigation)/layout.tsx b/apps/web/app/(navigation)/layout.tsx
index 7b7628bb..68a67a93 100644
--- a/apps/web/app/(navigation)/layout.tsx
+++ b/apps/web/app/(navigation)/layout.tsx
@@ -3,6 +3,8 @@
import { GraphDialog } from "@/components/graph-dialog"
import { Header } from "@/components/header"
import { AddMemoryView } from "@/components/views/add-memory"
+import { usePathname, useRouter } from "next/navigation"
+import { useFeatureFlagEnabled } from "posthog-js/react"
import { useEffect, useState } from "react"
export default function NavigationLayout({
@@ -11,6 +13,16 @@ export default function NavigationLayout({
children: React.ReactNode
}) {
const [showAddMemoryView, setShowAddMemoryView] = useState(false)
+ const pathname = usePathname()
+ const router = useRouter()
+ const flagEnabled = useFeatureFlagEnabled("nova-alpha-access")
+
+ useEffect(() => {
+ if (flagEnabled && !pathname.includes("/new")) {
+ router.replace("/new")
+ }
+ }, [flagEnabled, router, pathname])
+
useEffect(() => {
const handleKeydown = (event: KeyboardEvent) => {
const target = event.target as HTMLElement
diff --git a/apps/web/app/api/og/route.ts b/apps/web/app/api/og/route.ts
index 97f024a5..4c61ebe5 100644
--- a/apps/web/app/api/og/route.ts
+++ b/apps/web/app/api/og/route.ts
@@ -37,6 +37,70 @@ function isPrivateHost(hostname: string): boolean {
return privateIpPatterns.some((pattern) => pattern.test(hostname))
}
+// File extensions that are not HTML and can't be scraped for OG data
+const NON_HTML_EXTENSIONS = [
+ ".pdf",
+ ".doc",
+ ".docx",
+ ".xls",
+ ".xlsx",
+ ".ppt",
+ ".pptx",
+ ".zip",
+ ".rar",
+ ".7z",
+ ".tar",
+ ".gz",
+ ".mp3",
+ ".mp4",
+ ".avi",
+ ".mov",
+ ".wmv",
+ ".flv",
+ ".webm",
+ ".wav",
+ ".ogg",
+ ".jpg",
+ ".jpeg",
+ ".png",
+ ".gif",
+ ".webp",
+ ".svg",
+ ".ico",
+ ".bmp",
+ ".tiff",
+ ".exe",
+ ".dmg",
+ ".iso",
+ ".bin",
+]
+
+function isNonHtmlUrl(url: string): boolean {
+ try {
+ const urlObj = new URL(url)
+ const pathname = urlObj.pathname.toLowerCase()
+ return NON_HTML_EXTENSIONS.some((ext) => pathname.endsWith(ext))
+ } catch {
+ return false
+ }
+}
+
+function extractImageUrl(image: unknown): string | undefined {
+ if (!image) return undefined
+
+ if (typeof image === "string") {
+ return image
+ }
+
+ if (Array.isArray(image) && image.length > 0) {
+ const first = image[0]
+ if (first && typeof first === "object" && "url" in first) {
+ return String(first.url)
+ }
+ }
+ return ""
+}
+
function extractMetaTag(html: string, patterns: RegExp[]): string {
for (const pattern of patterns) {
const match = html.match(pattern)
@@ -101,6 +165,19 @@ export async function GET(request: Request) {
)
}
+ // Skip OG scraping for non-HTML files (PDFs, images, etc.)
+ if (isNonHtmlUrl(trimmedUrl)) {
+ return Response.json(
+ { title: "", description: "" },
+ {
+ headers: {
+ "Cache-Control":
+ "public, s-maxage=3600, stale-while-revalidate=86400",
+ },
+ },
+ )
+ }
+
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 8000)
diff --git a/apps/web/app/new/page.tsx b/apps/web/app/new/page.tsx
index afb077b3..31fab182 100644
--- a/apps/web/app/new/page.tsx
+++ b/apps/web/app/new/page.tsx
@@ -9,12 +9,17 @@ import { AddDocumentModal } from "@/components/new/add-document"
import { MCPModal } from "@/components/new/mcp-modal"
import { DocumentModal } from "@/components/new/document-modal"
import { DocumentsCommandPalette } from "@/components/new/documents-command-palette"
+import { FullscreenNoteModal } from "@/components/new/fullscreen-note-modal"
+import type { HighlightItem } from "@/components/new/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 { useProject } from "@/stores"
+import { useQuickNoteDraftReset } from "@/stores/quick-note-draft"
import { analytics } from "@/lib/analytics"
+import { useDocumentMutations } from "@/hooks/use-document-mutations"
+import { useQuery } from "@tanstack/react-query"
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
import type { z } from "zod"
@@ -31,6 +36,56 @@ export default function NewPage() {
useState<DocumentWithMemories | null>(null)
const [isDocumentModalOpen, setIsDocumentModalOpen] = useState(false)
+ const [isFullScreenNoteOpen, setIsFullScreenNoteOpen] = useState(false)
+ const [fullscreenInitialContent, setFullscreenInitialContent] = useState("")
+ const [queuedChatSeed, setQueuedChatSeed] = useState<string | null>(null)
+ const [searchPrefill, setSearchPrefill] = useState("")
+
+ const resetDraft = useQuickNoteDraftReset(selectedProject)
+
+ const { noteMutation } = useDocumentMutations({
+ onClose: () => {
+ resetDraft()
+ setIsFullScreenNoteOpen(false)
+ },
+ })
+
+ // Fetch space highlights (highlights + suggested questions)
+ type SpaceHighlightsResponse = {
+ highlights: HighlightItem[]
+ questions: string[]
+ generatedAt: string
+ }
+ const { data: highlightsData, isLoading: isLoadingHighlights } =
+ useQuery<SpaceHighlightsResponse>({
+ queryKey: ["space-highlights", selectedProject],
+ queryFn: async (): Promise<SpaceHighlightsResponse> => {
+ 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: selectedProject || "sm_project_default",
+ highlightsCount: 3,
+ questionsCount: 4,
+ includeHighlights: true,
+ includeQuestions: true,
+ }),
+ },
+ )
+
+ if (!response.ok) {
+ throw new Error("Failed to fetch space highlights")
+ }
+
+ return response.json()
+ },
+ staleTime: 4 * 60 * 60 * 1000, // 4 hours (matches backend cache)
+ refetchOnWindowFocus: false,
+ })
+
useHotkeys("c", () => {
analytics.addDocumentModalOpened()
setIsAddDocumentOpen(true)
@@ -46,6 +101,42 @@ export default function NewPage() {
setIsDocumentModalOpen(true)
}, [])
+ const handleQuickNoteSave = useCallback(
+ (content: string) => {
+ if (content.trim()) {
+ noteMutation.mutate({ content, project: selectedProject })
+ }
+ },
+ [selectedProject, noteMutation],
+ )
+
+ const handleFullScreenSave = useCallback(
+ (content: string) => {
+ if (content.trim()) {
+ noteMutation.mutate({ content, project: selectedProject })
+ }
+ },
+ [selectedProject, noteMutation],
+ )
+
+ const handleMaximize = useCallback(
+ (content: string) => {
+ setFullscreenInitialContent(content)
+ setIsFullScreenNoteOpen(true)
+ },
+ [],
+ )
+
+ const handleHighlightsChat = useCallback((seed: string) => {
+ setQueuedChatSeed(seed)
+ setIsChatOpen(true)
+ }, [])
+
+ const handleHighlightsShowRelated = useCallback((query: string) => {
+ setSearchPrefill(query)
+ setIsSearchOpen(true)
+ }, [])
+
return (
<HotkeysProvider>
<div className="bg-black min-h-screen">
@@ -69,10 +160,21 @@ export default function NewPage() {
key={`main-container-${isChatOpen}`}
className="z-10 flex flex-col md:flex-row relative"
>
- <div className="flex-1 p-4 md:p-6 md:pr-0">
+ <div className="flex-1 p-4 md:p-6 md:pr-0 pt-2!">
<MemoriesGrid
isChatOpen={isChatOpen}
onOpenDocument={handleOpenDocument}
+ quickNoteProps={{
+ onSave: handleQuickNoteSave,
+ onMaximize: handleMaximize,
+ isSaving: noteMutation.isPending,
+ }}
+ highlightsProps={{
+ items: highlightsData?.highlights || [],
+ onChat: handleHighlightsChat,
+ onShowRelated: handleHighlightsShowRelated,
+ isLoading: isLoadingHighlights,
+ }}
/>
</div>
<div className="hidden md:block md:sticky md:top-0 md:h-screen">
@@ -80,13 +182,22 @@ export default function NewPage() {
<ChatSidebar
isChatOpen={isChatOpen}
setIsChatOpen={setIsChatOpen}
+ queuedMessage={queuedChatSeed}
+ onConsumeQueuedMessage={() => setQueuedChatSeed(null)}
+ emptyStateSuggestions={highlightsData?.questions}
/>
</AnimatePresence>
</div>
</main>
{isMobile && (
- <ChatSidebar isChatOpen={isChatOpen} setIsChatOpen={setIsChatOpen} />
+ <ChatSidebar
+ isChatOpen={isChatOpen}
+ setIsChatOpen={setIsChatOpen}
+ queuedMessage={queuedChatSeed}
+ onConsumeQueuedMessage={() => setQueuedChatSeed(null)}
+ emptyStateSuggestions={highlightsData?.questions}
+ />
)}
<AddDocumentModal
@@ -99,15 +210,26 @@ export default function NewPage() {
/>
<DocumentsCommandPalette
open={isSearchOpen}
- onOpenChange={setIsSearchOpen}
+ onOpenChange={(open) => {
+ setIsSearchOpen(open)
+ if (!open) setSearchPrefill("")
+ }}
projectId={selectedProject}
onOpenDocument={handleOpenDocument}
+ initialSearch={searchPrefill}
/>
<DocumentModal
document={selectedDocument}
isOpen={isDocumentModalOpen}
onClose={() => setIsDocumentModalOpen(false)}
/>
+ <FullscreenNoteModal
+ isOpen={isFullScreenNoteOpen}
+ onClose={() => setIsFullScreenNoteOpen(false)}
+ initialContent={fullscreenInitialContent}
+ onSave={handleFullScreenSave}
+ isSaving={noteMutation.isPending}
+ />
</div>
</HotkeysProvider>
)