mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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
This commit is contained in:
committed by
GitHub
Unverified
parent
6835161f2d
commit
411ee7a60f
@@ -0,0 +1,3 @@
|
||||
export { useCancellableRequest, isAbortError } from './useCancellableRequest';
|
||||
export { useDragDrop } from './use-drag-drop';
|
||||
export type { UseDragDropOptions, UseDragDropReturn } from './use-drag-drop';
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* 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,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Custom hook for managing cancellable requests with AbortController
|
||||
* Reduces duplication across agent and workflow views
|
||||
*/
|
||||
|
||||
import { useState, useRef, useCallback } from "react";
|
||||
|
||||
/**
|
||||
* Hook for managing cancellable requests with AbortController
|
||||
* @returns Object with cancellation state and methods
|
||||
*/
|
||||
export function useCancellableRequest() {
|
||||
const [isCancelling, setIsCancelling] = useState(false);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
/**
|
||||
* Creates a new AbortController and returns its signal
|
||||
* Resets the cancelling state
|
||||
*/
|
||||
const createAbortSignal = useCallback((): AbortSignal => {
|
||||
abortControllerRef.current = new AbortController();
|
||||
setIsCancelling(false);
|
||||
return abortControllerRef.current.signal;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Cancels the current request if one exists
|
||||
*/
|
||||
const handleCancel = useCallback(() => {
|
||||
if (abortControllerRef.current) {
|
||||
setIsCancelling(true);
|
||||
abortControllerRef.current.abort();
|
||||
abortControllerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Resets the cancelling state - useful in error handlers
|
||||
*/
|
||||
const resetCancelling = useCallback(() => {
|
||||
setIsCancelling(false);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Cleanup function to be called when component unmounts
|
||||
*/
|
||||
const cleanup = useCallback(() => {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
abortControllerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isCancelling,
|
||||
createAbortSignal,
|
||||
handleCancel,
|
||||
resetCancelling,
|
||||
cleanup,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to check if an error is an AbortError
|
||||
* @param error - The error to check
|
||||
* @returns true if the error is an AbortError
|
||||
*/
|
||||
export function isAbortError(error: unknown): boolean {
|
||||
return error instanceof DOMException && error.name === 'AbortError';
|
||||
}
|
||||
Reference in New Issue
Block a user