upload file message input

This commit is contained in:
marko-kraemer 2025-04-11 14:04:28 +01:00
parent 9cd703788e
commit 1d22b51ac6
1 changed files with 153 additions and 6 deletions

View File

@ -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 ? (