diff --git a/frontend/components/ui/button.tsx b/frontend/components/ui/button.tsx index c901d8ad..81a3537d 100644 --- a/frontend/components/ui/button.tsx +++ b/frontend/components/ui/button.tsx @@ -3,7 +3,7 @@ import { cva, type VariantProps } from "class-variance-authority"; import * as React from "react"; const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none disabled:select-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none disabled:select-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", { variants: { variant: { diff --git a/frontend/components/ui/inputs/embedding-model.tsx b/frontend/components/ui/inputs/embedding-model.tsx new file mode 100644 index 00000000..ded138ad --- /dev/null +++ b/frontend/components/ui/inputs/embedding-model.tsx @@ -0,0 +1,62 @@ +import { ModelOption } from "@/app/api/queries/useGetModelsQuery"; +import { + getFallbackModels, + ModelProvider, +} from "@/app/settings/helpers/model-helpers"; +import { ModelSelectItems } from "@/app/settings/helpers/model-select-item"; +import { LabelWrapper } from "@/components/label-wrapper"; +import { + Select, + SelectContent, + SelectTrigger, + SelectValue, +} from "@radix-ui/react-select"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@radix-ui/react-tooltip"; + +interface EmbeddingModelInputProps { + disabled?: boolean; + value: string; + onChange: (value: string) => void; + modelsData?: { + embedding_models: ModelOption[]; + }; + currentProvider?: ModelProvider; +} + +export const EmbeddingModelInput = ({ + disabled, + value, + onChange, + modelsData, + currentProvider = "openai", +}: EmbeddingModelInputProps) => { + return ( + + + + ); +}; diff --git a/frontend/components/ui/inputs/number-input.tsx b/frontend/components/ui/inputs/number-input.tsx new file mode 100644 index 00000000..b312292f --- /dev/null +++ b/frontend/components/ui/inputs/number-input.tsx @@ -0,0 +1,74 @@ +import { LabelWrapper } from "@/components/label-wrapper"; +import { Button } from "../button"; +import { Input } from "../input"; +import { Minus, Plus } from "lucide-react"; + +interface NumberInputProps { + id: string; + label: string; + value: number; + onChange: (value: number) => void; + unit: string; + min?: number; + max?: number; + disabled?: boolean; +} + +export const NumberInput = ({ + id, + label, + value, + onChange, + min = 1, + max, + disabled, + unit, +}: NumberInputProps) => { + return ( + +
+ onChange(parseInt(e.target.value) || 0)} + className="w-full pr-20 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" + /> +
+ + {unit} + +
+ + +
+
+
+
+ ); +}; diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 56dc8dc8..5b786b7b 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -351,4 +351,17 @@ .discord-error { @apply text-xs opacity-70; } + + .box-shadow-inner::after { + content: " "; + position: absolute; + bottom: 0; + left: 0; + right: 0; + pointer-events: none; + background: linear-gradient(to top, hsl(var(--background)), transparent); + display: block; + width: 100%; + height: 30px; + } } diff --git a/frontend/src/app/upload/[provider]/page.tsx b/frontend/src/app/upload/[provider]/page.tsx index 7c72ec3d..10b9b0e5 100644 --- a/frontend/src/app/upload/[provider]/page.tsx +++ b/frontend/src/app/upload/[provider]/page.tsx @@ -6,373 +6,394 @@ import { useEffect, useState } from "react"; import { type CloudFile, UnifiedCloudPicker } from "@/components/cloud-picker"; import type { IngestSettings } from "@/components/cloud-picker/types"; import { Button } from "@/components/ui/button"; -import { Toast } from "@/components/ui/toast"; import { useTask } from "@/contexts/task-context"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; // CloudFile interface is now imported from the unified cloud picker interface CloudConnector { - id: string; - name: string; - description: string; - status: "not_connected" | "connecting" | "connected" | "error"; - type: string; - connectionId?: string; - clientId: string; - hasAccessToken: boolean; - accessTokenError?: string; + id: string; + name: string; + description: string; + status: "not_connected" | "connecting" | "connected" | "error"; + type: string; + connectionId?: string; + clientId: string; + hasAccessToken: boolean; + accessTokenError?: string; } export default function UploadProviderPage() { - const params = useParams(); - const router = useRouter(); - const provider = params.provider as string; - const { addTask, tasks } = useTask(); + const params = useParams(); + const router = useRouter(); + const provider = params.provider as string; + const { addTask, tasks } = useTask(); - const [connector, setConnector] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [accessToken, setAccessToken] = useState(null); - const [selectedFiles, setSelectedFiles] = useState([]); - const [isIngesting, setIsIngesting] = useState(false); - const [currentSyncTaskId, setCurrentSyncTaskId] = useState( - null, - ); - const [ingestSettings, setIngestSettings] = useState({ - chunkSize: 1000, - chunkOverlap: 200, - ocr: false, - pictureDescriptions: false, - embeddingModel: "text-embedding-3-small", - }); + const [connector, setConnector] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [accessToken, setAccessToken] = useState(null); + const [selectedFiles, setSelectedFiles] = useState([]); + const [isIngesting, setIsIngesting] = useState(false); + const [currentSyncTaskId, setCurrentSyncTaskId] = useState( + null + ); + const [ingestSettings, setIngestSettings] = useState({ + chunkSize: 1000, + chunkOverlap: 200, + ocr: false, + pictureDescriptions: false, + embeddingModel: "text-embedding-3-small", + }); - useEffect(() => { - const fetchConnectorInfo = async () => { - setIsLoading(true); - setError(null); + useEffect(() => { + const fetchConnectorInfo = async () => { + setIsLoading(true); + setError(null); - try { - // Fetch available connectors to validate the provider - const connectorsResponse = await fetch("/api/connectors"); - if (!connectorsResponse.ok) { - throw new Error("Failed to load connectors"); - } + try { + // Fetch available connectors to validate the provider + const connectorsResponse = await fetch("/api/connectors"); + if (!connectorsResponse.ok) { + throw new Error("Failed to load connectors"); + } - const connectorsResult = await connectorsResponse.json(); - const providerInfo = connectorsResult.connectors[provider]; + const connectorsResult = await connectorsResponse.json(); + const providerInfo = connectorsResult.connectors[provider]; - if (!providerInfo || !providerInfo.available) { - setError( - `Cloud provider "${provider}" is not available or configured.`, - ); - return; - } + if (!providerInfo || !providerInfo.available) { + setError( + `Cloud provider "${provider}" is not available or configured.` + ); + return; + } - // Check connector status - const statusResponse = await fetch( - `/api/connectors/${provider}/status`, - ); - if (!statusResponse.ok) { - throw new Error(`Failed to check ${provider} status`); - } + // Check connector status + const statusResponse = await fetch( + `/api/connectors/${provider}/status` + ); + if (!statusResponse.ok) { + throw new Error(`Failed to check ${provider} status`); + } - const statusData = await statusResponse.json(); - const connections = statusData.connections || []; - const activeConnection = connections.find( - (conn: { is_active: boolean; connection_id: string }) => - conn.is_active, - ); - const isConnected = activeConnection !== undefined; + const statusData = await statusResponse.json(); + const connections = statusData.connections || []; + const activeConnection = connections.find( + (conn: { is_active: boolean; connection_id: string }) => + conn.is_active + ); + const isConnected = activeConnection !== undefined; - let hasAccessToken = false; - let accessTokenError: string | undefined; + let hasAccessToken = false; + let accessTokenError: string | undefined; - // Try to get access token for connected connectors - if (isConnected && activeConnection) { - try { - const tokenResponse = await fetch( - `/api/connectors/${provider}/token?connection_id=${activeConnection.connection_id}`, - ); - if (tokenResponse.ok) { - const tokenData = await tokenResponse.json(); - if (tokenData.access_token) { - hasAccessToken = true; - setAccessToken(tokenData.access_token); - } - } else { - const errorData = await tokenResponse - .json() - .catch(() => ({ error: "Token unavailable" })); - accessTokenError = errorData.error || "Access token unavailable"; - } - } catch { - accessTokenError = "Failed to fetch access token"; - } - } + // Try to get access token for connected connectors + if (isConnected && activeConnection) { + try { + const tokenResponse = await fetch( + `/api/connectors/${provider}/token?connection_id=${activeConnection.connection_id}` + ); + if (tokenResponse.ok) { + const tokenData = await tokenResponse.json(); + if (tokenData.access_token) { + hasAccessToken = true; + setAccessToken(tokenData.access_token); + } + } else { + const errorData = await tokenResponse + .json() + .catch(() => ({ error: "Token unavailable" })); + accessTokenError = errorData.error || "Access token unavailable"; + } + } catch { + accessTokenError = "Failed to fetch access token"; + } + } - setConnector({ - id: provider, - name: providerInfo.name, - description: providerInfo.description, - status: isConnected ? "connected" : "not_connected", - type: provider, - connectionId: activeConnection?.connection_id, - clientId: activeConnection?.client_id, - hasAccessToken, - accessTokenError, - }); - } catch (error) { - console.error("Failed to load connector info:", error); - setError( - error instanceof Error - ? error.message - : "Failed to load connector information", - ); - } finally { - setIsLoading(false); - } - }; + setConnector({ + id: provider, + name: providerInfo.name, + description: providerInfo.description, + status: isConnected ? "connected" : "not_connected", + type: provider, + connectionId: activeConnection?.connection_id, + clientId: activeConnection?.client_id, + hasAccessToken, + accessTokenError, + }); + } catch (error) { + console.error("Failed to load connector info:", error); + setError( + error instanceof Error + ? error.message + : "Failed to load connector information" + ); + } finally { + setIsLoading(false); + } + }; - if (provider) { - fetchConnectorInfo(); - } - }, [provider]); + if (provider) { + fetchConnectorInfo(); + } + }, [provider]); - // Watch for sync task completion and redirect - useEffect(() => { - if (!currentSyncTaskId) return; + // Watch for sync task completion and redirect + useEffect(() => { + if (!currentSyncTaskId) return; - const currentTask = tasks.find( - (task) => task.task_id === currentSyncTaskId, - ); + const currentTask = tasks.find( + (task) => task.task_id === currentSyncTaskId + ); - if (currentTask && currentTask.status === "completed") { - // Task completed successfully, show toast and redirect - setIsIngesting(false); - setTimeout(() => { - router.push("/knowledge"); - }, 2000); // 2 second delay to let user see toast - } else if (currentTask && currentTask.status === "failed") { - // Task failed, clear the tracking but don't redirect - setIsIngesting(false); - setCurrentSyncTaskId(null); - } - }, [tasks, currentSyncTaskId, router]); + if (currentTask && currentTask.status === "completed") { + // Task completed successfully, show toast and redirect + setIsIngesting(false); + setTimeout(() => { + router.push("/knowledge"); + }, 2000); // 2 second delay to let user see toast + } else if (currentTask && currentTask.status === "failed") { + // Task failed, clear the tracking but don't redirect + setIsIngesting(false); + setCurrentSyncTaskId(null); + } + }, [tasks, currentSyncTaskId, router]); - const handleFileSelected = (files: CloudFile[]) => { - setSelectedFiles(files); - console.log(`Selected ${files.length} files from ${provider}:`, files); - // You can add additional handling here like triggering sync, etc. - }; + const handleFileSelected = (files: CloudFile[]) => { + setSelectedFiles(files); + console.log(`Selected ${files.length} files from ${provider}:`, files); + // You can add additional handling here like triggering sync, etc. + }; - const handleSync = async (connector: CloudConnector) => { - if (!connector.connectionId || selectedFiles.length === 0) return; + const handleSync = async (connector: CloudConnector) => { + if (!connector.connectionId || selectedFiles.length === 0) return; - setIsIngesting(true); + setIsIngesting(true); - try { - const syncBody: { - connection_id: string; - max_files?: number; - selected_files?: string[]; - settings?: IngestSettings; - } = { - connection_id: connector.connectionId, - selected_files: selectedFiles.map((file) => file.id), - settings: ingestSettings, - }; + try { + const syncBody: { + connection_id: string; + max_files?: number; + selected_files?: string[]; + settings?: IngestSettings; + } = { + connection_id: connector.connectionId, + selected_files: selectedFiles.map((file) => file.id), + settings: ingestSettings, + }; - const response = await fetch(`/api/connectors/${connector.type}/sync`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(syncBody), - }); + const response = await fetch(`/api/connectors/${connector.type}/sync`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(syncBody), + }); - const result = await response.json(); + const result = await response.json(); - if (response.status === 201) { - const taskIds = result.task_ids; - if (taskIds && taskIds.length > 0) { - const taskId = taskIds[0]; // Use the first task ID - addTask(taskId); - setCurrentSyncTaskId(taskId); - } - } else { - console.error("Sync failed:", result.error); - } - } catch (error) { - console.error("Sync error:", error); - setIsIngesting(false); - } - }; + if (response.status === 201) { + const taskIds = result.task_ids; + if (taskIds && taskIds.length > 0) { + const taskId = taskIds[0]; // Use the first task ID + addTask(taskId); + setCurrentSyncTaskId(taskId); + } + } else { + console.error("Sync failed:", result.error); + } + } catch (error) { + console.error("Sync error:", error); + setIsIngesting(false); + } + }; - const getProviderDisplayName = () => { - const nameMap: { [key: string]: string } = { - google_drive: "Google Drive", - onedrive: "OneDrive", - sharepoint: "SharePoint", - }; - return nameMap[provider] || provider; - }; + const getProviderDisplayName = () => { + const nameMap: { [key: string]: string } = { + google_drive: "Google Drive", + onedrive: "OneDrive", + sharepoint: "SharePoint", + }; + return nameMap[provider] || provider; + }; - if (isLoading) { - return ( -
-
-
-
-

