mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
Co-authored-by: Hiep Le <69354317+hieptl@users.noreply.github.com> Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
153 lines
5.0 KiB
TypeScript
153 lines
5.0 KiB
TypeScript
import { isFileImage } from "#/utils/is-file-image";
|
|
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
|
import { validateFiles } from "#/utils/file-validation";
|
|
import { CustomChatInput } from "./custom-chat-input";
|
|
import { AgentState } from "#/types/agent-state";
|
|
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
|
import { GitControlBar } from "./git-control-bar";
|
|
import { useConversationStore } from "#/state/conversation-store";
|
|
import { useAgentState } from "#/hooks/use-agent-state";
|
|
import { processFiles, processImages } from "#/utils/file-processing";
|
|
|
|
interface InteractiveChatBoxProps {
|
|
onSubmit: (message: string, images: File[], files: File[]) => void;
|
|
}
|
|
|
|
export function InteractiveChatBox({ onSubmit }: InteractiveChatBoxProps) {
|
|
const {
|
|
images,
|
|
files,
|
|
addImages,
|
|
addFiles,
|
|
clearAllFiles,
|
|
addFileLoading,
|
|
removeFileLoading,
|
|
addImageLoading,
|
|
removeImageLoading,
|
|
} = useConversationStore();
|
|
const { curAgentState } = useAgentState();
|
|
const { data: conversation } = useActiveConversation();
|
|
|
|
// Helper function to validate and filter files
|
|
const validateAndFilterFiles = (selectedFiles: File[]) => {
|
|
const validation = validateFiles(selectedFiles, [...images, ...files]);
|
|
|
|
if (!validation.isValid) {
|
|
displayErrorToast(`Error: ${validation.errorMessage}`);
|
|
return null;
|
|
}
|
|
|
|
const validFiles = selectedFiles.filter((f) => !isFileImage(f));
|
|
const validImages = selectedFiles.filter((f) => isFileImage(f));
|
|
|
|
return { validFiles, validImages };
|
|
};
|
|
|
|
// Helper function to show loading indicators for files
|
|
const showLoadingIndicators = (validFiles: File[], validImages: File[]) => {
|
|
validFiles.forEach((file) => addFileLoading(file.name));
|
|
validImages.forEach((image) => addImageLoading(image.name));
|
|
};
|
|
|
|
// Helper function to handle successful file processing results
|
|
const handleSuccessfulFiles = (fileResults: { successful: File[] }) => {
|
|
if (fileResults.successful.length > 0) {
|
|
addFiles(fileResults.successful);
|
|
fileResults.successful.forEach((file) => removeFileLoading(file.name));
|
|
}
|
|
};
|
|
|
|
// Helper function to handle successful image processing results
|
|
const handleSuccessfulImages = (imageResults: { successful: File[] }) => {
|
|
if (imageResults.successful.length > 0) {
|
|
addImages(imageResults.successful);
|
|
imageResults.successful.forEach((image) =>
|
|
removeImageLoading(image.name),
|
|
);
|
|
}
|
|
};
|
|
|
|
// Helper function to handle failed file processing results
|
|
const handleFailedFiles = (
|
|
fileResults: { failed: { file: File; error: Error }[] },
|
|
imageResults: { failed: { file: File; error: Error }[] },
|
|
) => {
|
|
fileResults.failed.forEach(({ file, error }) => {
|
|
removeFileLoading(file.name);
|
|
displayErrorToast(
|
|
`Failed to process file ${file.name}: ${error.message}`,
|
|
);
|
|
});
|
|
|
|
imageResults.failed.forEach(({ file, error }) => {
|
|
removeImageLoading(file.name);
|
|
displayErrorToast(
|
|
`Failed to process image ${file.name}: ${error.message}`,
|
|
);
|
|
});
|
|
};
|
|
|
|
// Helper function to clear loading states on error
|
|
const clearLoadingStates = (validFiles: File[], validImages: File[]) => {
|
|
validFiles.forEach((file) => removeFileLoading(file.name));
|
|
validImages.forEach((image) => removeImageLoading(image.name));
|
|
};
|
|
|
|
const handleUpload = async (selectedFiles: File[]) => {
|
|
// Step 1: Validate and filter files
|
|
const result = validateAndFilterFiles(selectedFiles);
|
|
if (!result) return;
|
|
|
|
const { validFiles, validImages } = result;
|
|
|
|
// Step 2: Show loading indicators immediately
|
|
showLoadingIndicators(validFiles, validImages);
|
|
|
|
// Step 3: Process files using REAL FileReader
|
|
try {
|
|
const [fileResults, imageResults] = await Promise.all([
|
|
processFiles(validFiles),
|
|
processImages(validImages),
|
|
]);
|
|
|
|
// Step 4: Handle successful results
|
|
handleSuccessfulFiles(fileResults);
|
|
handleSuccessfulImages(imageResults);
|
|
|
|
// Step 5: Handle failed results
|
|
handleFailedFiles(fileResults, imageResults);
|
|
} catch {
|
|
// Clear loading states and show error
|
|
clearLoadingStates(validFiles, validImages);
|
|
displayErrorToast("An unexpected error occurred while processing files");
|
|
}
|
|
};
|
|
|
|
const handleSubmit = (message: string) => {
|
|
onSubmit(message, images, files);
|
|
clearAllFiles();
|
|
};
|
|
|
|
const handleSuggestionsClick = (suggestion: string) => {
|
|
handleSubmit(suggestion);
|
|
};
|
|
|
|
const isDisabled =
|
|
curAgentState === AgentState.LOADING ||
|
|
curAgentState === AgentState.AWAITING_USER_CONFIRMATION;
|
|
|
|
return (
|
|
<div data-testid="interactive-chat-box">
|
|
<CustomChatInput
|
|
disabled={isDisabled}
|
|
onSubmit={handleSubmit}
|
|
onFilesPaste={handleUpload}
|
|
conversationStatus={conversation?.status || null}
|
|
/>
|
|
<div className="mt-4">
|
|
<GitControlBar onSuggestionsClick={handleSuggestionsClick} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|