mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 16:43:16 +00:00
Refactor S3 storage implementation and enhance file handling
- Improved code readability by reorganizing imports and formatting. - Updated S3Storage class to handle file uploads and downloads more efficiently. - Added a new function to generate image URLs based on settings. - Enhanced file listing and processing methods for better error handling. - Introduced a FileUpload component for improved file upload experience in the frontend. - Updated agent management components to support image uploads and previews. - Added new SVG assets for UI enhancements. - Modified API client to support FormData for file uploads.
This commit is contained in:
1
frontend/src/assets/cross.svg
Normal file
1
frontend/src/assets/cross.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
|
After Width: | Height: | Size: 262 B |
3
frontend/src/assets/images.svg
Normal file
3
frontend/src/assets/images.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="40" height="39" viewBox="0 0 40 39" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9477 3.02295H33.8827C35.898 3.02295 37.5388 4.6819 37.5388 6.71923V22.9819C37.5388 25.0193 35.898 26.6782 33.8827 26.6782H11.9477C9.9328 26.6782 8.29192 25.0193 8.29192 22.9819V6.71923C8.29192 4.6819 9.9328 3.02295 11.9477 3.02295ZM33.8827 5.97992H11.9477C11.5442 5.97992 11.2167 6.31098 11.2167 6.71916V20.6741L15.2527 16.595C16.2515 15.5839 17.8791 15.5839 18.8792 16.595L20.6486 18.3795L26.0799 11.7888C26.5653 11.2003 27.276 10.8603 28.0335 10.856C28.7953 10.8735 29.5046 11.1841 29.9946 11.765L34.614 17.2147V6.71923C34.614 6.31104 34.2866 5.97992 33.8827 5.97992ZM6.40446 25.1242C7.16068 27.3803 9.243 28.8957 11.584 28.8957H32.8128L31.4954 33.1312C31.1223 34.5715 29.7916 35.5487 28.3352 35.5487C28.051 35.5485 27.768 35.5117 27.4929 35.4393L4.88059 29.3169C3.13614 28.8305 2.09642 27.0048 2.55267 25.2438L6.10025 13.2714V23.3516C6.10025 23.8543 6.17497 24.3567 6.35333 24.9542L6.40446 25.1242ZM18.53 10.4151C18.53 12.0459 17.2186 13.3721 15.6055 13.3721C13.9926 13.3721 12.6808 12.0458 12.6808 10.4151C12.6808 8.78445 13.9925 7.45815 15.6055 7.45815C17.2186 7.45815 18.53 8.78438 18.53 10.4151Z" fill="#A3A3A3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
229
frontend/src/components/FileUpload.tsx
Normal file
229
frontend/src/components/FileUpload.tsx
Normal file
@@ -0,0 +1,229 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import Cross from '../assets/cross.svg';
|
||||
import ImagesIcon from '../assets/images.svg';
|
||||
|
||||
interface FileUploadProps {
|
||||
onUpload: (files: File[]) => void;
|
||||
onRemove?: (file: File) => void;
|
||||
multiple?: boolean;
|
||||
maxFiles?: number;
|
||||
maxSize?: number; // in bytes
|
||||
accept?: Record<string, string[]>; // e.g. { 'image/*': ['.png', '.jpg'] }
|
||||
showPreview?: boolean;
|
||||
previewSize?: number;
|
||||
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
activeClassName?: string;
|
||||
acceptClassName?: string;
|
||||
rejectClassName?: string;
|
||||
|
||||
uploadText?: string | { text: string; colorClass?: string }[];
|
||||
dragActiveText?: string;
|
||||
fileTypeText?: string;
|
||||
sizeLimitText?: string;
|
||||
|
||||
disabled?: boolean;
|
||||
validator?: (file: File) => { isValid: boolean; error?: string };
|
||||
}
|
||||
|
||||
export const FileUpload = ({
|
||||
onUpload,
|
||||
onRemove,
|
||||
multiple = false,
|
||||
maxFiles = 1,
|
||||
maxSize = 5 * 1024 * 1024,
|
||||
accept = { 'image/*': ['.jpeg', '.png', '.jpg'] },
|
||||
showPreview = false,
|
||||
previewSize = 80,
|
||||
children,
|
||||
className = 'border-2 border-dashed rounded-3xl p-6 text-center cursor-pointer transition-colors border-silver dark:border-[#7E7E7E]',
|
||||
activeClassName = 'border-blue-500 bg-blue-50',
|
||||
acceptClassName = 'border-green-500 dark:border-green-500 bg-green-50 dark:bg-green-50/10',
|
||||
rejectClassName = 'border-red-500 bg-red-50 dark:bg-red-500/10 dark:border-red-500',
|
||||
uploadText = 'Click to upload or drag and drop',
|
||||
dragActiveText = 'Drop the files here',
|
||||
fileTypeText = 'PNG, JPG, JPEG up to',
|
||||
sizeLimitText = 'MB',
|
||||
disabled = false,
|
||||
validator,
|
||||
}: FileUploadProps) => {
|
||||
const [errors, setErrors] = useState<string[]>([]);
|
||||
const [preview, setPreview] = useState<string | null>(null);
|
||||
const [currentFile, setCurrentFile] = useState<File | null>(null);
|
||||
|
||||
const validateFile = (file: File) => {
|
||||
const defaultValidation = {
|
||||
isValid: true,
|
||||
error: '',
|
||||
};
|
||||
|
||||
if (validator) {
|
||||
const customValidation = validator(file);
|
||||
if (!customValidation.isValid) {
|
||||
return customValidation;
|
||||
}
|
||||
}
|
||||
|
||||
if (file.size > maxSize) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: `File exceeds ${maxSize / 1024 / 1024}MB limit`,
|
||||
};
|
||||
}
|
||||
|
||||
return defaultValidation;
|
||||
};
|
||||
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: File[], fileRejections: any[]) => {
|
||||
setErrors([]);
|
||||
|
||||
if (fileRejections.length > 0) {
|
||||
const newErrors = fileRejections
|
||||
.map(({ errors }) => errors.map((e: any) => e.message))
|
||||
.flat();
|
||||
setErrors(newErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
const validationResults = acceptedFiles.map(validateFile);
|
||||
const invalidFiles = validationResults.filter((r) => !r.isValid);
|
||||
|
||||
if (invalidFiles.length > 0) {
|
||||
setErrors(invalidFiles.map((f) => f.error!));
|
||||
return;
|
||||
}
|
||||
|
||||
const filesToUpload = multiple ? acceptedFiles : [acceptedFiles[0]];
|
||||
onUpload(filesToUpload);
|
||||
|
||||
const file = multiple ? acceptedFiles[0] : acceptedFiles[0];
|
||||
setCurrentFile(file);
|
||||
|
||||
if (showPreview && file.type.startsWith('image/')) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => setPreview(reader.result as string);
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
},
|
||||
[onUpload, multiple, maxSize, validator],
|
||||
);
|
||||
|
||||
const {
|
||||
getRootProps,
|
||||
getInputProps,
|
||||
isDragActive,
|
||||
isDragAccept,
|
||||
isDragReject,
|
||||
} = useDropzone({
|
||||
onDrop,
|
||||
multiple,
|
||||
maxFiles,
|
||||
maxSize,
|
||||
accept,
|
||||
disabled,
|
||||
});
|
||||
|
||||
const currentClassName = twMerge(
|
||||
'border-2 border-dashed rounded-3xl p-8 text-center cursor-pointer transition-colors border-silver dark:border-[#7E7E7E]',
|
||||
className,
|
||||
isDragActive && activeClassName,
|
||||
isDragAccept && acceptClassName,
|
||||
isDragReject && rejectClassName,
|
||||
disabled && 'opacity-50 cursor-not-allowed',
|
||||
);
|
||||
|
||||
const handleRemove = () => {
|
||||
setPreview(null);
|
||||
setCurrentFile(null);
|
||||
if (onRemove && currentFile) onRemove(currentFile);
|
||||
};
|
||||
|
||||
const renderPreview = () => (
|
||||
<div
|
||||
className="relative"
|
||||
style={{ width: previewSize, height: previewSize }}
|
||||
>
|
||||
<img
|
||||
src={preview ?? undefined}
|
||||
alt="preview"
|
||||
className="h-full w-full rounded-md object-cover"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRemove();
|
||||
}}
|
||||
className="absolute -right-2 -top-2 rounded-full bg-[#7D54D1] p-1 transition-colors hover:bg-[#714cbc]"
|
||||
>
|
||||
<img src={Cross} alt="remove" className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderUploadText = () => {
|
||||
if (Array.isArray(uploadText)) {
|
||||
return (
|
||||
<p className="text-sm font-semibold">
|
||||
{uploadText.map((segment, i) => (
|
||||
<span key={i} className={segment.colorClass || ''}>
|
||||
{segment.text}
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return <p className="text-sm font-semibold">{uploadText}</p>;
|
||||
};
|
||||
|
||||
const defaultContent = (
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
{showPreview && preview ? (
|
||||
renderPreview()
|
||||
) : (
|
||||
<div
|
||||
style={{ width: previewSize, height: previewSize }}
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
<img src={ImagesIcon} className="h-10 w-10" />
|
||||
</div>
|
||||
)}
|
||||
<div className="text-center">
|
||||
<div className="text-sm font-medium">
|
||||
{isDragActive ? (
|
||||
<p className="text-sm font-semibold">{dragActiveText}</p>
|
||||
) : (
|
||||
renderUploadText()
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-[#A3A3A3]">
|
||||
{fileTypeText} {maxSize / 1024 / 1024}
|
||||
{sizeLimitText}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div {...getRootProps({ className: currentClassName })}>
|
||||
<input {...getInputProps()} />
|
||||
{children || defaultContent}
|
||||
{errors.length > 0 && (
|
||||
<div className="absolute left-0 right-0 mt-[2px] px-4 text-xs text-red-600">
|
||||
{errors.map((error, i) => (
|
||||
<p key={i} className="truncate">
|
||||
{error}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user