feat: add global file drag-and-drop to open Add Memory modal

- Dropping files anywhere on the main page opens the modal with files pre-loaded
- Full-screen overlay with 'Drop files to add as memories' appears during drag
- Unsupported file types show error toast, accepted files are queued in file tab
- Settings page excluded (separate page component, not affected)
- initialFiles prop threaded through AddDocumentModal → AddDocument
- Consumed-once ref prevents re-seeding on re-renders
This commit is contained in:
MaheshtheDev 2026-05-12 05:40:08 +00:00
parent 24aca7ae72
commit 56118cacec
2 changed files with 112 additions and 2 deletions

View file

@ -24,6 +24,7 @@ import { RaycastDetail } from "@/components/integrations/raycast-detail"
import { PluginsDetail } from "@/components/integrations/plugins-detail"
import { AnimatedGradientBackground } from "@/components/animated-gradient-background"
import { AddDocumentModal } from "@/components/add-document"
import { isAcceptedFile } from "@/components/add-document/file"
import { DocumentModal } from "@/components/document-modal"
import { DocumentsCommandPalette } from "@/components/documents-command-palette"
import { FullscreenNoteModal } from "@/components/fullscreen-note-modal"
@ -49,6 +50,7 @@ import type { z } from "zod"
import { useViewMode } from "@/lib/view-mode-context"
import type { MemoryOfDay } from "@/components/dashboard-view"
import { ErrorBoundary } from "@/components/error-boundary"
import { FileTextIcon } from "lucide-react"
import { cn } from "@lib/utils"
import {
addDocumentParam,
@ -381,6 +383,64 @@ export default function NewPage() {
enabled: !!user,
})
// Global file drag-and-drop: dropping files anywhere on the page opens the Add Memory modal
const [globalDroppedFiles, setGlobalDroppedFiles] = useState<File[]>([])
const [isGlobalDragging, setIsGlobalDragging] = useState(false)
const globalDragCounter = useRef(0)
const handleGlobalDragEnter = useCallback((e: React.DragEvent) => {
e.preventDefault()
globalDragCounter.current++
if (e.dataTransfer.types.includes("Files")) {
setIsGlobalDragging(true)
}
}, [])
const handleGlobalDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault()
globalDragCounter.current--
if (globalDragCounter.current === 0) {
setIsGlobalDragging(false)
}
}, [])
const handleGlobalDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault()
}, [])
const handleGlobalDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault()
globalDragCounter.current = 0
setIsGlobalDragging(false)
if (addDoc !== null) return // Modal already open, let it handle its own drops
const files = Array.from(e.dataTransfer.files)
const accepted = files.filter(isAcceptedFile)
if (accepted.length === 0) {
if (files.length > 0) {
toast.error(
files.length === 1
? "This file type is not supported"
: `${files.length} files are not supported`,
)
}
return
}
const rejected = files.length - accepted.length
if (rejected > 0) {
toast.error(
rejected === 1
? "One file type is not supported"
: `${rejected} files are not supported`,
)
}
setGlobalDroppedFiles(accepted)
analytics.addDocumentModalOpened()
setAddDoc("file")
},
[addDoc, setAddDoc],
)
useHotkeys("c", () => {
analytics.addDocumentModalOpened()
setAddDoc("note")
@ -540,7 +600,24 @@ export default function NewPage() {
"relative flex min-h-dvh flex-col bg-[#05080D]",
isGraphMode && "h-dvh overflow-hidden",
)}
onDragEnter={handleGlobalDragEnter}
onDragLeave={handleGlobalDragLeave}
onDragOver={handleGlobalDragOver}
onDrop={handleGlobalDrop}
>
{isGlobalDragging && addDoc === null && (
<div className="pointer-events-none fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div className="flex flex-col items-center gap-3 rounded-2xl border-2 border-dashed border-[#4BA0FA] bg-[#4BA0FA]/10 px-12 py-10">
<FileTextIcon className="size-10 text-[#4BA0FA]" />
<p className="text-lg font-medium text-white">
Drop files to add as memories
</p>
<p className="text-sm text-[#737373]">
PDFs, images, documents, and more
</p>
</div>
</div>
)}
{showNovaBackdrop && (
<>
<AnimatedGradientBackground
@ -719,7 +796,11 @@ export default function NewPage() {
<AddDocumentModal
isOpen={addDoc !== null}
onClose={() => setAddDoc(null)}
onClose={() => {
setAddDoc(null)
setGlobalDroppedFiles([])
}}
initialFiles={globalDroppedFiles}
/>
<DocumentsCommandPalette
open={isSearchOpen}

View file

@ -29,9 +29,14 @@ type TabType = "note" | "link" | "file" | "connect"
interface AddDocumentModalProps {
isOpen: boolean
onClose: () => void
initialFiles?: File[]
}
export function AddDocumentModal({ isOpen, onClose }: AddDocumentModalProps) {
export function AddDocumentModal({
isOpen,
onClose,
initialFiles,
}: AddDocumentModalProps) {
const isMobile = useIsMobile()
const hasUnsavedContentRef = useRef<() => boolean>(() => false)
const isSubmittingRef = useRef(false)
@ -74,6 +79,7 @@ export function AddDocumentModal({ isOpen, onClose }: AddDocumentModalProps) {
isOpen={isOpen}
hasUnsavedContentRef={hasUnsavedContentRef}
isSubmittingRef={isSubmittingRef}
initialFiles={initialFiles}
/>
</div>
</DialogContent>
@ -115,12 +121,14 @@ export function AddDocument({
isOpen,
hasUnsavedContentRef,
isSubmittingRef,
initialFiles,
}: {
onClose: () => void
onRequestClose?: () => void
isOpen?: boolean
hasUnsavedContentRef?: React.MutableRefObject<() => boolean>
isSubmittingRef?: React.MutableRefObject<boolean>
initialFiles?: File[]
}) {
const isMobile = useIsMobile()
const [addParam, setAddParam] = useQueryState("add", addDocumentParam)
@ -182,6 +190,27 @@ export function AddDocument({
}
}, [isOpen])
// Seed file queue from global drag-and-drop (only once per modal open)
const initialFilesConsumed = useRef(false)
useEffect(() => {
if (!isOpen) {
initialFilesConsumed.current = false
return
}
if (initialFilesConsumed.current) return
if (!initialFiles || initialFiles.length === 0) return
initialFilesConsumed.current = true
const items: FileQueueItem[] = initialFiles.map((file) => ({
id: crypto.randomUUID(),
file,
status: "pending" as const,
}))
setFileData((prev) => ({
...prev,
items: [...prev.items, ...items],
}))
}, [isOpen, initialFiles])
// Submit handlers
const handleNoteSubmit = useCallback(
(content: string, contentType: "note" | "link") => {