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";
|
"use client";
|
||||||
|
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
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 { useParams, useRouter } from "next/navigation";
|
||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { useDropzone } from "react-dropzone";
|
import { useDropzone } from "react-dropzone";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
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
|
// Grid pattern component inspired by Aceternity UI
|
||||||
function GridPattern() {
|
function GridPattern() {
|
||||||
|
@ -34,14 +39,13 @@ function GridPattern() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FileUploader() {
|
export default function FileUploader() {
|
||||||
// Use the useParams hook to get the params
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const search_space_id = params.search_space_id as string;
|
const search_space_id = params.search_space_id as string;
|
||||||
|
|
||||||
const [files, setFiles] = useState<File[]>([]);
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
const [uploadProgress, setUploadProgress] = useState(0);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
// Audio files are always supported (using whisper)
|
// Audio files are always supported (using whisper)
|
||||||
const audioFileTypes = {
|
const audioFileTypes = {
|
||||||
|
@ -204,7 +208,6 @@ export default function FileUploader() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const acceptedFileTypes = getAcceptedFileTypes();
|
const acceptedFileTypes = getAcceptedFileTypes();
|
||||||
|
|
||||||
const supportedExtensions = Array.from(new Set(Object.values(acceptedFileTypes).flat())).sort();
|
const supportedExtensions = Array.from(new Set(Object.values(acceptedFileTypes).flat())).sort();
|
||||||
|
|
||||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||||
|
@ -215,12 +218,10 @@ export default function FileUploader() {
|
||||||
onDrop,
|
onDrop,
|
||||||
accept: acceptedFileTypes,
|
accept: acceptedFileTypes,
|
||||||
maxSize: 50 * 1024 * 1024, // 50MB
|
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) => {
|
const removeFile = (index: number) => {
|
||||||
setFiles((prevFiles) => prevFiles.filter((_, i) => i !== index));
|
setFiles((prevFiles) => prevFiles.filter((_, i) => i !== index));
|
||||||
};
|
};
|
||||||
|
@ -235,6 +236,7 @@ export default function FileUploader() {
|
||||||
|
|
||||||
const handleUpload = async () => {
|
const handleUpload = async () => {
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
|
setUploadProgress(0);
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
|
@ -244,12 +246,16 @@ export default function FileUploader() {
|
||||||
formData.append("search_space_id", search_space_id);
|
formData.append("search_space_id", search_space_id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// toast("File Upload", {
|
// Simulate progress for better UX
|
||||||
// description: "Files Uploading Initiated",
|
const progressInterval = setInterval(() => {
|
||||||
// })
|
setUploadProgress((prev) => {
|
||||||
|
if (prev >= 90) return prev;
|
||||||
|
return prev + Math.random() * 10;
|
||||||
|
});
|
||||||
|
}, 200);
|
||||||
|
|
||||||
const response = await fetch(
|
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",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -259,6 +265,9 @@ export default function FileUploader() {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
clearInterval(progressInterval);
|
||||||
|
setUploadProgress(100);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Upload failed");
|
throw new Error("Upload failed");
|
||||||
}
|
}
|
||||||
|
@ -272,31 +281,15 @@ export default function FileUploader() {
|
||||||
router.push(`/dashboard/${search_space_id}/documents`);
|
router.push(`/dashboard/${search_space_id}/documents`);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setIsUploading(false);
|
setIsUploading(false);
|
||||||
|
setUploadProgress(0);
|
||||||
toast("Upload Error", {
|
toast("Upload Error", {
|
||||||
description: `Error uploading files: ${error.message}`,
|
description: `Error uploading files: ${error.message}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const mainVariant = {
|
const getTotalFileSize = () => {
|
||||||
initial: {
|
return files.reduce((total, file) => total + file.size, 0);
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
animate: {
|
|
||||||
x: 20,
|
|
||||||
y: -20,
|
|
||||||
opacity: 0.9,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const secondaryVariant = {
|
|
||||||
initial: {
|
|
||||||
opacity: 0,
|
|
||||||
},
|
|
||||||
animate: {
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const containerVariants = {
|
const containerVariants = {
|
||||||
|
@ -326,251 +319,252 @@ export default function FileUploader() {
|
||||||
return (
|
return (
|
||||||
<div className="grow flex items-center justify-center p-4 md:p-8">
|
<div className="grow flex items-center justify-center p-4 md:p-8">
|
||||||
<motion.div
|
<motion.div
|
||||||
className="w-full max-w-3xl mx-auto"
|
className="w-full max-w-4xl mx-auto space-y-6"
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate="visible"
|
animate="visible"
|
||||||
variants={containerVariants}
|
variants={containerVariants}
|
||||||
>
|
>
|
||||||
<motion.div
|
{/* Header Card */}
|
||||||
className="bg-background rounded-xl shadow-lg overflow-hidden border border-border"
|
<motion.div variants={itemVariants}>
|
||||||
variants={itemVariants}
|
<Card>
|
||||||
>
|
<CardHeader>
|
||||||
<motion.div
|
<CardTitle className="flex items-center gap-2">
|
||||||
className="p-10 group/file block rounded-lg cursor-pointer w-full relative overflow-hidden"
|
<Upload className="h-5 w-5" />
|
||||||
whileHover="animate"
|
Upload Documents
|
||||||
onClick={handleClick}
|
</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 */}
|
{/* 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 />
|
<GridPattern />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative z-10">
|
<CardContent className="p-10 relative z-10">
|
||||||
{/* Dropzone area */}
|
<div
|
||||||
<div {...getRootProps()} className="flex flex-col items-center justify-center">
|
{...getRootProps()}
|
||||||
<input {...getInputProps()} ref={fileInputRef} className="hidden" />
|
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"
|
||||||
|
|
||||||
<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 }}
|
|
||||||
>
|
|
||||||
{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 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
></motion.div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* File list section */}
|
|
||||||
<AnimatePresence mode="wait">
|
|
||||||
{files.length > 0 && (
|
|
||||||
<motion.div
|
|
||||||
className="px-8 pb-8"
|
|
||||||
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">
|
<input {...getInputProps()} className="hidden" />
|
||||||
<h3 className="font-medium">Selected Files ({files.length})</h3>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
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
|
{isDragActive ? (
|
||||||
setTimeout(() => {
|
<motion.div
|
||||||
const event = new Event("resize");
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
window.dispatchEvent(event);
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
}, 350);
|
className="flex flex-col items-center gap-4"
|
||||||
}}
|
|
||||||
disabled={isUploading}
|
|
||||||
>
|
>
|
||||||
Clear all
|
<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 }}
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
<div className="space-y-4 max-h-[400px] overflow-y-auto pr-2 custom-scrollbar">
|
{/* File List Card */}
|
||||||
<AnimatePresence>
|
<AnimatePresence mode="wait">
|
||||||
{files.map((file, index) => (
|
{files.length > 0 && (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={`${file.name}-${index}`}
|
variants={itemVariants}
|
||||||
layoutId={index === 0 ? "file-upload" : `file-upload-${index}`}
|
initial={{ opacity: 0, height: 0 }}
|
||||||
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"
|
animate={{ opacity: 1, height: "auto" }}
|
||||||
initial="hidden"
|
exit={{ opacity: 0, height: 0 }}
|
||||||
animate="visible"
|
transition={{ duration: 0.3 }}
|
||||||
exit="exit"
|
>
|
||||||
variants={fileItemVariants}
|
<Card>
|
||||||
>
|
<CardHeader>
|
||||||
<div className="flex justify-between w-full items-center gap-4">
|
<div className="flex items-center justify-between">
|
||||||
<motion.p
|
<div>
|
||||||
initial={{ opacity: 0 }}
|
<CardTitle>Selected Files ({files.length})</CardTitle>
|
||||||
animate={{ opacity: 1 }}
|
<CardDescription>
|
||||||
layout
|
Total size: {formatFileSize(getTotalFileSize())}
|
||||||
className="text-base text-neutral-700 dark:text-neutral-300 truncate max-w-xs font-medium"
|
</CardDescription>
|
||||||
>
|
</div>
|
||||||
{file.name}
|
<Button
|
||||||
</motion.p>
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setFiles([])}
|
||||||
|
disabled={isUploading}
|
||||||
|
>
|
||||||
|
Clear all
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</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}`}
|
||||||
|
variants={fileItemVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
exit="exit"
|
||||||
|
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)}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{file.type || "Unknown type"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<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"
|
|
||||||
>
|
|
||||||
{formatFileSize(file.size)}
|
|
||||||
</motion.p>
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => removeFile(index)}
|
onClick={() => removeFile(index)}
|
||||||
disabled={isUploading}
|
disabled={isUploading}
|
||||||
className="h-8 w-8"
|
className="h-8 w-8"
|
||||||
aria-label={`Remove ${file.name}`}
|
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
className="mt-6"
|
|
||||||
initial={{ opacity: 0, y: 10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.2 }}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
className="w-full py-6 text-base font-medium"
|
|
||||||
onClick={handleUpload}
|
|
||||||
disabled={isUploading || files.length === 0}
|
|
||||||
>
|
|
||||||
{isUploading ? (
|
|
||||||
<motion.div
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
animate={{ rotate: 360 }}
|
|
||||||
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
|
|
||||||
>
|
|
||||||
<Upload className="h-5 w-5" />
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<span>Uploading...</span>
|
))}
|
||||||
</motion.div>
|
</AnimatePresence>
|
||||||
) : (
|
</div>
|
||||||
<motion.div
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
whileHover={{ scale: 1.02 }}
|
|
||||||
whileTap={{ scale: 0.98 }}
|
|
||||||
>
|
|
||||||
<CheckCircle2 className="h-5 w-5" />
|
|
||||||
<span>
|
|
||||||
Upload {files.length} {files.length === 1 ? "file" : "files"}
|
|
||||||
</span>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
{/* File type information */}
|
{isUploading && (
|
||||||
<motion.div className="px-8 pb-8" variants={itemVariants}>
|
<motion.div
|
||||||
<div className="p-4 bg-muted rounded-lg">
|
initial={{ opacity: 0, y: 10 }}
|
||||||
<div className="flex items-center gap-2 mb-3">
|
animate={{ opacity: 1, y: 0 }}
|
||||||
<Tag className="h-4 w-4 text-primary" />
|
className="mt-6 space-y-3"
|
||||||
<p className="text-sm font-medium">Supported file types:</p>
|
>
|
||||||
</div>
|
<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 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className="w-full py-6 text-base font-medium"
|
||||||
|
onClick={handleUpload}
|
||||||
|
disabled={isUploading || files.length === 0}
|
||||||
|
>
|
||||||
|
{isUploading ? (
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
animate={{ rotate: 360 }}
|
||||||
|
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
|
||||||
|
>
|
||||||
|
<Upload className="h-5 w-5" />
|
||||||
|
</motion.div>
|
||||||
|
<span>Uploading...</span>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
>
|
||||||
|
<CheckCircle2 className="h-5 w-5" />
|
||||||
|
<span>
|
||||||
|
Upload {files.length} {files.length === 1 ? "file" : "files"}
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* 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">
|
<div className="flex flex-wrap gap-2">
|
||||||
{supportedExtensions.map((ext) => (
|
{supportedExtensions.map((ext) => (
|
||||||
<motion.span
|
<Badge key={ext} variant="outline" className="text-xs">
|
||||||
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
|
|
||||||
>
|
|
||||||
{ext}
|
{ext}
|
||||||
</motion.span>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CardContent>
|
||||||
</motion.div>
|
</Card>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue