diff --git a/src/apps/chat/components/composer/Composer.tsx b/src/apps/chat/components/composer/Composer.tsx
index b2530f199..433bd2c05 100644
--- a/src/apps/chat/components/composer/Composer.tsx
+++ b/src/apps/chat/components/composer/Composer.tsx
@@ -73,8 +73,8 @@ import { ComposerTextAreaActions } from './textarea/ComposerTextAreaActions';
import { StatusBar } from '../StatusBar';
import { TokenBadgeMemo } from './tokens/TokenBadge';
import { TokenProgressbarMemo } from './tokens/TokenProgressbar';
+import { useComposerDragDrop } from './useComposerDragDrop';
import { useComposerStartupText } from './store-composer';
-import { useDragDrop } from './useComposerDragDrop';
const zIndexComposerOverlayMic = 10;
@@ -159,7 +159,7 @@ export function Composer(props: {
const llmAttachmentDraftsCollection = useLLMAttachmentDrafts(attachmentDrafts, props.chatLLM);
// drag/drop
- const { dragContainerSx, dragDropComponent, handleDragEnter, handleDragStart } = useDragDrop(!!props.isMobile, attachAppendDataTransfer);
+ const { dragContainerSx, dropComponent, handleContainerDragEnter, handleContainerDragStart } = useComposerDragDrop(!props.isMobile, attachAppendDataTransfer);
// ai functions
const agiAttachmentPrompts = useAgiAttachmentPrompts(useChatAutoSuggestAttachmentPrompts(), attachmentDrafts);
@@ -572,8 +572,8 @@ export function Composer(props: {
@@ -877,7 +877,7 @@ export function Composer(props: {
{/* overlay: Drag & Drop*/}
- {dragDropComponent}
+ {dropComponent}
diff --git a/src/apps/chat/components/composer/useComposerDragDrop.tsx b/src/apps/chat/components/composer/useComposerDragDrop.tsx
index a919894f3..37abe5bfa 100644
--- a/src/apps/chat/components/composer/useComposerDragDrop.tsx
+++ b/src/apps/chat/components/composer/useComposerDragDrop.tsx
@@ -1,84 +1,15 @@
import * as React from 'react';
-import type { SxProps } from '@mui/joy/styles/types';
-import { Card, Typography } from '@mui/joy';
-import AttachFileRoundedIcon from '@mui/icons-material/AttachFileRounded';
+import { useDragDropDataTransfer } from '~/common/components/useDragDropDataTransfer';
-const zIndexComposerOverlayDrop = 20;
-
-
-const containerSx: SxProps = {
- position: 'relative', /* for Drop overlay */
-} as const;
-
-const inactiveCardSx: SxProps = {
- display: 'none',
- position: 'absolute',
- inset: 0,
- pointerEvents: 'none',
- zIndex: zIndexComposerOverlayDrop,
-} as const;
-
-const draggingCardSx: SxProps = {
- ...inactiveCardSx,
- pointerEvents: undefined,
- border: '1px dashed',
- borderRadius: 'sm',
- boxShadow: 'inset 1px 0px 3px -2px var(--joy-palette-success-softColor)',
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- gap: 2,
-} as const;
-
-
-export function useDragDrop(
- disabled: boolean,
+export function useComposerDragDrop(
+ enabled: boolean,
onDataTransfer: (dataTransfer: DataTransfer, type: 'paste' | 'drop', isDropOnTextarea: boolean) => Promise,
) {
- // state
- const [isDragging, setIsDragging] = React.useState(false);
-
-
- // handlers
-
- const eatDragEvent = React.useCallback((event: React.DragEvent) => {
- event.preventDefault();
- event.stopPropagation();
- }, []);
-
-
- const handleExtDragEnter = React.useCallback((event: React.DragEvent) => {
- const isFromSelf = event.dataTransfer.types.includes('x-app/agi');
- if (!isFromSelf) {
- eatDragEvent(event);
- setIsDragging(true);
- }
- }, [eatDragEvent]);
-
- const handleExtDragStart = React.useCallback((event: React.DragEvent) => {
- event.dataTransfer.setData('x-app/agi', 'do-not-intercept');
- }, []);
-
-
- const handleDragLeave = React.useCallback((event: React.DragEvent) => {
- eatDragEvent(event);
- setIsDragging(false);
- }, [eatDragEvent]);
-
- const handleDragOver = React.useCallback((event: React.DragEvent) => {
- eatDragEvent(event);
- // this makes sure we don't "transfer" (or move) the item, but we tell the sender we'll copy it
- event.dataTransfer.dropEffect = 'copy';
- }, [eatDragEvent]);
-
- const handleDrop = React.useCallback(async (event: React.DragEvent) => {
- eatDragEvent(event);
- setIsDragging(false);
-
- const { dataTransfer } = event;
+ // drop implementation for the composer
+ const handleComposerDrop = React.useCallback(async (dataTransfer: DataTransfer) => {
// VSCode: detect failure of dropping from VSCode, details below:
// https://github.com/microsoft/vscode/issues/98629#issuecomment-634475572
@@ -106,34 +37,8 @@ export function useDragDrop(
// textarea drop
void onDataTransfer(dataTransfer, 'drop', true); // fire/forget
- }, [eatDragEvent, onDataTransfer]);
+ }, [onDataTransfer]);
- const dropComponent = React.useMemo(() => {
- if (disabled) return null;
-
- return (
-
- {isDragging && }
- {isDragging &&
- I will hold on to this for you.
- }
-
- );
- }, [handleDragLeave, handleDragOver, handleDrop, isDragging, disabled]);
-
- return {
- dragContainerSx: containerSx,
- dragDropComponent: dropComponent,
- handleDragEnter: handleExtDragEnter,
- handleDragStart: handleExtDragStart,
- };
+ return useDragDropDataTransfer(enabled, 'I will hold on to this for you.', handleComposerDrop);
}
diff --git a/src/common/components/useDragDropDataTransfer.tsx b/src/common/components/useDragDropDataTransfer.tsx
new file mode 100644
index 000000000..574751d80
--- /dev/null
+++ b/src/common/components/useDragDropDataTransfer.tsx
@@ -0,0 +1,118 @@
+import * as React from 'react';
+
+import type { SxProps } from '@mui/joy/styles/types';
+import { Card, Typography } from '@mui/joy';
+import AttachFileRoundedIcon from '@mui/icons-material/AttachFileRounded';
+
+
+// constants
+const zIndexComposerOverlayDrop = 20;
+const EXCLUDE_SELF_TYPE = 'x-app/agi';
+
+
+// styles
+
+const dragContainerSx: SxProps = {
+ position: 'relative', /* for Drop overlay */
+} as const;
+
+const dropCardInactiveSx: SxProps = {
+ display: 'none',
+ position: 'absolute',
+ inset: 0,
+ pointerEvents: 'none',
+ zIndex: zIndexComposerOverlayDrop,
+} as const;
+
+const dropCardDraggingCardSx: SxProps = {
+ ...dropCardInactiveSx,
+ pointerEvents: undefined,
+ border: '1px dashed',
+ borderRadius: 'sm',
+ boxShadow: 'inset 1px 0px 3px -2px var(--joy-palette-success-softColor)',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: 2,
+} as const;
+
+
+// Drag/Drop that can be used in any component and invokes a DataTransfer callback on success
+
+export function useDragDropDataTransfer(enabled: boolean, dropText: string, onDropCallback: (dataTransfer: DataTransfer) => Promise) {
+
+ // state
+ const [isDragging, setIsDragging] = React.useState(false);
+
+
+ const _eatDragEvent = React.useCallback((event: React.DragEvent) => {
+ event.preventDefault();
+ event.stopPropagation();
+ }, []);
+
+
+ // Container events
+
+ const handleContainerDragEnter = React.useCallback((event: React.DragEvent) => {
+ const isFromSelf = event.dataTransfer.types.includes(EXCLUDE_SELF_TYPE);
+ if (!isFromSelf) {
+ _eatDragEvent(event);
+ setIsDragging(true);
+ }
+ }, [_eatDragEvent]);
+
+ const handleContainerDragStart = React.useCallback((event: React.DragEvent) => {
+ event.dataTransfer.setData(EXCLUDE_SELF_TYPE, 'do-not-intercept');
+ }, []);
+
+
+ // Drop Target events
+
+ const _handleDragOver = React.useCallback((event: React.DragEvent) => {
+ _eatDragEvent(event);
+ // this makes sure we don't "transfer" (or move) the item, but we tell the sender we'll copy it
+ event.dataTransfer.dropEffect = 'copy';
+ }, [_eatDragEvent]);
+
+ const _handleDragLeave = React.useCallback((event: React.DragEvent) => {
+ _eatDragEvent(event);
+ setIsDragging(false);
+ }, [_eatDragEvent]);
+
+ const _handleDrop = React.useCallback(async (event: React.DragEvent) => {
+ _eatDragEvent(event);
+ setIsDragging(false);
+ await onDropCallback(event.dataTransfer);
+ }, [_eatDragEvent, onDropCallback]);
+
+
+ const dropComponent = React.useMemo(() => {
+ if (!enabled) return null;
+
+ return (
+
+ {isDragging && }
+ {isDragging &&
+ {dropText}
+ }
+
+ );
+ }, [enabled, isDragging, _handleDragLeave, _handleDragOver, _handleDrop, dropText]);
+
+
+ return {
+ dragContainerSx,
+ dropComponent,
+ handleContainerDragEnter,
+ handleContainerDragStart,
+ isDragging,
+ };
+}