@@ -51,6 +51,14 @@ import { cn } from "@/lib/utils";
5151const MODEL_OPTIONS_STORAGE_PREFIX = "deeix-chat:chat-model-options:" ;
5252const EMPTY_CONVERSATION_OPTIONS : ConversationOptions = { } ;
5353
54+ function dragEventContainsFiles ( event : React . DragEvent < HTMLElement > ) : boolean {
55+ return Array . from ( event . dataTransfer . types ?? [ ] ) . includes ( "Files" ) ;
56+ }
57+
58+ function droppedFiles ( event : React . DragEvent < HTMLElement > ) : File [ ] {
59+ return Array . from ( event . dataTransfer . files ?? [ ] ) . filter ( ( file ) => file . name . trim ( ) || file . size > 0 ) ;
60+ }
61+
5462function modelOptionsStorageKey ( platformModelName : string ) : string {
5563 return `${ MODEL_OPTIONS_STORAGE_PREFIX } ${ encodeURIComponent ( platformModelName ) } ` ;
5664}
@@ -215,6 +223,8 @@ export function AppChatArea() {
215223 const [ selectedToolIDs , setSelectedToolIDs ] = React . useState < number [ ] > ( [ ] ) ;
216224 const htmlVisualPrompt = useHTMLVisualPrompt ( ) ;
217225 const initializedOptionsModelRef = React . useRef ( "" ) ;
226+ const fileDragDepthRef = React . useRef ( 0 ) ;
227+ const [ fileDragActive , setFileDragActive ] = React . useState ( false ) ;
218228
219229 React . useEffect ( ( ) => {
220230 setSelectedToolIDs ( ( current ) => {
@@ -359,6 +369,7 @@ export function AppChatArea() {
359369 activeGenerationRunsRef,
360370 } ) ;
361371 const generating = sending || Boolean ( resumingRunID ) ;
372+ const uploadDropDisabled = generating || loading || uploading ;
362373 const showLiveAssistant = showPendingAssistant || Boolean ( resumingRunID ) ;
363374 const latestMessageKey = visibleMessages . at ( - 1 ) ?. key ?? "" ;
364375 const onStopActiveMessage = React . useCallback ( ( ) => {
@@ -637,6 +648,59 @@ export function AppChatArea() {
637648 const selectedModelDefaultOptions = modelOptionPolicyDisabled
638649 ? EMPTY_CONVERSATION_OPTIONS
639650 : ( selectedModel ?. defaultOptions ?? EMPTY_CONVERSATION_OPTIONS ) ;
651+ const resetFileDragState = React . useCallback ( ( ) => {
652+ fileDragDepthRef . current = 0 ;
653+ setFileDragActive ( false ) ;
654+ } , [ ] ) ;
655+ const onFileDragEnter = React . useCallback ( ( event : React . DragEvent < HTMLDivElement > ) => {
656+ if ( ! dragEventContainsFiles ( event ) ) {
657+ return ;
658+ }
659+ event . preventDefault ( ) ;
660+ event . stopPropagation ( ) ;
661+ if ( uploadDropDisabled ) {
662+ return ;
663+ }
664+ fileDragDepthRef . current += 1 ;
665+ setFileDragActive ( true ) ;
666+ } , [ uploadDropDisabled ] ) ;
667+ const onFileDragOver = React . useCallback ( ( event : React . DragEvent < HTMLDivElement > ) => {
668+ if ( ! dragEventContainsFiles ( event ) ) {
669+ return ;
670+ }
671+ event . preventDefault ( ) ;
672+ event . stopPropagation ( ) ;
673+ event . dataTransfer . dropEffect = uploadDropDisabled ? "none" : "copy" ;
674+ } , [ uploadDropDisabled ] ) ;
675+ const onFileDragLeave = React . useCallback ( ( event : React . DragEvent < HTMLDivElement > ) => {
676+ if ( ! dragEventContainsFiles ( event ) ) {
677+ return ;
678+ }
679+ event . preventDefault ( ) ;
680+ event . stopPropagation ( ) ;
681+ fileDragDepthRef . current = Math . max ( 0 , fileDragDepthRef . current - 1 ) ;
682+ if ( fileDragDepthRef . current === 0 ) {
683+ setFileDragActive ( false ) ;
684+ }
685+ } , [ ] ) ;
686+ const onFileDrop = React . useCallback ( ( event : React . DragEvent < HTMLDivElement > ) => {
687+ if ( ! dragEventContainsFiles ( event ) ) {
688+ return ;
689+ }
690+ event . preventDefault ( ) ;
691+ event . stopPropagation ( ) ;
692+ const files = droppedFiles ( event ) ;
693+ resetFileDragState ( ) ;
694+ if ( uploadDropDisabled || files . length === 0 ) {
695+ return ;
696+ }
697+ void onUploadFiles ( files ) ;
698+ } , [ onUploadFiles , resetFileDragState , uploadDropDisabled ] ) ;
699+ React . useEffect ( ( ) => {
700+ if ( uploadDropDisabled ) {
701+ resetFileDragState ( ) ;
702+ }
703+ } , [ resetFileDragState , uploadDropDisabled ] ) ;
640704
641705 const chatInputProps = {
642706 draft,
@@ -661,6 +725,7 @@ export function AppChatArea() {
661725 defaultOptions : selectedModelDefaultOptions ,
662726 modelOptionPolicy,
663727 modelLoading : modelsLoading ,
728+ dropActive : fileDragActive ,
664729 onDraftChange : setDraft ,
665730 onModelChange : setSelectedPlatformModelName ,
666731 onSelectedToolsChange : setSelectedToolIDs ,
@@ -679,7 +744,13 @@ export function AppChatArea() {
679744 ! isConversationLoading && ! isConversationLoadFailed && ! isConversationMode && messagesWithInlineError . length === 0 ;
680745
681746 return (
682- < div className = "flex h-full min-h-0 w-full flex-1 flex-col overflow-hidden md:overflow-visible" >
747+ < div
748+ className = "relative flex h-full min-h-0 w-full flex-1 flex-col overflow-hidden md:overflow-visible"
749+ onDragEnter = { onFileDragEnter }
750+ onDragOver = { onFileDragOver }
751+ onDragLeave = { onFileDragLeave }
752+ onDrop = { onFileDrop }
753+ >
683754 { shouldUseCenteredComposer ? (
684755 < div className = "flex min-h-0 flex-1 flex-col overflow-hidden" >
685756 < ChatEmptyState greetingTitle = { greetingTitle } >
0 commit comments