Files
agent-framework/python/packages/devui/frontend/src/hooks/use-drag-drop.ts
T
Victor Dibia 411ee7a60f Python: DevUI fixes : Add multimodal input support for workflows and refactor chat input (#2593)
* show app version in devui .NET: Python: Improved Versioning for DevUI
Fixes #2059

* feat: Add multimodal input support for workflows and refactor chat input

This PR adds support for multimodal content (images, files) in workflow
inputs and refactors the chat input into a reusable component.

## Multimodal Workflow Support
- Add `isChatMessageSchema()` to detect ChatMessage input schemas
- Update `RunWorkflowButton` to use `ChatMessageInput` for ChatMessage workflows
- Wrap multimodal content in OpenAI message format for backend processing
- Add `_is_openai_multimodal_format()` to detect OpenAI ResponseInputParam
- Update `_parse_workflow_input()` to route multimodal input through
  existing `_convert_input_to_chat_message()` converter

## Reusable ChatMessageInput Component
- Extract chat input logic from agent-view into `ChatMessageInput` component
- Support file upload, drag & drop, paste handling, and attachments
- Add `useDragDrop` hook for parent-level drag handling with full-area
  drop zones
- Refactor agent-view to use the new shared component

## Other Improvements
- Add `isStreaming` prop to executor nodes for animation control
- Clean up unused imports and state variables in agent-view
- Add tests for multimodal workflow input handling

Fixes workflow input not receiving images when using AgentExecutor nodes.

* add self loop edge, fix #2470

* fix test
2025-12-03 20:15:51 +00:00

99 lines
2.5 KiB
TypeScript

/**
* useDragDrop - Hook for handling drag and drop file uploads at parent level
* Provides drag state and handlers that can be spread on a container element
*/
import { useState, useCallback, useRef } from "react";
export interface UseDragDropOptions {
/** Called when files are dropped */
onDrop?: (files: File[]) => void;
/** Whether drag/drop is disabled */
disabled?: boolean;
}
export interface UseDragDropReturn {
/** Whether a drag is currently over the drop zone */
isDragOver: boolean;
/** Files that were dropped (cleared after processing) */
droppedFiles: File[];
/** Clear the dropped files after they've been processed */
clearDroppedFiles: () => void;
/** Event handlers to spread on the container element */
dragHandlers: {
onDragEnter: (e: React.DragEvent) => void;
onDragLeave: (e: React.DragEvent) => void;
onDragOver: (e: React.DragEvent) => void;
onDrop: (e: React.DragEvent) => void;
};
}
export function useDragDrop(options: UseDragDropOptions = {}): UseDragDropReturn {
const { onDrop, disabled = false } = options;
const [isDragOver, setIsDragOver] = useState(false);
const [droppedFiles, setDroppedFiles] = useState<File[]>([]);
const dragCounterRef = useRef(0);
const handleDragEnter = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (disabled) return;
dragCounterRef.current++;
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
setIsDragOver(true);
}
}, [disabled]);
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (disabled) return;
dragCounterRef.current--;
if (dragCounterRef.current === 0) {
setIsDragOver(false);
}
}, [disabled]);
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
}, []);
const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
dragCounterRef.current = 0;
if (disabled) return;
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) {
setDroppedFiles(files);
onDrop?.(files);
}
}, [disabled, onDrop]);
const clearDroppedFiles = useCallback(() => {
setDroppedFiles([]);
}, []);
return {
isDragOver,
droppedFiles,
clearDroppedFiles,
dragHandlers: {
onDragEnter: handleDragEnter,
onDragLeave: handleDragLeave,
onDragOver: handleDragOver,
onDrop: handleDrop,
},
};
}