diff --git a/src/app/(agent)/agent/edit/components/DynamicField.tsx b/src/app/(agent)/agent/edit/components/DynamicField.tsx
index b122a3a..96e39fe 100644
--- a/src/app/(agent)/agent/edit/components/DynamicField.tsx
+++ b/src/app/(agent)/agent/edit/components/DynamicField.tsx
@@ -1,6 +1,7 @@
'use client';
-import React from 'react';
+import React, { useRef, useState, useCallback } from 'react';
+import Image from 'next/image';
import Select, { MultiValue, StylesConfig } from 'react-select';
import { ProfileField } from '@/services/profile-sections.service';
import { FormInput } from './FormInput';
@@ -139,7 +140,7 @@ export default function DynamicField({ field, value, onChange, error }: DynamicF
case 'TAG_INPUT':
return (
-
);
+ case 'FILE':
+ case 'FILE_UPLOAD':
+ return (
+
+ );
+
default:
return (
-
+
{field.name}
{field.isRequired && *}
@@ -199,17 +210,33 @@ function CheckboxGroup({ field, value, onChange }: CheckboxGroupProps) {
className="grid gap-2"
style={{ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` }}
>
- {field.options?.map((option) => (
-
- handleToggle(option.value)}
- className="w-4 h-4 rounded border-[#00293D]/20 text-[#E58625] focus:ring-[#E58625]"
- />
- {option.label}
-
- ))}
+ {field.options?.map((option) => {
+ const isChecked = value.includes(option.value);
+ return (
+
+
+
handleToggle(option.value)}
+ className="sr-only"
+ />
+
+ {isChecked && (
+
+ )}
+
+
+ {option.label}
+
+ );
+ })}
);
@@ -225,7 +252,7 @@ interface RadioGroupProps {
function RadioGroup({ field, value, onChange }: RadioGroupProps) {
return (
-
+
{field.name}
{field.isRequired && *}
@@ -339,7 +366,7 @@ function MultiSelect({ field, value, onChange }: MultiSelectProps) {
return (
-
+
{field.name}
{field.isRequired && *}
@@ -375,7 +402,7 @@ function RangeSlider({ field, value, onChange }: RangeSliderProps) {
return (
-
+
{field.name}
{field.isRequired && *}
@@ -400,3 +427,316 @@ function RangeSlider({ field, value, onChange }: RangeSliderProps) {
);
}
+
+// File Upload Types
+interface UploadedFile {
+ id: string;
+ name: string;
+ size: number;
+ type: string;
+ url: string;
+ uploadedAt: string;
+}
+
+interface UploadingFile {
+ id: string;
+ name: string;
+ size: number;
+ progress: number;
+ error?: string;
+}
+
+// File Upload Component
+interface FileUploadProps {
+ field: ProfileField;
+ value: UploadedFile[];
+ onChange: (value: UploadedFile[]) => void;
+}
+
+function FileUpload({ field, value, onChange }: FileUploadProps) {
+ const fileInputRef = useRef(null);
+ const [isDragging, setIsDragging] = useState(false);
+ const [uploadingFiles, setUploadingFiles] = useState([]);
+
+ // Get allowed formats from field config or use defaults
+ const allowedFormats = field.validation?.allowedFormats || ['PDF', 'DOCX', 'JPG', 'PNG'];
+ const maxFileSize = field.validation?.maxFileSize || 10; // MB
+ const acceptString = allowedFormats.map(f => `.${f.toLowerCase()}`).join(',');
+
+ 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 formatUploadTime = (dateString: string): string => {
+ const date = new Date(dateString);
+ const now = new Date();
+ const diffMs = now.getTime() - date.getTime();
+ const diffMins = Math.floor(diffMs / 60000);
+
+ if (diffMins < 1) return 'Uploaded just now';
+ if (diffMins < 60) return `Uploaded ${diffMins} min ago`;
+ const diffHours = Math.floor(diffMins / 60);
+ if (diffHours < 24) return `Uploaded ${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
+ return `Uploaded on ${date.toLocaleDateString()}`;
+ };
+
+ const uploadFile = async (file: File, tempId: string) => {
+ try {
+ // Import dynamically to avoid SSR issues
+ const { uploadService } = await import('@/services/upload.service');
+
+ const uploadedFile = await uploadService.uploadFile(
+ file,
+ 'documents',
+ (progress) => {
+ setUploadingFiles(prev =>
+ prev.map(f =>
+ f.id === tempId ? { ...f, progress: progress.percentage } : f
+ )
+ );
+ }
+ );
+
+ // Remove from uploading and add to uploaded
+ setUploadingFiles(prev => prev.filter(f => f.id !== tempId));
+ onChange([...value, uploadedFile]);
+ } catch (error) {
+ console.error('Upload failed:', error);
+ setUploadingFiles(prev =>
+ prev.map(f =>
+ f.id === tempId ? { ...f, error: 'Upload failed. Please try again.' } : f
+ )
+ );
+ }
+ };
+
+ const handleFiles = useCallback(async (files: FileList | null) => {
+ if (!files) return;
+
+ const validFiles: { file: File; tempId: string }[] = [];
+
+ Array.from(files).forEach(file => {
+ // Check file size
+ if (file.size > maxFileSize * 1024 * 1024) {
+ alert(`File "${file.name}" exceeds the maximum size of ${maxFileSize}MB`);
+ return;
+ }
+
+ // Check file type
+ const extension = file.name.split('.').pop()?.toUpperCase() || '';
+ if (!allowedFormats.includes(extension)) {
+ alert(`File type ".${extension}" is not allowed. Allowed formats: ${allowedFormats.join(', ')}`);
+ return;
+ }
+
+ const tempId = `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+ validFiles.push({ file, tempId });
+ });
+
+ if (validFiles.length === 0) return;
+
+ // Add files to uploading state
+ setUploadingFiles(prev => [
+ ...prev,
+ ...validFiles.map(({ file, tempId }) => ({
+ id: tempId,
+ name: file.name,
+ size: file.size,
+ progress: 0,
+ })),
+ ]);
+
+ // Upload each file
+ for (const { file, tempId } of validFiles) {
+ await uploadFile(file, tempId);
+ }
+ }, [value, onChange, maxFileSize, allowedFormats]);
+
+ const handleDrop = useCallback((e: React.DragEvent) => {
+ e.preventDefault();
+ setIsDragging(false);
+ handleFiles(e.dataTransfer.files);
+ }, [handleFiles]);
+
+ const handleDragOver = useCallback((e: React.DragEvent) => {
+ e.preventDefault();
+ setIsDragging(true);
+ }, []);
+
+ const handleDragLeave = useCallback((e: React.DragEvent) => {
+ e.preventDefault();
+ setIsDragging(false);
+ }, []);
+
+ const handleRemoveFile = async (fileId: string) => {
+ try {
+ // Import dynamically to avoid SSR issues
+ const { uploadService } = await import('@/services/upload.service');
+
+ // Try to delete from S3
+ await uploadService.deleteFile(fileId);
+ } catch (error) {
+ console.error('Failed to delete file from server:', error);
+ // Continue to remove from local state even if server delete fails
+ }
+
+ onChange(value.filter(f => f.id !== fileId));
+ };
+
+ const handleRemoveUploadingFile = (tempId: string) => {
+ setUploadingFiles(prev => prev.filter(f => f.id !== tempId));
+ };
+
+ const handleSelectFiles = () => {
+ fileInputRef.current?.click();
+ };
+
+ const isUploading = uploadingFiles.length > 0;
+
+ return (
+
+ {/* Upload Area */}
+
+
{
+ handleFiles(e.target.files);
+ e.target.value = ''; // Reset input
+ }}
+ className="hidden"
+ disabled={isUploading}
+ />
+
+
+
+
+
+ Click to upload or drag and drop
+
+
+ {allowedFormats.join(', ')} (max. {maxFileSize}MB)
+
+
+
+
+
+
+ {/* Uploading Files Progress */}
+ {uploadingFiles.length > 0 && (
+
+ {uploadingFiles.map((file) => (
+
+
+
+ {file.name}
+
+
+
+ {file.error ? (
+
{file.error}
+ ) : (
+
+ )}
+
+ {file.error ? '' : `${file.progress}% • ${formatFileSize(file.size)}`}
+
+
+ ))}
+
+ )}
+
+ {/* Attached Documents List */}
+ {value.length > 0 && (
+
+
+
+ Attached Documents
+
+
+ {value.map((file) => (
+
+
+
+ {file.url ? (
+
+ {file.name}
+
+ ) : (
+
+ {file.name}
+
+ )}
+
+
+
+ {formatFileSize(file.size)} • {formatUploadTime(file.uploadedAt)}
+
+
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/src/app/(agent)/agent/edit/components/FormInput.tsx b/src/app/(agent)/agent/edit/components/FormInput.tsx
index 128c14f..90ec3f1 100644
--- a/src/app/(agent)/agent/edit/components/FormInput.tsx
+++ b/src/app/(agent)/agent/edit/components/FormInput.tsx
@@ -16,7 +16,7 @@ interface FormInputProps {
export function FormInput({ label, value, onChange, placeholder, type = 'text', disabled, required, error, min, max }: FormInputProps) {
return (
-
+
{label}
{required && *}
diff --git a/src/app/(agent)/agent/edit/components/FormSelect.tsx b/src/app/(agent)/agent/edit/components/FormSelect.tsx
index 26c99e1..d1f558a 100644
--- a/src/app/(agent)/agent/edit/components/FormSelect.tsx
+++ b/src/app/(agent)/agent/edit/components/FormSelect.tsx
@@ -11,7 +11,7 @@ interface FormSelectProps {
export function FormSelect({ label, value, onChange, options, placeholder }: FormSelectProps) {
return (
-
+
{label}