mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
expand chat file drop zone (#1085)
## What changed - Added a chat-shell-level file drag/drop target so the whole chat area, including the conversation area, shows the drop overlay and accepts files. - Disabled the nested composer drop handler for the sidebar chat path to avoid duplicate drops or stuck overlay state. - Kept the existing local input drop behavior available for other ChatInput usages like the home composer.
This commit is contained in:
parent
6efb4ec3c2
commit
a2a1b065a2
2 changed files with 120 additions and 14 deletions
|
|
@ -201,6 +201,7 @@ export function ChatSidebar({
|
|||
const [attachmentDrafts, setAttachmentDrafts] = useState<
|
||||
ChatAttachmentDraft[]
|
||||
>([])
|
||||
const [isChatDraggingFiles, setIsChatDraggingFiles] = useState(false)
|
||||
const [selectedModel, setSelectedModel] = useState<ModelId>(
|
||||
initialSelectedModel ?? "grok-4.3",
|
||||
)
|
||||
|
|
@ -246,6 +247,7 @@ export function ChatSidebar({
|
|||
null,
|
||||
)
|
||||
const messagesContainerRef = useRef<HTMLDivElement>(null)
|
||||
const chatDragDepthRef = useRef(0)
|
||||
const isScrolledToBottomRef = useRef(true)
|
||||
const userJustSentRef = useRef(false)
|
||||
const sentQueuedMessageRef = useRef<string | null>(null)
|
||||
|
|
@ -725,6 +727,77 @@ export function ChatSidebar({
|
|||
[attachmentDrafts, currentChatId, uploadAttachmentDraft],
|
||||
)
|
||||
|
||||
const hasDraggedFiles = useCallback(
|
||||
(event: React.DragEvent) =>
|
||||
Array.from(event.dataTransfer.types).includes("Files"),
|
||||
[],
|
||||
)
|
||||
|
||||
const resetChatFileDrag = useCallback(() => {
|
||||
chatDragDepthRef.current = 0
|
||||
setIsChatDraggingFiles(false)
|
||||
}, [])
|
||||
|
||||
const handleChatDragEnter = useCallback(
|
||||
(event: React.DragEvent) => {
|
||||
if (!hasDraggedFiles(event)) return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
chatDragDepthRef.current += 1
|
||||
if (status !== "submitted" && status !== "streaming") {
|
||||
setIsChatDraggingFiles(true)
|
||||
}
|
||||
},
|
||||
[hasDraggedFiles, status],
|
||||
)
|
||||
|
||||
const handleChatDragOver = useCallback(
|
||||
(event: React.DragEvent) => {
|
||||
if (!hasDraggedFiles(event)) return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
event.dataTransfer.dropEffect =
|
||||
status === "submitted" || status === "streaming" ? "none" : "copy"
|
||||
},
|
||||
[hasDraggedFiles, status],
|
||||
)
|
||||
|
||||
const handleChatDragLeave = useCallback(
|
||||
(event: React.DragEvent) => {
|
||||
if (!hasDraggedFiles(event)) return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
chatDragDepthRef.current = Math.max(0, chatDragDepthRef.current - 1)
|
||||
if (chatDragDepthRef.current === 0) {
|
||||
setIsChatDraggingFiles(false)
|
||||
}
|
||||
},
|
||||
[hasDraggedFiles],
|
||||
)
|
||||
|
||||
const handleChatDrop = useCallback(
|
||||
(event: React.DragEvent) => {
|
||||
if (!hasDraggedFiles(event)) return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
resetChatFileDrag()
|
||||
const files = event.dataTransfer.files
|
||||
if (status !== "submitted" && status !== "streaming" && files.length) {
|
||||
handleAddAttachmentFiles(files)
|
||||
}
|
||||
},
|
||||
[handleAddAttachmentFiles, hasDraggedFiles, resetChatFileDrag, status],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (status === "submitted" || status === "streaming") {
|
||||
resetChatFileDrag()
|
||||
}
|
||||
}, [resetChatFileDrag, status])
|
||||
|
||||
useEffect(() => {
|
||||
if (pendingThreadLoad && currentChatId === pendingThreadLoad.id) {
|
||||
setMessages(pendingThreadLoad.messages)
|
||||
|
|
@ -1765,6 +1838,27 @@ export function ChatSidebar({
|
|||
</div>
|
||||
) : null
|
||||
|
||||
const chatDropOverlay = isChatDraggingFiles ? (
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-0 z-[80] grid place-items-center border border-dashed border-[#4B5563] bg-black/72 text-sm font-medium text-fg-primary backdrop-blur-sm",
|
||||
isMobile || isPageDesktop ? "rounded-none" : "rounded-2xl",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div className="rounded-lg border border-white/10 bg-black/50 px-4 py-2 shadow-[0_12px_32px_rgba(0,0,0,0.35)]">
|
||||
Drop files to attach
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
const chatDropTargetProps = {
|
||||
onDragEnter: handleChatDragEnter,
|
||||
onDragOver: handleChatDragOver,
|
||||
onDragLeave: handleChatDragLeave,
|
||||
onDrop: handleChatDrop,
|
||||
onDragEnd: resetChatFileDrag,
|
||||
}
|
||||
|
||||
const shell = (
|
||||
<>
|
||||
{showHeaderRow ? (
|
||||
|
|
@ -1983,6 +2077,7 @@ export function ChatSidebar({
|
|||
onRetryAttachment={handleRetryAttachment}
|
||||
canSend={canSendMessage}
|
||||
attachmentAccept={CHAT_ATTACHMENT_ACCEPT}
|
||||
disableFileDropZone
|
||||
sendDisabled={isResponding && isQueueFull}
|
||||
sendDisabledTooltip={`Queue is full (${CHAT_QUEUE_LIMIT} max)`}
|
||||
activeStatus={
|
||||
|
|
@ -2063,7 +2158,9 @@ export function ChatSidebar({
|
|||
layout === "page" ? { opacity: 0, y: 12 } : { x: "100px", opacity: 0 }
|
||||
}
|
||||
transition={{ duration: 0.3, ease: "easeOut", bounce: 0 }}
|
||||
{...(!isPageDesktop ? chatDropTargetProps : {})}
|
||||
>
|
||||
{!isPageDesktop && chatDropOverlay}
|
||||
{chatHistorySheet}
|
||||
{isPageDesktop ? (
|
||||
<div className="flex h-full min-h-0 w-full flex-1 flex-row">
|
||||
|
|
@ -2073,7 +2170,11 @@ export function ChatSidebar({
|
|||
chatProject === AUTO_CHAT_SPACE_ID ? null : [chatProject]
|
||||
}
|
||||
/>
|
||||
<div className="flex h-full min-h-0 w-full min-w-0 max-w-[min(720px,100%)] shrink-0 basis-[min(720px,50vw)] flex-col">
|
||||
<div
|
||||
{...chatDropTargetProps}
|
||||
className="relative flex h-full min-h-0 w-full min-w-0 max-w-[min(720px,100%)] shrink-0 basis-[min(720px,50vw)] flex-col"
|
||||
>
|
||||
{chatDropOverlay}
|
||||
{pageDesktopToolbarRow}
|
||||
<div className="relative mx-auto flex h-full min-h-0 w-full min-w-0 max-w-[min(720px,100%)] flex-1 flex-col px-3 sm:px-4 md:px-0">
|
||||
{shell}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ interface ChatInputProps {
|
|||
onRetryAttachment?: (id: string) => void
|
||||
canSend?: boolean
|
||||
attachmentAccept?: string
|
||||
disableFileDropZone?: boolean
|
||||
}
|
||||
|
||||
export default function ChatInput({
|
||||
|
|
@ -84,6 +85,7 @@ export default function ChatInput({
|
|||
onRetryAttachment,
|
||||
canSend,
|
||||
attachmentAccept = CHAT_ATTACHMENT_ACCEPT,
|
||||
disableFileDropZone = false,
|
||||
}: ChatInputProps) {
|
||||
const [isMultiline, setIsMultiline] = useState(false)
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
|
|
@ -200,11 +202,20 @@ export default function ChatInput({
|
|||
</>
|
||||
) : null
|
||||
|
||||
const dropOverlay = isDraggingFiles ? (
|
||||
const dropOverlay =
|
||||
!disableFileDropZone && isDraggingFiles ? (
|
||||
<div className="pointer-events-none absolute inset-1 z-10 grid place-items-center rounded-lg border border-dashed border-[#4B5563] bg-black/70 text-sm font-medium text-fg-primary backdrop-blur-sm">
|
||||
Drop files to attach
|
||||
</div>
|
||||
) : null
|
||||
const dropZoneProps = disableFileDropZone
|
||||
? {}
|
||||
: {
|
||||
onDragEnter: handleDragEnter,
|
||||
onDragOver: handleDragOver,
|
||||
onDragLeave: handleDragLeave,
|
||||
onDrop: handleDrop,
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
|
|
@ -328,10 +339,7 @@ export default function ChatInput({
|
|||
{stackedToolbar ? (
|
||||
<fieldset
|
||||
aria-label="Chat input with file drop zone"
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
{...dropZoneProps}
|
||||
className="relative z-30 m-0 flex min-w-0 flex-col gap-2 rounded-xl border-0 bg-surface-card/60 p-2 shadow-[0_16px_48px_rgba(0,0,0,0.34)] backdrop-blur-md transition-all duration-200 focus-within:ring-1 focus-within:ring-fg-primary/10"
|
||||
>
|
||||
{dropOverlay}
|
||||
|
|
@ -368,10 +376,7 @@ export default function ChatInput({
|
|||
) : (
|
||||
<fieldset
|
||||
aria-label="Chat input with file drop zone"
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
{...dropZoneProps}
|
||||
className={cn(
|
||||
"relative m-0 flex min-w-0 flex-col gap-2 rounded-xl border-0 bg-surface-card/60 p-2 shadow-[0_16px_48px_rgba(0,0,0,0.34)] backdrop-blur-md transition-all duration-200 focus-within:ring-1 focus-within:ring-fg-primary/10",
|
||||
isMultiline && "flex-col",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue