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
2 changes: 1 addition & 1 deletion apps/mcp/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export class SupermemoryClient {
response.searchResults = {
results: (result.searchResults.results as SDKResult[]).map((r) => ({
id: r.id,
memory: limitByChars(r.content || r.context || ""),
memory: limitByChars(r.content || r.memory || r.context || ""),
similarity: r.similarity,
title: r.title,
content: r.content,
Expand Down
12 changes: 12 additions & 0 deletions apps/web/app/(navigation)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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
Expand Down
61 changes: 61 additions & 0 deletions apps/web/app/api/og/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,54 @@ 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

Expand Down Expand Up @@ -110,6 +158,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 { result, error } = await ogs({
url: trimmedUrl,
timeout: 8000,
Expand Down
128 changes: 125 additions & 3 deletions apps/web/app/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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)
Expand All @@ -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">
Expand All @@ -69,24 +160,44 @@ 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">
<AnimatePresence mode="popLayout">
<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
Expand All @@ -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>
)
Expand Down
5 changes: 3 additions & 2 deletions apps/web/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { ScrollArea } from "@ui/components/scroll-area"
import { formatDistanceToNow } from "date-fns"
import { cn } from "@lib/utils"
import { useEffect, useMemo, useState } from "react"
import { generateId } from "@lib/generate-id"

export function Header({ onAddMemory }: { onAddMemory?: () => void }) {
const { user } = useAuth()
Expand Down Expand Up @@ -98,7 +99,7 @@ export function Header({ onAddMemory }: { onAddMemory?: () => void }) {

function handleNewChat() {
analytics.newChatStarted()
const newId = crypto.randomUUID()
const newId = generateId()
setCurrentChatId(newId)
router.push(`/chat/${newId}`)
setIsDialogOpen(false)
Expand Down Expand Up @@ -129,7 +130,7 @@ export function Header({ onAddMemory }: { onAddMemory?: () => void }) {
>
{getCurrentChat()?.title && pathname.includes("/chat") ? (
<div className="flex items-center gap-2 md:gap-4 min-w-0 max-w-[200px] md:max-w-md">
<Logo className="h-6 block text-foreground flex-shrink-0" />
<Logo className="h-6 block text-foreground shrink-0" />
<span className="truncate text-sm md:text-base">
{getCurrentChat()?.title}
</span>
Expand Down
Loading
Loading