import { ChevronLeft, FileText, Lock, Plug } from 'lucide-react'; import { envVar } from '@/env'; import { cn } from '@/lib/utils'; import { useCallback, useEffect, useRef, useState } from 'react'; import { nanoid } from '@reduxjs/toolkit'; import type { FileRejection } from 'react-dropzone'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector, useStore } from 'react-redux'; import { useNavigate } from 'react-router-dom'; import type { RootState } from '../store'; import userService from '../api/services/userService'; import { Alert, AlertDescription } from '../components/ui/alert'; import { Button } from '../components/ui/button'; import { Input } from '../components/ui/input'; import { FormField as UiFormField } from '../components/ui/form-field'; import { Label } from '../components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '../components/ui/select'; import { Switch } from '../components/ui/switch'; import { Textarea } from '../components/ui/textarea'; import { Modal } from '../components/ui/modal'; import { Separator } from '../components/ui/separator'; import { OptionCard } from '../components/ui/option-card'; import ConnectorSetupNotice from '../connectors/ConnectorSetupNotice'; import useConnectorLauncher from '../connectors/useConnectorLauncher'; import { loadConnectors, selectConnections, selectConnectorCatalog, selectConnectorsEnabled, selectConnectorsLoaded, } from '../connectors/connectorsSlice'; import type { AppDispatch } from '../store'; import { Card } from '../components/ui/card'; import { Dropzone } from '../components/ui/dropzone'; import { ListRow, ListRows } from '../components/ui/list-row'; import { formatBytes } from '../components/artifactViewUtils'; import { ActiveState, Doc } from '../models/misc'; import { getDocs } from '../preferences/preferenceApi'; import { selectSelectedDocs, selectToken, setSelectedDocs, setSourceDocs, } from '../preferences/preferenceSlice'; import { CONNECTION_INGESTORS, IngestorDefaultConfigs, IngestorFormSchemas, getIngestorSchema, IngestorOption, UPLOAD_AND_WEB_INGESTORS, } 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 { FILE_UPLOAD_ACCEPT } from '../constants/fileUpload'; import RetrievalOptions, { DEFAULT_RETRIEVAL_OPTIONS, isPrescreenConfigValid, optionsToConfig, type RetrievalOptionsValue, } from '../settings/components/RetrievalOptions'; import useRetrievalAvailability from '../settings/components/useRetrievalAvailability'; /** Per-file limit for local uploads (25 MB), enforced by the dropzone. */ const MAX_UPLOAD_BYTES = 25000000; function Upload({ receivedFile = [], setModalState, isOnboarding, renderTab = null, close, onSuccessfulUpload = () => undefined, selectUploadedDoc = true, initialIngestor, }: { receivedFile: File[]; setModalState: (state: ActiveState) => void; isOnboarding: boolean; renderTab: string | null; close: () => void; /** Fires once the upload is ingested, with the id of the source it created. */ onSuccessfulUpload?: (sourceId?: string) => void; /** * Whether a finished upload also becomes the chat's selected source. Callers * that only need the new source's id (the agent builder) pass ``false`` so * uploading never repoints the conversation the user left open. */ selectUploadedDoc?: boolean; /** Open straight on this source type's form (Connect from a connector card). */ initialIngestor?: IngestorType; }) { const token = useSelector(selectToken); const selectedDocs = useSelector(selectSelectedDocs); const connectorCatalog = useSelector(selectConnectorCatalog); const connections = useSelector(selectConnections); const connectorsLoaded = useSelector(selectConnectorsLoaded); const connectorsEnabled = useSelector(selectConnectorsEnabled); const navigate = useNavigate(); // GitHub's private-repository hand-over goes to the connect wizard, the // one flow every entry point uses; this modal steps aside and closes // with it. const [handedOver, setHandedOver] = useState(false); const { launch, modals: connectModals } = useConnectorLauncher({ onConnected: () => close(), }); const [files, setfiles] = useState(receivedFile); // Names of the files the last drop turned away (over the size limit or of // an unaccepted type), shown under the dropzone. const [rejectedFiles, setRejectedFiles] = useState([]); const [activeTab, setActiveTab] = useState(true); const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); const [retrievalOptions, setRetrievalOptions] = useState(DEFAULT_RETRIEVAL_OPTIONS); const { graphRAGAvailable, hybridAvailable, availableModels } = useRetrievalAvailability(token); // File picker state const [selectedFiles, setSelectedFiles] = useState([]); const [selectedFolders, setSelectedFolders] = useState([]); // The connection (account) the source syncs from. Pickers report it; S3 // and Reddit pick it here ('' means "enter new credentials"). const [connectionId, setConnectionId] = useState(null); 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 && !(usingSavedKeys && credentialKeys.has(field.name)), ); const advancedFields = schema.filter((field: FormField) => field.advanced); return (
{keyAccounts.length > 0 && ( )}
{generalFields.map((field: FormField) => renderField(field))}
{advancedFields.length > 0 && (
{advancedFields.map((field: FormField) => renderField(field))}
)}
); }; const renderField = (field: FormField) => { const isRequired = field.required ?? false; const fieldLabel = field.labelKey ? t(field.labelKey) : field.label; switch (field.type) { case 'string': return ( handleIngestorChange( field.name as keyof IngestorConfig['config'], e.target.value, ) } required={isRequired} /> ); case 'number': return ( handleIngestorChange( field.name as keyof IngestorConfig['config'], Number(e.target.value), ) } required={isRequired} /> ); case 'enum': { const currentValue = String( ingestor.config[field.name as keyof typeof ingestor.config] ?? '', ); return ( ); } case 'boolean': return (
{ handleIngestorChange( field.name as keyof IngestorConfig['config'], checked, ); }} />
); case 'textarea': return (