mirror of https://github.com/kortix-ai/suna.git
upload file message input
This commit is contained in:
parent
9cd703788e
commit
1d22b51ac6
|
@ -3,7 +3,12 @@
|
|||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Send, Square, Loader2, File } from "lucide-react";
|
||||
import { Send, Square, Loader2, File, Upload, X } from "lucide-react";
|
||||
import { createClient } from "@/utils/supabase/client";
|
||||
import { toast } from "sonner";
|
||||
|
||||
// Define API_URL
|
||||
const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '';
|
||||
|
||||
interface ChatInputProps {
|
||||
onSubmit: (message: string) => void;
|
||||
|
@ -19,6 +24,12 @@ interface ChatInputProps {
|
|||
sandboxId?: string;
|
||||
}
|
||||
|
||||
interface UploadedFile {
|
||||
name: string;
|
||||
path: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export function ChatInput({
|
||||
onSubmit,
|
||||
placeholder = "Type your message... (Enter to send, Shift+Enter for new line)",
|
||||
|
@ -34,6 +45,9 @@ export function ChatInput({
|
|||
}: ChatInputProps) {
|
||||
const [inputValue, setInputValue] = useState(value || "");
|
||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [uploadedFile, setUploadedFile] = useState<UploadedFile | null>(null);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
// Allow controlled or uncontrolled usage
|
||||
const isControlled = value !== undefined && onChange !== undefined;
|
||||
|
@ -72,18 +86,29 @@ export function ChatInput({
|
|||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!inputValue.trim() || loading || (disabled && !isAgentRunning)) return;
|
||||
if ((!inputValue.trim() && !uploadedFile) || loading || (disabled && !isAgentRunning)) return;
|
||||
|
||||
if (isAgentRunning && onStopAgent) {
|
||||
onStopAgent();
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit(inputValue);
|
||||
let message = inputValue;
|
||||
|
||||
// Add file information to the message if a file was uploaded
|
||||
if (uploadedFile) {
|
||||
const fileInfo = `[Uploaded file: ${uploadedFile.name} (${formatFileSize(uploadedFile.size)}) at ${uploadedFile.path}]`;
|
||||
message = message ? `${message}\n\n${fileInfo}` : fileInfo;
|
||||
}
|
||||
|
||||
onSubmit(message);
|
||||
|
||||
if (!isControlled) {
|
||||
setInputValue("");
|
||||
}
|
||||
|
||||
// Reset the uploaded file after sending
|
||||
setUploadedFile(null);
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
|
@ -98,15 +123,111 @@ export function ChatInput({
|
|||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
if (inputValue.trim() && !loading && (!disabled || isAgentRunning)) {
|
||||
if ((inputValue.trim() || uploadedFile) && !loading && (!disabled || isAgentRunning)) {
|
||||
handleSubmit(e as React.FormEvent);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileUpload = () => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
const processFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!sandboxId || !event.target.files || event.target.files.length === 0) return;
|
||||
|
||||
try {
|
||||
setIsUploading(true);
|
||||
|
||||
const file = event.target.files[0];
|
||||
|
||||
if (file.size > 50 * 1024 * 1024) { // 50MB limit
|
||||
toast.error("File size exceeds 50MB limit");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a FormData object
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
// Upload to workspace root by default
|
||||
const uploadPath = `/workspace/${file.name}`;
|
||||
formData.append('path', uploadPath);
|
||||
|
||||
const supabase = createClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
if (!session?.access_token) {
|
||||
throw new Error('No access token available');
|
||||
}
|
||||
|
||||
// Upload using FormData
|
||||
const response = await fetch(`${API_URL}/sandboxes/${sandboxId}/files`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${session.access_token}`,
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Upload failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
// Set the uploaded file info
|
||||
setUploadedFile({
|
||||
name: file.name,
|
||||
path: uploadPath,
|
||||
size: file.size
|
||||
});
|
||||
|
||||
toast.success(`File uploaded: ${file.name}`);
|
||||
} catch (error) {
|
||||
console.error("File upload failed:", error);
|
||||
toast.error(typeof error === 'string' ? error : (error instanceof Error ? error.message : "Failed to upload file"));
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
// Reset the input
|
||||
event.target.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
};
|
||||
|
||||
const removeUploadedFile = () => {
|
||||
setUploadedFile(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<form onSubmit={handleSubmit} className="relative">
|
||||
{uploadedFile && (
|
||||
<div className="mb-2 p-2 bg-secondary/20 rounded-md flex items-center justify-between">
|
||||
<div className="flex items-center text-sm">
|
||||
<File className="h-4 w-4 mr-2 text-primary" />
|
||||
<span className="font-medium">{uploadedFile.name}</span>
|
||||
<span className="text-xs text-muted-foreground ml-2">
|
||||
({formatFileSize(uploadedFile.size)})
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={removeUploadedFile}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
value={inputValue}
|
||||
|
@ -117,12 +238,38 @@ export function ChatInput({
|
|||
? "Agent is thinking..."
|
||||
: placeholder
|
||||
}
|
||||
className="min-h-[50px] max-h-[200px] pr-12 resize-none"
|
||||
className="min-h-[50px] max-h-[200px] pr-20 resize-none"
|
||||
disabled={loading || (disabled && !isAgentRunning)}
|
||||
rows={1}
|
||||
/>
|
||||
|
||||
<div className="absolute right-2 bottom-2 flex items-center space-x-1">
|
||||
{/* Upload file button */}
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleFileUpload}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 rounded-full"
|
||||
disabled={loading || (disabled && !isAgentRunning) || isUploading}
|
||||
aria-label="Upload file"
|
||||
>
|
||||
{isUploading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Upload className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Hidden file input */}
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
className="hidden"
|
||||
onChange={processFileUpload}
|
||||
/>
|
||||
|
||||
{/* File browser button */}
|
||||
{onFileBrowse && (
|
||||
<Button
|
||||
type="button"
|
||||
|
@ -143,7 +290,7 @@ export function ChatInput({
|
|||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 rounded-full"
|
||||
disabled={(!inputValue.trim() && !isAgentRunning) || loading || (disabled && !isAgentRunning)}
|
||||
disabled={((!inputValue.trim() && !uploadedFile) && !isAgentRunning) || loading || (disabled && !isAgentRunning)}
|
||||
aria-label={isAgentRunning ? 'Stop agent' : 'Send message'}
|
||||
>
|
||||
{loading ? (
|
||||
|
|
Loading…
Reference in New Issue