import { useCallback, useState } from 'react'; import { nanoid } from '@reduxjs/toolkit'; import { useDropzone } from 'react-dropzone'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import userService from '../api/services/userService'; import { getSessionToken } from '../utils/providerUtils'; import Dropdown from '../components/Dropdown'; import Input from '../components/Input'; import ToggleSwitch from '../components/ToggleSwitch'; import WrapperModal from '../modals/WrapperModal'; import { ActiveState, Doc } from '../models/misc'; import { getDocs } from '../preferences/preferenceApi'; import { selectSourceDocs, selectToken, setSelectedDocs, setSourceDocs, } from '../preferences/preferenceSlice'; import { IngestorDefaultConfigs, IngestorFormSchemas, getIngestorSchema, IngestorOption, } from '../upload/types/ingestor'; import { addUploadTask, updateUploadTask } from './uploadSlice'; import { FormField, IngestorConfig, IngestorType } from './types/ingestor'; import { FilePicker } from '../components/FilePicker'; import GoogleDrivePicker from '../components/GoogleDrivePicker'; import ChevronRight from '../assets/chevron-right.svg'; function Upload({ receivedFile = [], setModalState, isOnboarding, renderTab = null, close, onSuccessfulUpload = () => undefined, }: { receivedFile: File[]; setModalState: (state: ActiveState) => void; isOnboarding: boolean; renderTab: string | null; close: () => void; onSuccessfulUpload?: () => void; }) { const token = useSelector(selectToken); const [files, setfiles] = useState(receivedFile); const [activeTab, setActiveTab] = useState(true); const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); // File picker state const [selectedFiles, setSelectedFiles] = useState([]); const [selectedFolders, setSelectedFolders] = useState([]); const renderFormFields = () => { if (!ingestor.type) return null; const ingestorSchema = getIngestorSchema(ingestor.type as IngestorType); if (!ingestorSchema) return null; const schema: FormField[] = ingestorSchema.fields; const generalFields = schema.filter((field: FormField) => !field.advanced); const advancedFields = schema.filter((field: FormField) => field.advanced); return (
{generalFields.map((field: FormField) => renderField(field))}
{advancedFields.length > 0 && (

{advancedFields.map((field: FormField) => renderField(field))}
)}
); }; const renderField = (field: FormField) => { const isRequired = field.required ?? false; switch (field.type) { case 'string': return ( handleIngestorChange( field.name as keyof IngestorConfig['config'], e.target.value, ) } borderVariant="thin" required={isRequired} colorVariant="silver" labelBgClassName="bg-white dark:bg-charleston-green-2" /> ); case 'number': return ( handleIngestorChange( field.name as keyof IngestorConfig['config'], Number(e.target.value), ) } borderVariant="thin" required={isRequired} colorVariant="silver" labelBgClassName="bg-white dark:bg-charleston-green-2" /> ); case 'enum': return ( opt.value === ingestor.config[field.name as keyof typeof ingestor.config], ) || null } onSelect={(selected: { label: string; value: string }) => { handleIngestorChange( field.name as keyof IngestorConfig['config'], selected.value, ); }} size="w-full" rounded="3xl" placeholder={field.label} border="border" buttonClassName="border-silver bg-white dark:border-dim-gray dark:bg-[#222327]" optionsClassName="border-silver bg-white dark:border-dim-gray dark:bg-[#383838]" placeholderClassName="text-gray-400 dark:text-silver" contentSize="text-sm" /> ); case 'boolean': return ( { handleIngestorChange( field.name as keyof IngestorConfig['config'], checked, ); }} size="small" className={`mt-2 text-base`} /> ); case 'local_file_picker': return (
{t('modals.uploadDoc.choose')}

{t('modals.uploadDoc.selectedFiles')}

{files.map((file) => (

{file.name}

))} {files.length === 0 && (

{t('modals.uploadDoc.noFilesSelected')}

)}
); case 'remote_file_picker': return ( { setSelectedFiles(selectedFileIds); setSelectedFolders(selectedFolderIds); }} provider={ingestor.type as unknown as string} token={token} initialSelectedFiles={selectedFiles} initialSelectedFolders={selectedFolders} /> ); case 'google_drive_picker': return ( { setSelectedFiles(selectedFileIds); setSelectedFolders(selectedFolderIds); }} token={token} /> ); default: return null; } }; // New unified ingestor state const [ingestor, setIngestor] = useState(() => ({ type: null, name: '', config: {}, })); const { t } = useTranslation(); const dispatch = useDispatch(); const ingestorOptions: IngestorOption[] = IngestorFormSchemas.filter( (schema) => (schema.validate ? schema.validate() : true), ).map((schema) => ({ label: schema.label, value: schema.key, icon: schema.icon, heading: schema.heading, })); const sourceDocs = useSelector(selectSourceDocs); const resetUploaderState = useCallback(() => { setIngestor({ type: null, name: '', config: {} }); setfiles([]); setSelectedFiles([]); setSelectedFolders([]); setShowAdvancedOptions(false); }, []); const handleTaskFailure = useCallback( (clientTaskId: string, errorMessage?: string) => { dispatch( updateUploadTask({ id: clientTaskId, updates: { status: 'failed', errorMessage: errorMessage || t('attachments.uploadFailed'), }, }), ); }, [dispatch, t], ); const trackTraining = useCallback( (backendTaskId: string, clientTaskId: string) => { let timeoutId: number | null = null; const poll = () => { userService .getTaskStatus(backendTaskId, null) .then((response) => response.json()) .then(async (data) => { if (data.status === 'SUCCESS') { if (timeoutId !== null) { clearTimeout(timeoutId); timeoutId = null; } const docs = await getDocs(token); dispatch(setSourceDocs(docs)); if (Array.isArray(docs)) { const existingDocIds = new Set( (Array.isArray(sourceDocs) ? sourceDocs : []) .map((doc: Doc) => doc?.id) .filter((id): id is string => Boolean(id)), ); const newDoc = docs.find( (doc: Doc) => doc.id && !existingDocIds.has(doc.id), ); if (newDoc) { dispatch(setSelectedDocs([newDoc])); } } if (data.result?.limited) { dispatch( updateUploadTask({ id: clientTaskId, updates: { status: 'failed', progress: 100, errorMessage: t('modals.uploadDoc.progress.tokenLimit'), }, }), ); } else { dispatch( updateUploadTask({ id: clientTaskId, updates: { status: 'completed', progress: 100, errorMessage: undefined, }, }), ); onSuccessfulUpload?.(); } } else if (data.status === 'FAILURE') { if (timeoutId !== null) { clearTimeout(timeoutId); timeoutId = null; } handleTaskFailure(clientTaskId, data.result?.message); } else if (data.status === 'PROGRESS') { dispatch( updateUploadTask({ id: clientTaskId, updates: { status: 'training', progress: Math.min(100, data.result?.current ?? 0), }, }), ); timeoutId = window.setTimeout(poll, 5000); } else { timeoutId = window.setTimeout(poll, 5000); } }) .catch(() => { if (timeoutId !== null) { clearTimeout(timeoutId); timeoutId = null; } handleTaskFailure(clientTaskId); }); }; timeoutId = window.setTimeout(poll, 3000); }, [dispatch, handleTaskFailure, onSuccessfulUpload, sourceDocs, t, token], ); const onDrop = useCallback( (acceptedFiles: File[]) => { setfiles(acceptedFiles); setIngestor((prev) => ({ ...prev, name: acceptedFiles[0]?.name || '' })); // If we're in local_file mode, update the ingestor config if (ingestor.type === 'local_file') { setIngestor((prevState) => ({ ...prevState, config: { ...prevState.config, files: acceptedFiles, }, })); } }, [ingestor.type], ); const doNothing = () => undefined; const uploadFile = (clientTaskId: string) => { const formData = new FormData(); files.forEach((file) => { formData.append('file', file); }); formData.append('name', ingestor.name); formData.append('user', 'local'); const apiHost = import.meta.env.VITE_API_HOST; const xhr = new XMLHttpRequest(); dispatch( updateUploadTask({ id: clientTaskId, updates: { status: 'uploading', progress: 0 }, }), ); xhr.upload.addEventListener('progress', (event) => { if (!event.lengthComputable) return; const progressPercentage = Number( ((event.loaded / event.total) * 100).toFixed(2), ); dispatch( updateUploadTask({ id: clientTaskId, updates: { progress: progressPercentage }, }), ); }); xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { try { const parsed = JSON.parse(xhr.responseText) as { task_id?: string }; if (parsed.task_id) { dispatch( updateUploadTask({ id: clientTaskId, updates: { taskId: parsed.task_id, status: 'training', progress: 0, }, }), ); trackTraining(parsed.task_id, clientTaskId); } else { dispatch( updateUploadTask({ id: clientTaskId, updates: { status: 'completed', progress: 100 }, }), ); onSuccessfulUpload?.(); } } catch (error) { handleTaskFailure(clientTaskId); } } else { handleTaskFailure(clientTaskId, xhr.statusText || undefined); } }; xhr.onerror = () => { handleTaskFailure(clientTaskId); }; xhr.open('POST', `${apiHost}/api/upload`); xhr.setRequestHeader('Authorization', `Bearer ${token}`); xhr.send(formData); }; const uploadRemote = (clientTaskId: string) => { if (!ingestor.type) { handleTaskFailure(clientTaskId); return; } const formData = new FormData(); formData.append('name', ingestor.name); formData.append('user', 'local'); formData.append('source', ingestor.type as string); const ingestorSchema = getIngestorSchema(ingestor.type as IngestorType); if (!ingestorSchema) { handleTaskFailure(clientTaskId); return; } const schema: FormField[] = ingestorSchema.fields; const hasLocalFilePicker = schema.some( (field: FormField) => field.type === 'local_file_picker', ); const hasRemoteFilePicker = schema.some( (field: FormField) => field.type === 'remote_file_picker', ); const hasGoogleDrivePicker = schema.some( (field: FormField) => field.type === 'google_drive_picker', ); let configData: Record = { ...ingestor.config }; if (hasLocalFilePicker) { files.forEach((file) => { formData.append('file', file); }); } else if (hasRemoteFilePicker || hasGoogleDrivePicker) { const sessionToken = getSessionToken(ingestor.type as string); configData = { provider: ingestor.type as string, session_token: sessionToken, file_ids: selectedFiles, folder_ids: selectedFolders, }; } formData.append('data', JSON.stringify(configData)); const apiHost: string = import.meta.env.VITE_API_HOST; const endpoint = ingestor.type === 'local_file' ? `${apiHost}/api/upload` : `${apiHost}/api/remote`; const xhr = new XMLHttpRequest(); dispatch( updateUploadTask({ id: clientTaskId, updates: { status: 'uploading', progress: 0 }, }), ); xhr.upload.addEventListener('progress', (event: ProgressEvent) => { if (!event.lengthComputable) return; const progressPercentage = Number( ((event.loaded / event.total) * 100).toFixed(2), ); dispatch( updateUploadTask({ id: clientTaskId, updates: { progress: progressPercentage }, }), ); }); xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { try { const response = JSON.parse(xhr.responseText) as { task_id?: string }; if (response.task_id) { dispatch( updateUploadTask({ id: clientTaskId, updates: { taskId: response.task_id, status: 'training', progress: 0, }, }), ); trackTraining(response.task_id, clientTaskId); } else { dispatch( updateUploadTask({ id: clientTaskId, updates: { status: 'completed', progress: 100 }, }), ); onSuccessfulUpload?.(); } } catch (error) { handleTaskFailure(clientTaskId); } } else { handleTaskFailure(clientTaskId, xhr.statusText || undefined); } }; xhr.onerror = () => { handleTaskFailure(clientTaskId); }; xhr.open('POST', endpoint); xhr.setRequestHeader('Authorization', `Bearer ${token}`); xhr.send(formData); }; const handleClose = useCallback(() => { resetUploaderState(); setModalState('INACTIVE'); close(); }, [close, resetUploaderState, setModalState]); const handleUpload = () => { if (!ingestor.type) return; const ingestorSchemaForUpload = getIngestorSchema( ingestor.type as IngestorType, ); if (!ingestorSchemaForUpload) return; const schema: FormField[] = ingestorSchemaForUpload.fields; const hasLocalFilePicker = schema.some( (field: FormField) => field.type === 'local_file_picker', ); const displayName = ingestor.name?.trim() || files[0]?.name || t('modals.uploadDoc.label'); const clientTaskId = nanoid(); dispatch( addUploadTask({ id: clientTaskId, fileName: displayName, progress: 0, status: 'preparing', }), ); if (hasLocalFilePicker) { uploadFile(clientTaskId); } else { uploadRemote(clientTaskId); } handleClose(); }; const { getRootProps, getInputProps } = useDropzone({ onDrop, multiple: true, onDragEnter: doNothing, onDragOver: doNothing, onDragLeave: doNothing, maxSize: 25000000, accept: { 'application/pdf': ['.pdf'], 'text/plain': ['.txt'], 'text/x-rst': ['.rst'], 'text/x-markdown': ['.md'], 'application/zip': ['.zip'], 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], 'application/json': ['.json'], 'text/csv': ['.csv'], 'text/html': ['.html'], 'application/epub+zip': ['.epub'], 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': [ '.xlsx', ], 'application/vnd.openxmlformats-officedocument.presentationml.presentation': ['.pptx'], 'image/png': ['.png'], 'image/jpeg': ['.jpeg'], 'image/jpg': ['.jpg'], }, }); const isUploadDisabled = (): boolean => { if (!activeTab) return true; if (!ingestor.name?.trim()) { return true; } if (!ingestor.type) return true; const ingestorSchemaForValidation = getIngestorSchema( ingestor.type as IngestorType, ); if (!ingestorSchemaForValidation) return true; const schema: FormField[] = ingestorSchemaForValidation.fields; const hasLocalFilePicker = schema.some( (field: FormField) => field.type === 'local_file_picker', ); const hasRemoteFilePicker = schema.some( (field: FormField) => field.type === 'remote_file_picker', ); const hasGoogleDrivePicker = schema.some( (field: FormField) => field.type === 'google_drive_picker', ); if (hasLocalFilePicker) { if (files.length === 0) { return true; } } else if (hasRemoteFilePicker || hasGoogleDrivePicker) { if (selectedFiles.length === 0 && selectedFolders.length === 0) { return true; } } const ingestorSchemaForFields = getIngestorSchema( ingestor.type as IngestorType, ); if (!ingestorSchemaForFields) return false; const formFields: FormField[] = ingestorSchemaForFields.fields; for (const field of formFields) { if (field.required) { // Validate only required fields const value = ingestor.config[field.name as keyof typeof ingestor.config]; if (typeof value === 'string' && !value.trim()) { return true; } if ( typeof value === 'number' && (value === null || value === undefined || value <= 0) ) { return true; } if (typeof value === 'boolean' && value === undefined) { return true; } } } return false; }; const handleIngestorChange = ( key: keyof IngestorConfig['config'], value: string | number | boolean, ) => { setIngestor((prevState) => ({ ...prevState, config: { ...prevState.config, [key]: value, }, })); }; const handleIngestorTypeChange = (type: IngestorType | null) => { if (type === null) { setIngestor({ type: null, name: '', config: {}, }); setfiles([]); return; } const defaultConfig = IngestorDefaultConfigs[type]; setIngestor({ type, name: defaultConfig.name, config: defaultConfig.config, }); // Clear files if switching away from local_file if (type !== 'local_file') { setfiles([]); } }; const renderIngestorSelection = () => { return (
{ingestorOptions.map((option) => (
handleIngestorTypeChange(option.value as IngestorType) } >
{option.label}

{t(`modals.uploadDoc.ingestors.${option.value}.label`)}

))}
); }; return (
{!ingestor.type && (

{t('modals.uploadDoc.selectSource')}

)} {activeTab && ( <> {!ingestor.type && renderIngestorSelection()} {ingestor.type && (

{ingestor.type && t(`modals.uploadDoc.ingestors.${ingestor.type}.heading`)}

{ setIngestor((prevState) => ({ ...prevState, name: e.target.value, })); }} borderVariant="thin" placeholder={t('modals.uploadDoc.name')} required={true} labelBgClassName="bg-white dark:bg-charleston-green-2" className="w-full" /> {renderFormFields()}
)} {ingestor.type && getIngestorSchema(ingestor.type as IngestorType)?.fields.some( (field: FormField) => field.advanced, ) && ( )} )}
{activeTab && ingestor.type && ( )}
); } export default Upload;