mirror of
https://github.com/MODSetter/SurfSense.git
synced 2025-09-02 02:29:08 +00:00
Improved upload page
This commit is contained in:
parent
a7c1fd49be
commit
49a5d048bf
1 changed files with 239 additions and 245 deletions
|
@ -1,12 +1,17 @@
|
|||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Calendar, CheckCircle2, FileType, Tag, Upload, X } from "lucide-react";
|
||||
import { CheckCircle2, FileType, Info, Tag, Upload, X } from "lucide-react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { toast } from "sonner";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
// Grid pattern component inspired by Aceternity UI
|
||||
function GridPattern() {
|
||||
|
@ -34,14 +39,13 @@ function GridPattern() {
|
|||
}
|
||||
|
||||
export default function FileUploader() {
|
||||
// Use the useParams hook to get the params
|
||||
const params = useParams();
|
||||
const search_space_id = params.search_space_id as string;
|
||||
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const router = useRouter();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Audio files are always supported (using whisper)
|
||||
const audioFileTypes = {
|
||||
|
@ -204,7 +208,6 @@ export default function FileUploader() {
|
|||
};
|
||||
|
||||
const acceptedFileTypes = getAcceptedFileTypes();
|
||||
|
||||
const supportedExtensions = Array.from(new Set(Object.values(acceptedFileTypes).flat())).sort();
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
|
@ -215,12 +218,10 @@ export default function FileUploader() {
|
|||
onDrop,
|
||||
accept: acceptedFileTypes,
|
||||
maxSize: 50 * 1024 * 1024, // 50MB
|
||||
noClick: false, // Ensure clicking is enabled
|
||||
noKeyboard: false, // Ensure keyboard navigation is enabled
|
||||
});
|
||||
|
||||
const handleClick = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const removeFile = (index: number) => {
|
||||
setFiles((prevFiles) => prevFiles.filter((_, i) => i !== index));
|
||||
};
|
||||
|
@ -235,6 +236,7 @@ export default function FileUploader() {
|
|||
|
||||
const handleUpload = async () => {
|
||||
setIsUploading(true);
|
||||
setUploadProgress(0);
|
||||
|
||||
const formData = new FormData();
|
||||
files.forEach((file) => {
|
||||
|
@ -244,12 +246,16 @@ export default function FileUploader() {
|
|||
formData.append("search_space_id", search_space_id);
|
||||
|
||||
try {
|
||||
// toast("File Upload", {
|
||||
// description: "Files Uploading Initiated",
|
||||
// })
|
||||
// Simulate progress for better UX
|
||||
const progressInterval = setInterval(() => {
|
||||
setUploadProgress((prev) => {
|
||||
if (prev >= 90) return prev;
|
||||
return prev + Math.random() * 10;
|
||||
});
|
||||
}, 200);
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL!}/api/v1/documents/fileupload`,
|
||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/fileupload`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
@ -259,6 +265,9 @@ export default function FileUploader() {
|
|||
}
|
||||
);
|
||||
|
||||
clearInterval(progressInterval);
|
||||
setUploadProgress(100);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Upload failed");
|
||||
}
|
||||
|
@ -272,31 +281,15 @@ export default function FileUploader() {
|
|||
router.push(`/dashboard/${search_space_id}/documents`);
|
||||
} catch (error: any) {
|
||||
setIsUploading(false);
|
||||
setUploadProgress(0);
|
||||
toast("Upload Error", {
|
||||
description: `Error uploading files: ${error.message}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const mainVariant = {
|
||||
initial: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
animate: {
|
||||
x: 20,
|
||||
y: -20,
|
||||
opacity: 0.9,
|
||||
},
|
||||
};
|
||||
|
||||
const secondaryVariant = {
|
||||
initial: {
|
||||
opacity: 0,
|
||||
},
|
||||
animate: {
|
||||
opacity: 1,
|
||||
},
|
||||
const getTotalFileSize = () => {
|
||||
return files.reduce((total, file) => total + file.size, 0);
|
||||
};
|
||||
|
||||
const containerVariants = {
|
||||
|
@ -326,184 +319,185 @@ export default function FileUploader() {
|
|||
return (
|
||||
<div className="grow flex items-center justify-center p-4 md:p-8">
|
||||
<motion.div
|
||||
className="w-full max-w-3xl mx-auto"
|
||||
className="w-full max-w-4xl mx-auto space-y-6"
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={containerVariants}
|
||||
>
|
||||
<motion.div
|
||||
className="bg-background rounded-xl shadow-lg overflow-hidden border border-border"
|
||||
variants={itemVariants}
|
||||
>
|
||||
<motion.div
|
||||
className="p-10 group/file block rounded-lg cursor-pointer w-full relative overflow-hidden"
|
||||
whileHover="animate"
|
||||
onClick={handleClick}
|
||||
>
|
||||
{/* Header Card */}
|
||||
<motion.div variants={itemVariants}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Upload className="h-5 w-5" />
|
||||
Upload Documents
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Upload your files to make them searchable and accessible through AI-powered
|
||||
conversations.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Alert>
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Maximum file size: 50MB per file. Supported formats vary based on your ETL service
|
||||
configuration.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
{/* Upload Area Card */}
|
||||
<motion.div variants={itemVariants}>
|
||||
<Card className="relative overflow-hidden">
|
||||
{/* Grid background pattern */}
|
||||
<div className="absolute inset-0 [mask-image:radial-gradient(ellipse_at_center,white,transparent)]">
|
||||
<div className="absolute inset-0 [mask-image:radial-gradient(ellipse_at_center,white,transparent)] opacity-30">
|
||||
<GridPattern />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10">
|
||||
{/* Dropzone area */}
|
||||
<div {...getRootProps()} className="flex flex-col items-center justify-center">
|
||||
<input {...getInputProps()} ref={fileInputRef} className="hidden" />
|
||||
|
||||
<p className="relative z-20 font-sans font-bold text-neutral-700 dark:text-neutral-300 text-xl">
|
||||
Upload files
|
||||
</p>
|
||||
<p className="relative z-20 font-sans font-normal text-neutral-400 dark:text-neutral-400 text-base mt-2">
|
||||
Drag or drop your files here or click to upload
|
||||
</p>
|
||||
|
||||
<div className="relative w-full mt-10 max-w-xl mx-auto">
|
||||
{!files.length && (
|
||||
<motion.div
|
||||
layoutId="file-upload"
|
||||
variants={mainVariant}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 20,
|
||||
}}
|
||||
className="relative group-hover/file:shadow-2xl z-40 bg-white dark:bg-neutral-900 flex items-center justify-center h-32 mt-4 w-full max-w-[8rem] mx-auto rounded-md shadow-[0px_10px_50px_rgba(0,0,0,0.1)]"
|
||||
key="upload-icon"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
<CardContent className="p-10 relative z-10">
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className="flex flex-col items-center justify-center min-h-[300px] border-2 border-dashed border-muted-foreground/25 rounded-lg hover:border-primary/50 transition-colors cursor-pointer"
|
||||
>
|
||||
<input {...getInputProps()} className="hidden" />
|
||||
|
||||
{isDragActive ? (
|
||||
<motion.p
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="text-neutral-600 flex flex-col items-center"
|
||||
>
|
||||
Drop it
|
||||
<Upload className="h-4 w-4 text-neutral-600 dark:text-neutral-400 mt-2" />
|
||||
</motion.p>
|
||||
) : (
|
||||
<Upload className="h-8 w-8 text-neutral-600 dark:text-neutral-300" />
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{!files.length && (
|
||||
<motion.div
|
||||
variants={secondaryVariant}
|
||||
className="absolute opacity-0 border border-dashed border-primary inset-0 z-30 bg-transparent flex items-center justify-center h-32 mt-4 w-full max-w-[8rem] mx-auto rounded-md"
|
||||
key="upload-border"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="flex flex-col items-center gap-4"
|
||||
>
|
||||
<Upload className="h-12 w-12 text-primary" />
|
||||
<p className="text-lg font-medium text-primary">Drop files here</p>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
></motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
className="flex flex-col items-center gap-4"
|
||||
>
|
||||
<Upload className="h-12 w-12 text-muted-foreground" />
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-medium">Drag & drop files here</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">or click to browse</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* File list section */}
|
||||
{/* Fallback button for better accessibility */}
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const input = document.querySelector(
|
||||
'input[type="file"]'
|
||||
) as HTMLInputElement;
|
||||
if (input) input.click();
|
||||
}}
|
||||
>
|
||||
Browse Files
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
{/* File List Card */}
|
||||
<AnimatePresence mode="wait">
|
||||
{files.length > 0 && (
|
||||
<motion.div
|
||||
className="px-8 pb-8"
|
||||
variants={itemVariants}
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: "auto" }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h3 className="font-medium">Selected Files ({files.length})</h3>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Selected Files ({files.length})</CardTitle>
|
||||
<CardDescription>
|
||||
Total size: {formatFileSize(getTotalFileSize())}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
// Use AnimatePresence to properly handle the transition
|
||||
// This will ensure the file icon reappears properly
|
||||
setFiles([]);
|
||||
|
||||
// Force a re-render after animation completes
|
||||
setTimeout(() => {
|
||||
const event = new Event("resize");
|
||||
window.dispatchEvent(event);
|
||||
}, 350);
|
||||
}}
|
||||
onClick={() => setFiles([])}
|
||||
disabled={isUploading}
|
||||
>
|
||||
Clear all
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 max-h-[400px] overflow-y-auto pr-2 custom-scrollbar">
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3 max-h-[400px] overflow-y-auto pr-2 custom-scrollbar">
|
||||
<AnimatePresence>
|
||||
{files.map((file, index) => (
|
||||
<motion.div
|
||||
key={`${file.name}-${index}`}
|
||||
layoutId={index === 0 ? "file-upload" : `file-upload-${index}`}
|
||||
className="relative overflow-hidden z-40 bg-white dark:bg-neutral-900 flex flex-col items-start justify-start p-4 w-full mx-auto rounded-md shadow-sm border border-border"
|
||||
variants={fileItemVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
variants={fileItemVariants}
|
||||
>
|
||||
<div className="flex justify-between w-full items-center gap-4">
|
||||
<motion.p
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
layout
|
||||
className="text-base text-neutral-700 dark:text-neutral-300 truncate max-w-xs font-medium"
|
||||
>
|
||||
{file.name}
|
||||
</motion.p>
|
||||
<div className="flex items-center gap-2">
|
||||
<motion.p
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
layout
|
||||
className="rounded-lg px-2 py-1 w-fit flex-shrink-0 text-sm text-neutral-600 dark:bg-neutral-800 dark:text-white bg-neutral-100"
|
||||
className="flex items-center justify-between p-4 rounded-lg border bg-card hover:bg-accent/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<div className="flex-shrink-0">
|
||||
<FileType className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium truncate">{file.name}</p>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{formatFileSize(file.size)}
|
||||
</motion.p>
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{file.type || "Unknown type"}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => removeFile(index)}
|
||||
disabled={isUploading}
|
||||
className="h-8 w-8"
|
||||
aria-label={`Remove ${file.name}`}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex text-sm md:flex-row flex-col items-start md:items-center w-full mt-2 justify-between text-neutral-600 dark:text-neutral-400">
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
layout
|
||||
className="flex items-center gap-1 px-2 py-1 rounded-md bg-gray-100 dark:bg-neutral-800"
|
||||
>
|
||||
<FileType className="h-3 w-3" />
|
||||
<span>{file.type || "Unknown type"}</span>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
layout
|
||||
className="flex items-center gap-1 mt-2 md:mt-0"
|
||||
>
|
||||
<Calendar className="h-3 w-3" />
|
||||
<span>modified {new Date(file.lastModified).toLocaleDateString()}</span>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{isUploading && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="mt-6 space-y-3"
|
||||
>
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span>Uploading files...</span>
|
||||
<span>{Math.round(uploadProgress)}%</span>
|
||||
</div>
|
||||
<Progress value={uploadProgress} className="h-2" />
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<motion.div
|
||||
className="mt-6"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
|
@ -543,34 +537,34 @@ export default function FileUploader() {
|
|||
)}
|
||||
</Button>
|
||||
</motion.div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* File type information */}
|
||||
<motion.div className="px-8 pb-8" variants={itemVariants}>
|
||||
<div className="p-4 bg-muted rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Tag className="h-4 w-4 text-primary" />
|
||||
<p className="text-sm font-medium">Supported file types:</p>
|
||||
</div>
|
||||
{/* Supported File Types Card */}
|
||||
<motion.div variants={itemVariants}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Tag className="h-5 w-5" />
|
||||
Supported File Types
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
These file types are supported based on your current ETL service configuration.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{supportedExtensions.map((ext) => (
|
||||
<motion.span
|
||||
key={ext}
|
||||
className="px-2 py-1 bg-primary/10 text-primary text-xs rounded-full"
|
||||
whileHover={{ scale: 1.05, backgroundColor: "rgba(var(--primary), 0.2)" }}
|
||||
initial={{ opacity: 1 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 1 }}
|
||||
layout
|
||||
>
|
||||
<Badge key={ext} variant="outline" className="text-xs">
|
||||
{ext}
|
||||
</motion.span>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue