From 56118cacecdcf51db061e8dd9217d9bf1e7565e5 Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Tue, 12 May 2026 05:40:08 +0000 Subject: [PATCH] feat: add global file drag-and-drop to open Add Memory modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- apps/web/app/(app)/page.tsx | 83 +++++++++++++++++++++- apps/web/components/add-document/index.tsx | 31 +++++++- 2 files changed, 112 insertions(+), 2 deletions(-) diff --git a/apps/web/app/(app)/page.tsx b/apps/web/app/(app)/page.tsx index 1f30081a..4b5a5209 100644 --- a/apps/web/app/(app)/page.tsx +++ b/apps/web/app/(app)/page.tsx @@ -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([]) + 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 && ( +
+
+ +

+ Drop files to add as memories +

+

+ PDFs, images, documents, and more +

+
+
+ )} {showNovaBackdrop && ( <> setAddDoc(null)} + onClose={() => { + setAddDoc(null) + setGlobalDroppedFiles([]) + }} + initialFiles={globalDroppedFiles} /> 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} /> @@ -115,12 +121,14 @@ export function AddDocument({ isOpen, hasUnsavedContentRef, isSubmittingRef, + initialFiles, }: { onClose: () => void onRequestClose?: () => void isOpen?: boolean hasUnsavedContentRef?: React.MutableRefObject<() => boolean> isSubmittingRef?: React.MutableRefObject + 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") => {