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 React, { useState, useRef, useEffect } from 'react';
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Button } from "@/components/ui/button";
|
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 {
|
interface ChatInputProps {
|
||||||
onSubmit: (message: string) => void;
|
onSubmit: (message: string) => void;
|
||||||
|
@ -19,6 +24,12 @@ interface ChatInputProps {
|
||||||
sandboxId?: string;
|
sandboxId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UploadedFile {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
export function ChatInput({
|
export function ChatInput({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
placeholder = "Type your message... (Enter to send, Shift+Enter for new line)",
|
placeholder = "Type your message... (Enter to send, Shift+Enter for new line)",
|
||||||
|
@ -34,6 +45,9 @@ export function ChatInput({
|
||||||
}: ChatInputProps) {
|
}: ChatInputProps) {
|
||||||
const [inputValue, setInputValue] = useState(value || "");
|
const [inputValue, setInputValue] = useState(value || "");
|
||||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
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
|
// Allow controlled or uncontrolled usage
|
||||||
const isControlled = value !== undefined && onChange !== undefined;
|
const isControlled = value !== undefined && onChange !== undefined;
|
||||||
|
@ -72,18 +86,29 @@ export function ChatInput({
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!inputValue.trim() || loading || (disabled && !isAgentRunning)) return;
|
if ((!inputValue.trim() && !uploadedFile) || loading || (disabled && !isAgentRunning)) return;
|
||||||
|
|
||||||
if (isAgentRunning && onStopAgent) {
|
if (isAgentRunning && onStopAgent) {
|
||||||
onStopAgent();
|
onStopAgent();
|
||||||
return;
|
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) {
|
if (!isControlled) {
|
||||||
setInputValue("");
|
setInputValue("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset the uploaded file after sending
|
||||||
|
setUploadedFile(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
@ -98,15 +123,111 @@ export function ChatInput({
|
||||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (inputValue.trim() && !loading && (!disabled || isAgentRunning)) {
|
if ((inputValue.trim() || uploadedFile) && !loading && (!disabled || isAgentRunning)) {
|
||||||
handleSubmit(e as React.FormEvent);
|
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 (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<form onSubmit={handleSubmit} className="relative">
|
<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
|
<Textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
|
@ -117,12 +238,38 @@ export function ChatInput({
|
||||||
? "Agent is thinking..."
|
? "Agent is thinking..."
|
||||||
: placeholder
|
: 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)}
|
disabled={loading || (disabled && !isAgentRunning)}
|
||||||
rows={1}
|
rows={1}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="absolute right-2 bottom-2 flex items-center space-x-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 && (
|
{onFileBrowse && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -143,7 +290,7 @@ export function ChatInput({
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8 rounded-full"
|
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'}
|
aria-label={isAgentRunning ? 'Stop agent' : 'Send message'}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
|
|
Loading…
Reference in New Issue