Loading {getProviderDisplayName()} connector...

-
-
-
- ); - } + if (isLoading) { + return ( +
+
+
+
+

Loading {getProviderDisplayName()} connector...

+
+
+
+ ); + } - if (error || !connector) { - return ( -
-
- -
+ if (error || !connector) { + return ( +
+
+ +
-
-
- -

- Provider Not Available -

-

{error}

- -
-
-
- ); - } +
+
+ +

+ Provider Not Available +

+

{error}

+ +
+
+
+ ); + } - if (connector.status !== "connected") { - return ( -
-
- -
+ if (connector.status !== "connected") { + return ( +
+
+ +
-
-
- -

- {connector.name} Not Connected -

-

- You need to connect your {connector.name} account before you can - select files. -

- -
-
-
- ); - } +
+
+ +

+ {connector.name} Not Connected +

+

+ You need to connect your {connector.name} account before you can + select files. +

+ +
+
+
+ ); + } - if (!connector.hasAccessToken) { - return ( -
-
- -
+ if (!connector.hasAccessToken) { + return ( +
+
+ +
-
-
- -

- Access Token Required -

-

- {connector.accessTokenError || - `Unable to get access token for ${connector.name}. Try reconnecting your account.`} -

- -
-
-
- ); - } +
+
+ +

+ Access Token Required +

+

+ {connector.accessTokenError || + `Unable to get access token for ${connector.name}. Try reconnecting your account.`} +

+ +
+
+
+ ); + } - return ( -
-
- -

- Add from {getProviderDisplayName()} -

-
+ const hasSelectedFiles = selectedFiles.length > 0; -
- -
+ return ( +
+
+ +

+ Add from {getProviderDisplayName()} +

+
-
-
- - -
-
-
- ); +
+ +
+ +
+
+ + + + + + {!hasSelectedFiles ? ( + + Select at least one file before ingesting + + ) : null} + +
+
+
+ ); } diff --git a/frontend/src/components/cloud-picker/file-item.tsx b/frontend/src/components/cloud-picker/file-item.tsx index 3f6b5ab5..f06aa92e 100644 --- a/frontend/src/components/cloud-picker/file-item.tsx +++ b/frontend/src/components/cloud-picker/file-item.tsx @@ -1,11 +1,16 @@ "use client"; -import { Badge } from "@/components/ui/badge"; -import { FileText, Folder, Trash } from "lucide-react"; +import { FileText, Folder, Trash2 } from "lucide-react"; import { CloudFile } from "./types"; +import GoogleDriveIcon from "@/app/settings/icons/google-drive-icon"; +import SharePointIcon from "@/app/settings/icons/share-point-icon"; +import OneDriveIcon from "@/app/settings/icons/one-drive-icon"; +import { Button } from "@/components/ui/button"; interface FileItemProps { + provider: string; file: CloudFile; + shouldDisableActions: boolean; onRemove: (fileId: string) => void; } @@ -41,27 +46,43 @@ const formatFileSize = (bytes?: number) => { return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`; }; -export const FileItem = ({ file, onRemove }: FileItemProps) => ( +const getProviderIcon = (provider: string) => { + switch (provider) { + case "google_drive": + return ; + case "onedrive": + return ; + case "sharepoint": + return ; + default: + return ; + } +}; + +export const FileItem = ({ file, onRemove, provider }: FileItemProps) => (
- {getFileIcon(file.mimeType)} + {provider ? getProviderIcon(provider) : getFileIcon(file.mimeType)} {file.name} - + {getMimeTypeLabel(file.mimeType)} - +
-
+
{formatFileSize(file.size) || "—"} - - onRemove(file.id)} - /> + > + +
); diff --git a/frontend/src/components/cloud-picker/file-list.tsx b/frontend/src/components/cloud-picker/file-list.tsx index 775d78c4..7033fcf8 100644 --- a/frontend/src/components/cloud-picker/file-list.tsx +++ b/frontend/src/components/cloud-picker/file-list.tsx @@ -5,37 +5,50 @@ import { CloudFile } from "./types"; import { FileItem } from "./file-item"; interface FileListProps { + provider: string; files: CloudFile[]; onClearAll: () => void; onRemoveFile: (fileId: string) => void; + shouldDisableActions: boolean; } export const FileList = ({ + provider, files, onClearAll, onRemoveFile, + shouldDisableActions, }: FileListProps) => { if (files.length === 0) { return null; } return ( -
+
-

Added files

+

Added files ({files.length})

-
- {files.map(file => ( - - ))} +
+
+ {files.map((file) => ( + + ))} +
); diff --git a/frontend/src/components/cloud-picker/ingest-settings.tsx b/frontend/src/components/cloud-picker/ingest-settings.tsx index d5843a2a..a9ed0c8f 100644 --- a/frontend/src/components/cloud-picker/ingest-settings.tsx +++ b/frontend/src/components/cloud-picker/ingest-settings.tsx @@ -1,14 +1,28 @@ "use client"; -import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; -import { ChevronRight, Info } from "lucide-react"; +import { ChevronRight } from "lucide-react"; import { IngestSettings as IngestSettingsType } from "./types"; +import { LabelWrapper } from "@/components/label-wrapper"; +import { + Select, + SelectContent, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { ModelSelectItems } from "@/app/settings/helpers/model-select-item"; +import { getFallbackModels } from "@/app/settings/helpers/model-helpers"; +import { NumberInput } from "@/components/ui/inputs/number-input"; interface IngestSettingsProps { isOpen: boolean; @@ -44,7 +58,7 @@ export const IngestSettings = ({
@@ -58,35 +72,85 @@ export const IngestSettings = ({ -
-
+
+ {/* Embedding model selection - currently disabled */} + + + +
+
+
-
Chunk size
- - handleSettingsChange({ - chunkSize: parseInt(e.target.value) || 0, - }) - } + onChange={(value) => handleSettingsChange({ chunkSize: value })} + unit="characters" />
-
Chunk overlap
- - handleSettingsChange({ - chunkOverlap: parseInt(e.target.value) || 0, - }) + onChange={(value) => + handleSettingsChange({ chunkOverlap: value }) } + unit="characters" />
-
+ {/*
+
+
Table Structure
+
+ Capture table structure during ingest. +
+
+ + handleSettingsChange({ tableStructure: checked }) + } + /> +
*/} + +
OCR
@@ -95,13 +159,13 @@ export const IngestSettings = ({
+ onCheckedChange={(checked) => handleSettingsChange({ ocr: checked }) } />
-
+
Picture descriptions @@ -112,26 +176,11 @@ export const IngestSettings = ({
+ onCheckedChange={(checked) => handleSettingsChange({ pictureDescriptions: checked }) } />
- -
-
- Embedding model - -
- - handleSettingsChange({ embeddingModel: e.target.value }) - } - placeholder="text-embedding-3-small" - /> -
diff --git a/frontend/src/components/cloud-picker/picker-header.tsx b/frontend/src/components/cloud-picker/picker-header.tsx index 54407aa7..e0d9cfa4 100644 --- a/frontend/src/components/cloud-picker/picker-header.tsx +++ b/frontend/src/components/cloud-picker/picker-header.tsx @@ -51,19 +51,13 @@ export const PickerHeader = ({ Select files from {getProviderName(provider)} to ingest.

-
- csv, json, pdf,{" "} - +16 more{" "} - 150 MB max -
); diff --git a/frontend/src/components/cloud-picker/types.ts b/frontend/src/components/cloud-picker/types.ts index ca346bf0..20b1eae0 100644 --- a/frontend/src/components/cloud-picker/types.ts +++ b/frontend/src/components/cloud-picker/types.ts @@ -25,6 +25,7 @@ export interface UnifiedCloudPickerProps { baseUrl?: string; // Ingest settings onSettingsChange?: (settings: IngestSettings) => void; + isIngesting: boolean; } export interface GoogleAPI { diff --git a/frontend/src/components/cloud-picker/unified-cloud-picker.tsx b/frontend/src/components/cloud-picker/unified-cloud-picker.tsx index fd77698f..ff0c3903 100644 --- a/frontend/src/components/cloud-picker/unified-cloud-picker.tsx +++ b/frontend/src/components/cloud-picker/unified-cloud-picker.tsx @@ -16,6 +16,7 @@ export const UnifiedCloudPicker = ({ onFileSelected, selectedFiles = [], isAuthenticated, + isIngesting, accessToken, onPickerStateChange, clientId, @@ -116,7 +117,7 @@ export const UnifiedCloudPicker = ({ const handler = createProviderHandler( provider, accessToken, - isOpen => { + (isOpen) => { setIsPickerOpen(isOpen); onPickerStateChange?.(isOpen); }, @@ -126,8 +127,8 @@ export const UnifiedCloudPicker = ({ handler.openPicker((files: CloudFile[]) => { // Merge new files with existing ones, avoiding duplicates - const existingIds = new Set(selectedFiles.map(f => f.id)); - const newFiles = files.filter(f => !existingIds.has(f.id)); + const existingIds = new Set(selectedFiles.map((f) => f.id)); + const newFiles = files.filter((f) => !existingIds.has(f.id)); onFileSelected([...selectedFiles, ...newFiles]); }); } catch (error) { @@ -138,7 +139,7 @@ export const UnifiedCloudPicker = ({ }; const handleRemoveFile = (fileId: string) => { - const updatedFiles = selectedFiles.filter(file => file.id !== fileId); + const updatedFiles = selectedFiles.filter((file) => file.id !== fileId); onFileSelected(updatedFiles); }; @@ -168,20 +169,24 @@ export const UnifiedCloudPicker = ({ } return ( -
- +
+
+ +