From 8248ae46d21f7987947e75f1e7528a501314afd5 Mon Sep 17 00:00:00 2001 From: Rahul Kumar Date: Fri, 26 Sep 2025 15:44:06 +0530 Subject: [PATCH 1/4] feat: XYN-298 make global state for uploading in kb make global state for uploading files in kb and refactor ui for kb upload --- .../src/assets/collectionIcons/document.svg | 6 + frontend/src/assets/collectionIcons/image.svg | 4 + frontend/src/assets/collectionIcons/pdf.svg | 4 + frontend/src/assets/collectionIcons/ppt.svg | 10 + .../assets/collectionIcons/spreadsheet.svg | 89 +++++ frontend/src/assets/collectionIcons/text.svg | 4 + frontend/src/components/ClFileUpload.tsx | 335 +++++++++++------- .../src/components/FileUploadSkeleton.tsx | 22 +- .../src/components/UploadProgressWidget.tsx | 207 +++++++++++ frontend/src/components/ui/toast.tsx | 1 + .../src/contexts/UploadProgressContext.tsx | 147 ++++++++ frontend/src/main.tsx | 9 +- .../_authenticated/knowledgeManagement.tsx | 309 +++++----------- 13 files changed, 793 insertions(+), 354 deletions(-) create mode 100644 frontend/src/assets/collectionIcons/document.svg create mode 100644 frontend/src/assets/collectionIcons/image.svg create mode 100644 frontend/src/assets/collectionIcons/pdf.svg create mode 100644 frontend/src/assets/collectionIcons/ppt.svg create mode 100644 frontend/src/assets/collectionIcons/spreadsheet.svg create mode 100644 frontend/src/assets/collectionIcons/text.svg create mode 100644 frontend/src/components/UploadProgressWidget.tsx create mode 100644 frontend/src/contexts/UploadProgressContext.tsx diff --git a/frontend/src/assets/collectionIcons/document.svg b/frontend/src/assets/collectionIcons/document.svg new file mode 100644 index 000000000..4a04d994f --- /dev/null +++ b/frontend/src/assets/collectionIcons/document.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/src/assets/collectionIcons/image.svg b/frontend/src/assets/collectionIcons/image.svg new file mode 100644 index 000000000..cee3fdca7 --- /dev/null +++ b/frontend/src/assets/collectionIcons/image.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/src/assets/collectionIcons/pdf.svg b/frontend/src/assets/collectionIcons/pdf.svg new file mode 100644 index 000000000..696a4c3d1 --- /dev/null +++ b/frontend/src/assets/collectionIcons/pdf.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/src/assets/collectionIcons/ppt.svg b/frontend/src/assets/collectionIcons/ppt.svg new file mode 100644 index 000000000..ce160b051 --- /dev/null +++ b/frontend/src/assets/collectionIcons/ppt.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/assets/collectionIcons/spreadsheet.svg b/frontend/src/assets/collectionIcons/spreadsheet.svg new file mode 100644 index 000000000..bd5d938c7 --- /dev/null +++ b/frontend/src/assets/collectionIcons/spreadsheet.svg @@ -0,0 +1,89 @@ + + + + Sheets-icon + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/assets/collectionIcons/text.svg b/frontend/src/assets/collectionIcons/text.svg new file mode 100644 index 000000000..25069913e --- /dev/null +++ b/frontend/src/assets/collectionIcons/text.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/src/components/ClFileUpload.tsx b/frontend/src/components/ClFileUpload.tsx index a22d1bc21..014f6a2f5 100644 --- a/frontend/src/components/ClFileUpload.tsx +++ b/frontend/src/components/ClFileUpload.tsx @@ -1,7 +1,60 @@ import { useState, useCallback, useRef, ChangeEvent } from "react" -import { Upload, Folder, File as FileIcon, X, Trash2 } from "lucide-react" +import { Upload, File as FileIcon, X, Trash2, FileUp } from "lucide-react" import { Button } from "@/components/ui/button" import FileUploadSkeleton from "@/components/FileUploadSkeleton" +import { FileType, MIME_TYPE_MAPPINGS, EXTENSION_MAPPINGS } from "../../../server/shared/types" +import { isValidFile } from "../../../server/shared/fileUtils" + +// SVG icon imports +import TextIcon from "@/assets/collectionIcons/text.svg" +import ImageIcon from "@/assets/collectionIcons/image.svg" +import PdfIcon from "@/assets/collectionIcons/pdf.svg" +import DocumentIcon from "@/assets/collectionIcons/document.svg" +import SpreadsheetIcon from "@/assets/collectionIcons/spreadsheet.svg" +import PresentationIcon from "@/assets/collectionIcons/ppt.svg" + +// Helper function to determine FileType from a file +const getFileType = (file: File): FileType => { + // First check by MIME type + for (const [fileType, mimeTypes] of Object.entries(MIME_TYPE_MAPPINGS)) { + if ((mimeTypes as readonly string[]).includes(file.type)) { + return fileType as FileType + } + } + + // Fallback to extension checking + const fileName = file.name.toLowerCase() + for (const [fileType, extensions] of Object.entries(EXTENSION_MAPPINGS)) { + if ((extensions as readonly string[]).some(ext => fileName.endsWith(ext))) { + return fileType as FileType + } + } + + // Default fallback + return FileType.FILE +} + +// Icon mapping from FileType to SVG component +const getFileIcon = (file: File) => { + const fileType = getFileType(file) + + switch (fileType) { + case FileType.TEXT: + return Text file + case FileType.IMAGE: + return Image file + case FileType.PDF: + return PDF file + case FileType.DOCUMENT: + return Document file + case FileType.SPREADSHEET: + return Spreadsheet file + case FileType.PRESENTATION: + return Presentation file + default: + return + } +} export interface SelectedFile { file: File @@ -233,154 +286,180 @@ const CollectionFileUpload = ({ return (
-
+ {/* Buttons above the upload area */} +
+ + +
+ + {/* Show drag & drop area only when no files are selected */} + {selectedFiles.length === 0 && (
- {selectedFiles.length > 0 && ( - - )} - -
- {selectedFiles.length === 0 ? ( +
+
- +
+ +

Drag & drop files or folders here

- or click to select files + or use the buttons above to select files

- ) : ( -
-
- {selectedFiles.map((selectedFile) => ( -
-
- - -
- -
-

- {selectedFile.file.name.length > 16 - ? `${selectedFile.file.name.substring( - 0, - 13, - )}...` - : selectedFile.file.name} -

-

- {(selectedFile.file.size / 1024).toFixed(2)} KB -

-
-
+
+
+
+ )} + + + + + {/* Upload Queue Section - only show when files are selected */} + {selectedFiles.length > 0 && ( +
+
+ {/* Header */} +
+

+ UPLOAD QUEUE +

+ + {selectedFiles.length} file{selectedFiles.length !== 1 ? "s" : ""} + + +
+ + {/* File List - scrollable with fixed height */} +
+ {selectedFiles.map((selectedFile, index) => { + const isSupported = isValidFile(selectedFile.file) + + return ( +
+ {/* File Icon */} +
+ {getFileIcon(selectedFile.file)} +
+ + {/* File Info */} +
+
+

+ {selectedFile.file.name} +

+ {!isSupported && ( +

+ UNSUPPORTED FORMAT +

+ )} + {isSupported && ( +

+ {(selectedFile.file.size / 1024 / 1024).toFixed(2)} MB +

+ )}
- ))} -
-
- )} -
-
-
+ {/* Remove Button */} +
+ +
+
+ ) + })} +
+ + {/* Upload Button - sticks to bottom */} +
- - {selectedFiles.length > 0 && ( -
- {selectedFiles.length} file - {selectedFiles.length !== 1 ? "s" : ""} selected -
- )} - -
- - - -
+ )}
) } diff --git a/frontend/src/components/FileUploadSkeleton.tsx b/frontend/src/components/FileUploadSkeleton.tsx index 12b412e14..108636b56 100644 --- a/frontend/src/components/FileUploadSkeleton.tsx +++ b/frontend/src/components/FileUploadSkeleton.tsx @@ -5,6 +5,7 @@ interface FileUploadSkeletonProps { processedFiles: number currentBatch: number totalBatches: number + showHeaders?: boolean } const FileUploadSkeleton: React.FC = ({ @@ -12,23 +13,26 @@ const FileUploadSkeleton: React.FC = ({ processedFiles, currentBatch, totalBatches, + showHeaders = true, }) => { // Show 3-4 skeleton rows const skeletonCount = Math.min(4, totalFiles - processedFiles) return (
- {/* Table header */} -
-
FOLDER
-
-
FILES
-
LAST UPDATED
-
UPDATED BY
-
+ {/* Table header - only show when showHeaders is true */} + {showHeaders && ( +
+
FOLDER
+
+
FILES
+
LAST UPDATED
+
UPDATED BY
+
+ )} {/* Skeleton rows */} -
+
{Array.from({ length: skeletonCount }).map((_, index) => (
diff --git a/frontend/src/components/UploadProgressWidget.tsx b/frontend/src/components/UploadProgressWidget.tsx new file mode 100644 index 000000000..dca571a52 --- /dev/null +++ b/frontend/src/components/UploadProgressWidget.tsx @@ -0,0 +1,207 @@ +import React, { useState } from 'react' +import { useUploadProgress } from '@/contexts/UploadProgressContext' +import { Button } from '@/components/ui/button' +import { X, ChevronUp, ChevronDown } from 'lucide-react' +import { ConfirmModal } from '@/components/ui/confirmModal' + +type TabType = 'all' | 'uploaded' | 'failed' + +export const UploadProgressWidget: React.FC = () => { + const { currentUpload, cancelUpload } = useUploadProgress() + const [isExpanded, setIsExpanded] = useState(false) + const [showCancelModal, setShowCancelModal] = useState(false) + const [activeTab, setActiveTab] = useState('all') + + if (!currentUpload || !currentUpload.isUploading) { + return null + } + + const { collectionName, batchProgress } = currentUpload + const progressPercentage = Math.round((batchProgress.current / batchProgress.total) * 100) + + const handleCancel = () => { + setShowCancelModal(true) + } + + const confirmCancel = () => { + cancelUpload(currentUpload.id) + setShowCancelModal(false) + } + + return ( + <> + {/* Main Upload Widget */} +
+ {/* Header */} +
+
+
+
+

+ UPLOADING FILES ({batchProgress.current}/{batchProgress.total}) +

+
+
+
+ + +
+
+ + {/* Progress Bar in Header */} +
+
+
+ + {/* Collection name and percentage - Always visible below progress bar */} +
+
+ + {collectionName} + +
+
+ + {progressPercentage}% + +
+
+
+ + {/* Tabbed File List - Only shown when expanded */} + {isExpanded && currentUpload.files && ( +
+
+ {/* Tab Navigation */} +
+
+ + + +
+
+ + {/* File List */} +
+ {(() => { + const filteredFiles = currentUpload.files.filter(file => { + if (activeTab === 'uploaded') return file.status === 'uploaded' + if (activeTab === 'failed') return file.status === 'failed' + return true // 'all' shows everything + }) + + if (filteredFiles.length === 0) { + return ( +
+ {activeTab === 'uploaded' && 'No files uploaded yet'} + {activeTab === 'failed' && 'No failed files'} + {activeTab === 'all' && 'No files'} +
+ ) + } + + return filteredFiles.map((file) => ( +
+ {/* Status Icon */} +
+
+ {(file.status === 'pending' || file.status === 'uploading') && ( +
+ )} + {file.status === 'uploaded' && ( + + + + )} + {file.status === 'failed' && ( + + + + )} +
+
+ + {/* File Info */} +
+

+ {file.name} +

+

+ {(file.size / 1024 / 1024).toFixed(2)} MB +

+ {file.status === 'failed' && file.error && ( +

+ {file.error} +

+ )} +
+
+ )) + })()} +
+
+
+ )} +
+ + {/* Cancel Confirmation Modal */} + setShowCancelModal(val.open ?? false)} + modalTitle="Cancel upload?" + modalMessage="Your upload is not complete. Would you like to cancel the upload?" + onConfirm={confirmCancel} + /> + + ) +} + +export default UploadProgressWidget diff --git a/frontend/src/components/ui/toast.tsx b/frontend/src/components/ui/toast.tsx index d02fd4e5e..72be6f7d5 100644 --- a/frontend/src/components/ui/toast.tsx +++ b/frontend/src/components/ui/toast.tsx @@ -67,6 +67,7 @@ const Toast = React.forwardRef<
diff --git a/frontend/src/contexts/UploadProgressContext.tsx b/frontend/src/contexts/UploadProgressContext.tsx new file mode 100644 index 000000000..1cb708969 --- /dev/null +++ b/frontend/src/contexts/UploadProgressContext.tsx @@ -0,0 +1,147 @@ +import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react' + +export interface UploadBatchProgress { + total: number + current: number + batch: number + totalBatches: number +} + +export interface UploadFileStatus { + id: string + name: string + size: number + status: 'pending' | 'uploading' | 'uploaded' | 'failed' + error?: string +} + +export interface UploadTask { + id: string + collectionName: string + isUploading: boolean + batchProgress: UploadBatchProgress + isNewCollection: boolean + targetCollectionId?: string + files: UploadFileStatus[] +} + +interface UploadProgressContextType { + currentUpload: UploadTask | null + startUpload: (collectionName: string, files: File[], totalBatches: number, isNewCollection: boolean, targetCollectionId?: string) => string + updateProgress: (uploadId: string, current: number, batch: number) => void + updateFileStatus: (uploadId: string, fileName: string, status: 'pending' | 'uploading' | 'uploaded' | 'failed', error?: string) => void + finishUpload: (uploadId: string) => void + cancelUpload: (uploadId: string) => void + getUploadProgress: (uploadId: string) => UploadTask | null +} + +const UploadProgressContext = createContext(undefined) + +export const useUploadProgress = () => { + const context = useContext(UploadProgressContext) + if (context === undefined) { + throw new Error('useUploadProgress must be used within an UploadProgressProvider') + } + return context +} + +interface UploadProgressProviderProps { + children: ReactNode +} + +export const UploadProgressProvider: React.FC = ({ children }) => { + const [currentUpload, setCurrentUpload] = useState(null) + + const startUpload = useCallback((collectionName: string, files: File[], totalBatches: number, isNewCollection: boolean, targetCollectionId?: string): string => { + const uploadId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + + const uploadFiles: UploadFileStatus[] = files.map((file, index) => ({ + id: `${uploadId}_file_${index}`, + name: file.name, + size: file.size, + status: 'pending' + })) + + const newUpload: UploadTask = { + id: uploadId, + collectionName, + isUploading: true, + batchProgress: { + total: files.length, + current: 0, + batch: 0, + totalBatches + }, + isNewCollection, + targetCollectionId, + files: uploadFiles + } + + setCurrentUpload(newUpload) + return uploadId + }, []) + + const updateProgress = useCallback((uploadId: string, current: number, batch: number) => { + setCurrentUpload(prev => { + if (!prev || prev.id !== uploadId) return prev + + return { + ...prev, + batchProgress: { + ...prev.batchProgress, + current, + batch + } + } + }) + }, []) + + const updateFileStatus = useCallback((uploadId: string, fileName: string, status: 'pending' | 'uploading' | 'uploaded' | 'failed', error?: string) => { + setCurrentUpload(prev => { + if (!prev || prev.id !== uploadId) return prev + + return { + ...prev, + files: prev.files.map(file => + file.name === fileName + ? { ...file, status, error } + : file + ) + } + }) + }, []) + + const finishUpload = useCallback((uploadId: string) => { + setCurrentUpload(prev => { + if (!prev || prev.id !== uploadId) return prev + return null + }) + }, []) + + const cancelUpload = useCallback((uploadId: string) => { + setCurrentUpload(prev => { + if (!prev || prev.id !== uploadId) return prev + return null + }) + }, []) + + const getUploadProgress = useCallback((uploadId: string): UploadTask | null => { + return currentUpload?.id === uploadId ? currentUpload : null + }, [currentUpload]) + + const value: UploadProgressContextType = { + currentUpload, + startUpload, + updateProgress, + updateFileStatus, + finishUpload, + cancelUpload, + getUploadProgress + } + + return ( + + {children} + + ) +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 66727fbfb..747c9a640 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -7,6 +7,8 @@ import { RouterProvider, createRouter } from "@tanstack/react-router" import { routeTree } from "@/routeTree.gen" import { ThemeProvider } from "@/components/ThemeContext" import { Toaster } from "@/components/ui/toaster" +import { UploadProgressProvider } from "@/contexts/UploadProgressContext" +import UploadProgressWidget from "@/components/UploadProgressWidget" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" @@ -27,8 +29,11 @@ const App = () => { return ( - - + + + + + ) diff --git a/frontend/src/routes/_authenticated/knowledgeManagement.tsx b/frontend/src/routes/_authenticated/knowledgeManagement.tsx index 52e3b780c..8c0047c3c 100644 --- a/frontend/src/routes/_authenticated/knowledgeManagement.tsx +++ b/frontend/src/routes/_authenticated/knowledgeManagement.tsx @@ -57,6 +57,7 @@ import { import ExcelViewer from "@/components/ExcelViewer" import CsvViewer from "@/components/CsvViewer" import TxtViewer from "@/components/TxtViewer" +import { useUploadProgress } from "@/contexts/UploadProgressContext" // Persistent storage for documentId -> tempChatId mapping using sessionStorage const DOCUMENT_CHAT_MAP_KEY = "documentToTempChatMap" @@ -80,49 +81,6 @@ interface Collection { isPrivate?: boolean } -// Helper functions for localStorage -const UPLOAD_STATE_KEY = "knowledgeManagement_uploadState" - -const saveUploadState = (state: { - isUploading: boolean - batchProgress: { - total: number - current: number - batch: number - totalBatches: number - } - uploadingCollectionName: string -}) => { - try { - localStorage.setItem(UPLOAD_STATE_KEY, JSON.stringify(state)) - } catch (error) { - console.error("Failed to save upload state:", error) - } -} - -const loadUploadState = () => { - try { - const saved = localStorage.getItem(UPLOAD_STATE_KEY) - if (saved) { - return JSON.parse(saved) - } - } catch (error) { - console.error("Failed to load upload state:", error) - } - return { - isUploading: false, - batchProgress: { total: 0, current: 0, batch: 0, totalBatches: 0 }, - uploadingCollectionName: "", - } -} - -const clearUploadState = () => { - try { - localStorage.removeItem(UPLOAD_STATE_KEY) - } catch (error) { - console.error("Failed to clear upload state:", error) - } -} // Memoized Document Viewer Container to prevent re-renders on sidebar resize const DocumentViewerContainer = memo( @@ -421,13 +379,15 @@ function KnowledgeManagementContent() { // Chat overlay state - only used when isChatHidden is true const [isChatOverlayOpen, setIsChatOverlayOpen] = useState(false) - // Load upload state from localStorage on mount - const savedState = loadUploadState() - const [isUploading, setIsUploading] = useState(savedState.isUploading) - const [batchProgress, setBatchProgress] = useState(savedState.batchProgress) - const [uploadingCollectionName, setUploadingCollectionName] = useState( - savedState.uploadingCollectionName, - ) + // Use global upload progress context + const { currentUpload, startUpload, updateProgress, updateFileStatus, finishUpload, cancelUpload } = useUploadProgress() + + // Derived state from global context + const isUploading = currentUpload?.isUploading || false + const batchProgress = currentUpload?.batchProgress || { total: 0, current: 0, batch: 0, totalBatches: 0 } + const uploadingCollectionName = currentUpload?.collectionName || "" + const isNewCollectionUpload = currentUpload?.isNewCollection || false + const targetCollectionId = currentUpload?.targetCollectionId // Zoom detection for chat component useEffect(() => { @@ -477,105 +437,7 @@ function KnowledgeManagementContent() { const [openDropdown, setOpenDropdown] = useState(null) - // Save upload state to localStorage whenever it changes - useEffect(() => { - saveUploadState({ - isUploading, - batchProgress, - uploadingCollectionName, - }) - }, [isUploading, batchProgress, uploadingCollectionName]) - - // Clean up on unmount - useEffect(() => { - return () => { - // Only clear if upload is not active - if (!isUploading) { - clearUploadState() - } - } - }, [isUploading]) - - // Fallback: Clear upload state if it's been "uploading" for too long - useEffect(() => { - if (!isUploading) return - - // If upload state has been active for more than 10 minutes, clear it - const timeout = setTimeout( - () => { - setIsUploading(false) - setBatchProgress({ total: 0, current: 0, batch: 0, totalBatches: 0 }) - setUploadingCollectionName("") - clearUploadState() - }, - 10 * 60 * 1000, - ) // 10 minutes - - return () => clearTimeout(timeout) - }, [isUploading]) - - // Check for ongoing uploads on component mount - useEffect(() => { - const checkForOngoingUploads = async () => { - const savedState = loadUploadState() - if (savedState.isUploading && savedState.uploadingCollectionName) { - // If there's an ongoing upload, check if it's actually complete - // Check if the collection exists and has files - try { - const response = await api.cl.$get({ - query: { includeItems: "true" }, - }) - if (response.ok) { - const data = await response.json() - const existingCollection = data.find( - (collection: CollectionType) => - collection.name.toLowerCase() === - savedState.uploadingCollectionName.toLowerCase(), - ) - - if ( - existingCollection && - existingCollection.totalItems >= savedState.batchProgress.total - ) { - // Upload appears to be complete, clear the state - setIsUploading(false) - setBatchProgress({ - total: 0, - current: 0, - batch: 0, - totalBatches: 0, - }) - setUploadingCollectionName("") - clearUploadState() - - // Show completion toast - toast.success({ - title: "Upload Complete", - description: `Upload of ${savedState.batchProgress.total} files to "${savedState.uploadingCollectionName}" completed while you were away.`, - }) - } - } - } catch (error) { - console.error("Error checking upload status:", error) - // If we can't check, clear the state after a timeout to avoid infinite skeleton - setTimeout(() => { - setIsUploading(false) - setBatchProgress({ - total: 0, - current: 0, - batch: 0, - totalBatches: 0, - }) - setUploadingCollectionName("") - clearUploadState() - }, 5000) - } - } - } - - checkForOngoingUploads() - }, [toast]) useEffect(() => { const fetchCollections = async () => { @@ -669,14 +531,10 @@ function KnowledgeManagementContent() { return } - setIsUploading(true) - setUploadingCollectionName(collectionName.trim()) - setBatchProgress({ - total: selectedFiles.length, - current: 0, - batch: 0, - totalBatches: 0, - }) + // Start the global upload progress + const batches = createBatches(selectedFiles, collectionName.trim()) + const files = selectedFiles.map(f => f.file) + const uploadId = startUpload(collectionName.trim(), files, batches.length, true) // Close the modal immediately after starting upload handleCloseModal() @@ -685,25 +543,37 @@ function KnowledgeManagementContent() { // First create the collection const cl = await createCollection(collectionName.trim(), "") - // Then upload files in batches - const batches = createBatches(selectedFiles, collectionName.trim()) - setBatchProgress((prev: typeof batchProgress) => ({ - ...prev, - totalBatches: batches.length, - })) - let totalSuccessful = 0 let totalSkipped = 0 let totalFailed = 0 for (let i = 0; i < batches.length; i++) { - setBatchProgress((prev: typeof batchProgress) => ({ - ...prev, - batch: i + 1, - })) const batchFiles = batches[i].map((f) => f.file) + + // Mark batch files as uploading + batchFiles.forEach(file => { + updateFileStatus(uploadId, file.name, 'uploading') + }) + const uploadResult = await uploadFileBatch(batchFiles, cl.id) + // Update individual file statuses based on results + if (uploadResult.results) { + uploadResult.results.forEach((result: any, index: number) => { + const file = batchFiles[index] + if (result.success) { + updateFileStatus(uploadId, file.name, 'uploaded') + } else { + updateFileStatus(uploadId, file.name, 'failed', result.error || 'Upload failed') + } + }) + } else { + // Fallback: mark all as uploaded if no individual results available + batchFiles.forEach(file => { + updateFileStatus(uploadId, file.name, 'uploaded') + }) + } + // Accumulate results from each batch if (uploadResult.summary) { totalSuccessful += uploadResult.summary.successful || 0 @@ -711,10 +581,9 @@ function KnowledgeManagementContent() { totalFailed += uploadResult.summary.failed || 0 } - setBatchProgress((prev: typeof batchProgress) => ({ - ...prev, - current: prev.current + batchFiles.length, - })) + // Update progress + const newCurrent = (i + 1) * batchFiles.length + updateProgress(uploadId, newCurrent, i + 1) } // Fetch the updated Collection data from the backend @@ -803,10 +672,7 @@ function KnowledgeManagementContent() { description: "Failed to create collection. Please try again.", }) } finally { - setIsUploading(false) - setBatchProgress({ total: 0, current: 0, batch: 0, totalBatches: 0 }) - setUploadingCollectionName("") - clearUploadState() + finishUpload(uploadId) } } @@ -848,14 +714,10 @@ function KnowledgeManagementContent() { return } - setIsUploading(true) - setUploadingCollectionName(addingToCollection.name) - setBatchProgress({ - total: selectedFiles.length, - current: 0, - batch: 0, - totalBatches: 0, - }) + // Start the global upload progress + const batches = createBatches(selectedFiles, addingToCollection.name) + const files = selectedFiles.map(f => f.file) + const uploadId = startUpload(addingToCollection.name, files, batches.length, false, addingToCollection.id) // Close the modal immediately after starting upload handleCloseModal() @@ -866,24 +728,37 @@ function KnowledgeManagementContent() { let totalSkipped = 0 let totalFailed = 0 - const batches = createBatches(selectedFiles, addingToCollection.name) - setBatchProgress((prev: typeof batchProgress) => ({ - ...prev, - totalBatches: batches.length, - })) - for (let i = 0; i < batches.length; i++) { - setBatchProgress((prev: typeof batchProgress) => ({ - ...prev, - batch: i + 1, - })) const batchFiles = batches[i].map((f) => f.file) + + // Mark batch files as uploading + batchFiles.forEach(file => { + updateFileStatus(uploadId, file.name, 'uploading') + }) + const uploadedResult = await uploadFileBatch( batchFiles, addingToCollection.id, targetFolder?.id, ) + // Update individual file statuses based on results + if (uploadedResult.results) { + uploadedResult.results.forEach((result: any, index: number) => { + const file = batchFiles[index] + if (result.success) { + updateFileStatus(uploadId, file.name, 'uploaded') + } else { + updateFileStatus(uploadId, file.name, 'failed', result.error || 'Upload failed') + } + }) + } else { + // Fallback: mark all as uploaded if no individual results available + batchFiles.forEach(file => { + updateFileStatus(uploadId, file.name, 'uploaded') + }) + } + // Accumulate results from each batch if (uploadedResult.summary) { totalSuccessful += uploadedResult.summary.successful || 0 @@ -891,10 +766,9 @@ function KnowledgeManagementContent() { totalFailed += uploadedResult.summary.failed || 0 } - setBatchProgress((prev: typeof batchProgress) => ({ - ...prev, - current: prev.current + batchFiles.length, - })) + // Update progress + const newCurrent = (i + 1) * batchFiles.length + updateProgress(uploadId, newCurrent, i + 1) } // Refresh the collection by fetching updated data from backend @@ -984,17 +858,13 @@ function KnowledgeManagementContent() { description: "Failed to add files to collection. Please try again.", }) } finally { - setIsUploading(false) - setBatchProgress({ total: 0, current: 0, batch: 0, totalBatches: 0 }) - setUploadingCollectionName("") - clearUploadState() + finishUpload(uploadId) } } const handleDeleteItem = async () => { if (!deletingItem) return - setIsUploading(true) try { // Find the item to delete based on the path const itemToDelete = findItemByPath( @@ -1061,7 +931,6 @@ function KnowledgeManagementContent() { }) } finally { setDeletingItem(null) - setIsUploading(false) } } @@ -1136,8 +1005,6 @@ function KnowledgeManagementContent() { const handleDeleteCollection = async () => { if (!deletingCollection) return - setIsUploading(true) - try { // Delete the collection await deleteCollection(deletingCollection.id) @@ -1158,8 +1025,6 @@ function KnowledgeManagementContent() { title: "Delete Failed", description: "Failed to delete collection. Please try again.", }) - } finally { - setIsUploading(false) } } @@ -1713,8 +1578,8 @@ function KnowledgeManagementContent() {
- {/* Show skeleton loader when uploading */} - {isUploading && batchProgress.total > 0 && ( + {/* Show skeleton loader when uploading to NEW collection */} + {isUploading && batchProgress.total > 0 && isNewCollectionUpload && (
@@ -1735,6 +1600,7 @@ function KnowledgeManagementContent() { processedFiles={batchProgress.current} currentBatch={batchProgress.batch} totalBatches={batchProgress.totalBatches} + showHeaders={true} />
)} @@ -1967,6 +1833,19 @@ function KnowledgeManagementContent() { } }} /> + {/* Show skeleton for existing collection uploads */} + {isUploading && + !isNewCollectionUpload && + targetCollectionId === collection.id && + batchProgress.total > 0 && ( + + )} )}
@@ -2084,8 +1963,8 @@ function KnowledgeManagementContent() {
)} {(showNewCollection || addingToCollection) && ( -
-
+
+

@@ -2111,12 +1990,12 @@ function KnowledgeManagementContent() { > {addingToCollection ? "Adding to collection" - : "Collection title"} + : "Collection name"} setCollectionName(e.target.value)} className="w-full text-xl placeholder:text-gray-400 placeholder:opacity-60 dark:placeholder:text-gray-500 dark:placeholder:opacity-50 !outline-none !focus:outline-none !focus:ring-0 !focus:shadow-none !bg-transparent !px-0 !shadow-none !ring-0 border-0 border-b border-gray-300 dark:border-gray-600 focus:border-b focus:border-gray-400 dark:focus:border-gray-500 !rounded-none" @@ -2210,8 +2089,8 @@ const createBatches = ( collectionName: string, ): FileUploadSelectedFile[][] => { const BATCH_CONFIG = { - MAX_PAYLOAD_SIZE: 45 * 1024 * 1024, - MAX_FILES_PER_BATCH: 50, + MAX_PAYLOAD_SIZE: 5 * 1024 * 1024, + MAX_FILES_PER_BATCH: 5, } const batches: FileUploadSelectedFile[][] = [] let currentBatch: FileUploadSelectedFile[] = [] From d7ec9373ada334b5ac6b81808fb9a7d082075d39 Mon Sep 17 00:00:00 2001 From: Rahul Kumar Date: Fri, 26 Sep 2025 17:20:48 +0530 Subject: [PATCH 2/4] refactor: XYN-298 resolve comments --- frontend/src/components/ClFileUpload.tsx | 2 +- .../src/components/UploadProgressWidget.tsx | 2 +- .../src/contexts/UploadProgressContext.tsx | 16 ++++----- .../_authenticated/knowledgeManagement.tsx | 36 +++++++++---------- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/frontend/src/components/ClFileUpload.tsx b/frontend/src/components/ClFileUpload.tsx index 014f6a2f5..40c5f54f1 100644 --- a/frontend/src/components/ClFileUpload.tsx +++ b/frontend/src/components/ClFileUpload.tsx @@ -1,5 +1,5 @@ import { useState, useCallback, useRef, ChangeEvent } from "react" -import { Upload, File as FileIcon, X, Trash2, FileUp } from "lucide-react" +import { File as FileIcon, X, FileUp } from "lucide-react" import { Button } from "@/components/ui/button" import FileUploadSkeleton from "@/components/FileUploadSkeleton" import { FileType, MIME_TYPE_MAPPINGS, EXTENSION_MAPPINGS } from "../../../server/shared/types" diff --git a/frontend/src/components/UploadProgressWidget.tsx b/frontend/src/components/UploadProgressWidget.tsx index dca571a52..17bb33bec 100644 --- a/frontend/src/components/UploadProgressWidget.tsx +++ b/frontend/src/components/UploadProgressWidget.tsx @@ -17,7 +17,7 @@ export const UploadProgressWidget: React.FC = () => { } const { collectionName, batchProgress } = currentUpload - const progressPercentage = Math.round((batchProgress.current / batchProgress.total) * 100) + const progressPercentage = batchProgress.total > 0 ? Math.round((batchProgress.current / batchProgress.total) * 100) : 0 const handleCancel = () => { setShowCancelModal(true) diff --git a/frontend/src/contexts/UploadProgressContext.tsx b/frontend/src/contexts/UploadProgressContext.tsx index 1cb708969..98b6ce13c 100644 --- a/frontend/src/contexts/UploadProgressContext.tsx +++ b/frontend/src/contexts/UploadProgressContext.tsx @@ -27,9 +27,9 @@ export interface UploadTask { interface UploadProgressContextType { currentUpload: UploadTask | null - startUpload: (collectionName: string, files: File[], totalBatches: number, isNewCollection: boolean, targetCollectionId?: string) => string + startUpload: (collectionName: string, files: { file: File; id: string }[], totalBatches: number, isNewCollection: boolean, targetCollectionId?: string) => string updateProgress: (uploadId: string, current: number, batch: number) => void - updateFileStatus: (uploadId: string, fileName: string, status: 'pending' | 'uploading' | 'uploaded' | 'failed', error?: string) => void + updateFileStatus: (uploadId: string, fileName: string, fileId: string, status: 'pending' | 'uploading' | 'uploaded' | 'failed', error?: string) => void finishUpload: (uploadId: string) => void cancelUpload: (uploadId: string) => void getUploadProgress: (uploadId: string) => UploadTask | null @@ -52,13 +52,13 @@ interface UploadProgressProviderProps { export const UploadProgressProvider: React.FC = ({ children }) => { const [currentUpload, setCurrentUpload] = useState(null) - const startUpload = useCallback((collectionName: string, files: File[], totalBatches: number, isNewCollection: boolean, targetCollectionId?: string): string => { + const startUpload = useCallback((collectionName: string, files: { file: File; id: string }[], totalBatches: number, isNewCollection: boolean, targetCollectionId?: string): string => { const uploadId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` const uploadFiles: UploadFileStatus[] = files.map((file, index) => ({ - id: `${uploadId}_file_${index}`, - name: file.name, - size: file.size, + id: file.id, + name: file.file.name, + size: file.file.size, status: 'pending' })) @@ -96,14 +96,14 @@ export const UploadProgressProvider: React.FC = ({ }) }, []) - const updateFileStatus = useCallback((uploadId: string, fileName: string, status: 'pending' | 'uploading' | 'uploaded' | 'failed', error?: string) => { + const updateFileStatus = useCallback((uploadId: string, fileName: string, fileId: string, status: 'pending' | 'uploading' | 'uploaded' | 'failed', error?: string) => { setCurrentUpload(prev => { if (!prev || prev.id !== uploadId) return prev return { ...prev, files: prev.files.map(file => - file.name === fileName + file.name === fileName && file.id === fileId ? { ...file, status, error } : file ) diff --git a/frontend/src/routes/_authenticated/knowledgeManagement.tsx b/frontend/src/routes/_authenticated/knowledgeManagement.tsx index e56270ef0..f0d9b5ee1 100644 --- a/frontend/src/routes/_authenticated/knowledgeManagement.tsx +++ b/frontend/src/routes/_authenticated/knowledgeManagement.tsx @@ -386,7 +386,7 @@ function KnowledgeManagementContent() { const [isVespaModalOpen, setIsVespaModalOpen] = useState(false) // Use global upload progress context - const { currentUpload, startUpload, updateProgress, updateFileStatus, finishUpload, cancelUpload } = useUploadProgress() + const { currentUpload, startUpload, updateProgress, updateFileStatus, finishUpload } = useUploadProgress() // Derived state from global context const isUploading = currentUpload?.isUploading || false @@ -540,7 +540,7 @@ function KnowledgeManagementContent() { // Start the global upload progress const batches = createBatches(selectedFiles, collectionName.trim()) - const files = selectedFiles.map(f => f.file) + const files = selectedFiles.map(f => ({ file: f.file, id: f.id })) const uploadId = startUpload(collectionName.trim(), files, batches.length, true) // Close the modal immediately after starting upload @@ -555,29 +555,29 @@ function KnowledgeManagementContent() { let totalFailed = 0 for (let i = 0; i < batches.length; i++) { - const batchFiles = batches[i].map((f) => f.file) - + const batchFiles = batches[i].map((f) => ({ file: f.file, id: f.id })) + // Mark batch files as uploading batchFiles.forEach(file => { - updateFileStatus(uploadId, file.name, 'uploading') + updateFileStatus(uploadId, file.file.name, file.id, 'uploading') }) - - const uploadResult = await uploadFileBatch(batchFiles, cl.id) + + const uploadResult = await uploadFileBatch(batchFiles.map((f) => f.file), cl.id) // Update individual file statuses based on results if (uploadResult.results) { uploadResult.results.forEach((result: any, index: number) => { const file = batchFiles[index] if (result.success) { - updateFileStatus(uploadId, file.name, 'uploaded') + updateFileStatus(uploadId, file.file.name, file.id, 'uploaded') } else { - updateFileStatus(uploadId, file.name, 'failed', result.error || 'Upload failed') + updateFileStatus(uploadId, file.file.name, file.id, 'failed', result.error || 'Upload failed') } }) } else { // Fallback: mark all as uploaded if no individual results available batchFiles.forEach(file => { - updateFileStatus(uploadId, file.name, 'uploaded') + updateFileStatus(uploadId, file.file.name, file.id, 'uploaded') }) } @@ -723,7 +723,7 @@ function KnowledgeManagementContent() { // Start the global upload progress const batches = createBatches(selectedFiles, addingToCollection.name) - const files = selectedFiles.map(f => f.file) + const files = selectedFiles.map(f => ({ file: f.file, id: f.id })) const uploadId = startUpload(addingToCollection.name, files, batches.length, false, addingToCollection.id) // Close the modal immediately after starting upload @@ -736,15 +736,15 @@ function KnowledgeManagementContent() { let totalFailed = 0 for (let i = 0; i < batches.length; i++) { - const batchFiles = batches[i].map((f) => f.file) - + const batchFiles = batches[i].map((f) => ({ file: f.file, id: f.id })) + // Mark batch files as uploading batchFiles.forEach(file => { - updateFileStatus(uploadId, file.name, 'uploading') + updateFileStatus(uploadId, file.file.name, file.id, 'uploading') }) const uploadedResult = await uploadFileBatch( - batchFiles, + batchFiles.map(f => f.file), addingToCollection.id, targetFolder?.id, ) @@ -754,15 +754,15 @@ function KnowledgeManagementContent() { uploadedResult.results.forEach((result: any, index: number) => { const file = batchFiles[index] if (result.success) { - updateFileStatus(uploadId, file.name, 'uploaded') + updateFileStatus(uploadId, file.file.name, file.id, 'uploaded') } else { - updateFileStatus(uploadId, file.name, 'failed', result.error || 'Upload failed') + updateFileStatus(uploadId, file.file.name, file.id, 'failed', result.error || 'Upload failed') } }) } else { // Fallback: mark all as uploaded if no individual results available batchFiles.forEach(file => { - updateFileStatus(uploadId, file.name, 'uploaded') + updateFileStatus(uploadId, file.file.name, file.id, 'uploaded') }) } From 3d184445b7685e07ef2589e7ccb119bf644b0d16 Mon Sep 17 00:00:00 2001 From: Rahul Kumar Date: Fri, 26 Sep 2025 17:59:10 +0530 Subject: [PATCH 3/4] refactor: XYN-298 resolve comments --- .../src/contexts/UploadProgressContext.tsx | 17 +++++-- .../_authenticated/knowledgeManagement.tsx | 51 +++++++++++++------ frontend/src/utils/fileUtils.ts | 2 + 3 files changed, 51 insertions(+), 19 deletions(-) diff --git a/frontend/src/contexts/UploadProgressContext.tsx b/frontend/src/contexts/UploadProgressContext.tsx index 98b6ce13c..5c4b6cc25 100644 --- a/frontend/src/contexts/UploadProgressContext.tsx +++ b/frontend/src/contexts/UploadProgressContext.tsx @@ -23,11 +23,12 @@ export interface UploadTask { isNewCollection: boolean targetCollectionId?: string files: UploadFileStatus[] + abortController: AbortController } interface UploadProgressContextType { currentUpload: UploadTask | null - startUpload: (collectionName: string, files: { file: File; id: string }[], totalBatches: number, isNewCollection: boolean, targetCollectionId?: string) => string + startUpload: (collectionName: string, files: { file: File; id: string }[], totalBatches: number, isNewCollection: boolean, targetCollectionId?: string) => { uploadId: string; abortController: AbortController } updateProgress: (uploadId: string, current: number, batch: number) => void updateFileStatus: (uploadId: string, fileName: string, fileId: string, status: 'pending' | 'uploading' | 'uploaded' | 'failed', error?: string) => void finishUpload: (uploadId: string) => void @@ -52,8 +53,9 @@ interface UploadProgressProviderProps { export const UploadProgressProvider: React.FC = ({ children }) => { const [currentUpload, setCurrentUpload] = useState(null) - const startUpload = useCallback((collectionName: string, files: { file: File; id: string }[], totalBatches: number, isNewCollection: boolean, targetCollectionId?: string): string => { + const startUpload = useCallback((collectionName: string, files: { file: File; id: string }[], totalBatches: number, isNewCollection: boolean, targetCollectionId?: string): { uploadId: string; abortController: AbortController } => { const uploadId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + const abortController = new AbortController() const uploadFiles: UploadFileStatus[] = files.map((file, index) => ({ id: file.id, @@ -74,11 +76,12 @@ export const UploadProgressProvider: React.FC = ({ }, isNewCollection, targetCollectionId, - files: uploadFiles + files: uploadFiles, + abortController } setCurrentUpload(newUpload) - return uploadId + return { uploadId, abortController } }, []) const updateProgress = useCallback((uploadId: string, current: number, batch: number) => { @@ -121,6 +124,12 @@ export const UploadProgressProvider: React.FC = ({ const cancelUpload = useCallback((uploadId: string) => { setCurrentUpload(prev => { if (!prev || prev.id !== uploadId) return prev + + // Abort all ongoing requests + if (prev.abortController) { + prev.abortController.abort() + } + return null }) }, []) diff --git a/frontend/src/routes/_authenticated/knowledgeManagement.tsx b/frontend/src/routes/_authenticated/knowledgeManagement.tsx index f0d9b5ee1..66c621860 100644 --- a/frontend/src/routes/_authenticated/knowledgeManagement.tsx +++ b/frontend/src/routes/_authenticated/knowledgeManagement.tsx @@ -541,7 +541,7 @@ function KnowledgeManagementContent() { // Start the global upload progress const batches = createBatches(selectedFiles, collectionName.trim()) const files = selectedFiles.map(f => ({ file: f.file, id: f.id })) - const uploadId = startUpload(collectionName.trim(), files, batches.length, true) + const { uploadId, abortController } = startUpload(collectionName.trim(), files, batches.length, true) // Close the modal immediately after starting upload handleCloseModal() @@ -553,6 +553,7 @@ function KnowledgeManagementContent() { let totalSuccessful = 0 let totalSkipped = 0 let totalFailed = 0 + let processed = 0 for (let i = 0; i < batches.length; i++) { const batchFiles = batches[i].map((f) => ({ file: f.file, id: f.id })) @@ -562,7 +563,7 @@ function KnowledgeManagementContent() { updateFileStatus(uploadId, file.file.name, file.id, 'uploading') }) - const uploadResult = await uploadFileBatch(batchFiles.map((f) => f.file), cl.id) + const uploadResult = await uploadFileBatch(batchFiles.map((f) => f.file), cl.id, null, abortController.signal) // Update individual file statuses based on results if (uploadResult.results) { @@ -589,8 +590,8 @@ function KnowledgeManagementContent() { } // Update progress - const newCurrent = (i + 1) * batchFiles.length - updateProgress(uploadId, newCurrent, i + 1) + processed += batchFiles.length + updateProgress(uploadId, processed, i + 1) } // Fetch the updated Collection data from the backend @@ -674,10 +675,19 @@ function KnowledgeManagementContent() { }) } catch (error) { console.error("Upload failed:", error) - toast.error({ - title: "Upload Failed", - description: "Failed to create collection. Please try again.", - }) + + // Check if the error is due to cancellation + if (error instanceof Error && error.name === 'AbortError') { + toast({ + title: "Upload Cancelled", + description: "File upload was cancelled by user.", + }) + } else { + toast.error({ + title: "Upload Failed", + description: "Failed to create collection. Please try again.", + }) + } } finally { finishUpload(uploadId) } @@ -724,7 +734,7 @@ function KnowledgeManagementContent() { // Start the global upload progress const batches = createBatches(selectedFiles, addingToCollection.name) const files = selectedFiles.map(f => ({ file: f.file, id: f.id })) - const uploadId = startUpload(addingToCollection.name, files, batches.length, false, addingToCollection.id) + const { uploadId, abortController } = startUpload(addingToCollection.name, files, batches.length, false, addingToCollection.id) // Close the modal immediately after starting upload handleCloseModal() @@ -734,6 +744,7 @@ function KnowledgeManagementContent() { let totalSuccessful = 0 let totalSkipped = 0 let totalFailed = 0 + let processed = 0 for (let i = 0; i < batches.length; i++) { const batchFiles = batches[i].map((f) => ({ file: f.file, id: f.id })) @@ -747,6 +758,7 @@ function KnowledgeManagementContent() { batchFiles.map(f => f.file), addingToCollection.id, targetFolder?.id, + abortController.signal, ) // Update individual file statuses based on results @@ -774,8 +786,8 @@ function KnowledgeManagementContent() { } // Update progress - const newCurrent = (i + 1) * batchFiles.length - updateProgress(uploadId, newCurrent, i + 1) + processed += batchFiles.length + updateProgress(uploadId, processed, i + 1) } // Refresh the collection by fetching updated data from backend @@ -860,10 +872,19 @@ function KnowledgeManagementContent() { handleCloseModal() } catch (error) { console.error("Add files failed:", error) - toast.error({ - title: "Add Files Failed", - description: "Failed to add files to collection. Please try again.", - }) + + // Check if the error is due to cancellation + if (error instanceof Error && error.name === 'AbortError') { + toast({ + title: "Upload Cancelled", + description: "File upload was cancelled by user.", + }) + } else { + toast.error({ + title: "Add Files Failed", + description: "Failed to add files to collection. Please try again.", + }) + } } finally { finishUpload(uploadId) } diff --git a/frontend/src/utils/fileUtils.ts b/frontend/src/utils/fileUtils.ts index 656d53d38..613b50069 100644 --- a/frontend/src/utils/fileUtils.ts +++ b/frontend/src/utils/fileUtils.ts @@ -206,6 +206,7 @@ export const uploadFileBatch = async ( files: File[], collectionId: string, parentId?: string | null, + abortSignal?: AbortSignal, ): Promise => { const formData = new FormData() @@ -228,6 +229,7 @@ export const uploadFileBatch = async ( { method: "POST", body: formData, + signal: abortSignal, }, ) From 753cf416a8fdf86e363fe1751c10ef8dedcc09fe Mon Sep 17 00:00:00 2001 From: Rahul Kumar Date: Mon, 29 Sep 2025 12:22:10 +0530 Subject: [PATCH 4/4] refactor: XYN-298 resolve comment --- frontend/src/components/ClFileUpload.tsx | 55 +---------------------- frontend/src/lib/common.tsx | 57 +++++++++++++++++++++++- 2 files changed, 58 insertions(+), 54 deletions(-) diff --git a/frontend/src/components/ClFileUpload.tsx b/frontend/src/components/ClFileUpload.tsx index 40c5f54f1..b00038623 100644 --- a/frontend/src/components/ClFileUpload.tsx +++ b/frontend/src/components/ClFileUpload.tsx @@ -1,60 +1,9 @@ import { useState, useCallback, useRef, ChangeEvent } from "react" -import { File as FileIcon, X, FileUp } from "lucide-react" +import { X, FileUp } from "lucide-react" import { Button } from "@/components/ui/button" import FileUploadSkeleton from "@/components/FileUploadSkeleton" -import { FileType, MIME_TYPE_MAPPINGS, EXTENSION_MAPPINGS } from "../../../server/shared/types" import { isValidFile } from "../../../server/shared/fileUtils" - -// SVG icon imports -import TextIcon from "@/assets/collectionIcons/text.svg" -import ImageIcon from "@/assets/collectionIcons/image.svg" -import PdfIcon from "@/assets/collectionIcons/pdf.svg" -import DocumentIcon from "@/assets/collectionIcons/document.svg" -import SpreadsheetIcon from "@/assets/collectionIcons/spreadsheet.svg" -import PresentationIcon from "@/assets/collectionIcons/ppt.svg" - -// Helper function to determine FileType from a file -const getFileType = (file: File): FileType => { - // First check by MIME type - for (const [fileType, mimeTypes] of Object.entries(MIME_TYPE_MAPPINGS)) { - if ((mimeTypes as readonly string[]).includes(file.type)) { - return fileType as FileType - } - } - - // Fallback to extension checking - const fileName = file.name.toLowerCase() - for (const [fileType, extensions] of Object.entries(EXTENSION_MAPPINGS)) { - if ((extensions as readonly string[]).some(ext => fileName.endsWith(ext))) { - return fileType as FileType - } - } - - // Default fallback - return FileType.FILE -} - -// Icon mapping from FileType to SVG component -const getFileIcon = (file: File) => { - const fileType = getFileType(file) - - switch (fileType) { - case FileType.TEXT: - return Text file - case FileType.IMAGE: - return Image file - case FileType.PDF: - return PDF file - case FileType.DOCUMENT: - return Document file - case FileType.SPREADSHEET: - return Spreadsheet file - case FileType.PRESENTATION: - return Presentation file - default: - return - } -} +import { getFileIcon } from "@/lib/common" export interface SelectedFile { file: File diff --git a/frontend/src/lib/common.tsx b/frontend/src/lib/common.tsx index 898240579..5470fa386 100644 --- a/frontend/src/lib/common.tsx +++ b/frontend/src/lib/common.tsx @@ -8,7 +8,8 @@ import { Github, BookOpen, Globe, -} from "lucide-react" // Added FileText, CalendarDays, PlugZap, Github, BookOpen + File as FileIcon, +} from "lucide-react" // Added FileText, CalendarDays, PlugZap, Github, BookOpen, File import DocsSvg from "@/assets/docs.svg" // Added this line import SlidesSvg from "@/assets/slides.svg" import SheetsSvg from "@/assets/sheets.svg" @@ -21,6 +22,14 @@ import Slides from "@/assets/slides.svg" import Image from "@/assets/images.svg" import GoogleCalendarSvg from "@/assets/googleCalendar.svg" import SlackSvg from "@/assets/slack.svg" + +// Collection icon imports for file upload +import TextIcon from "@/assets/collectionIcons/text.svg" +import ImageIcon from "@/assets/collectionIcons/image.svg" +import PdfIcon from "@/assets/collectionIcons/pdf.svg" +import DocumentIcon from "@/assets/collectionIcons/document.svg" +import SpreadsheetIcon from "@/assets/collectionIcons/spreadsheet.svg" +import PresentationIcon from "@/assets/collectionIcons/ppt.svg" import type { Entity } from "shared/types" import { Apps, @@ -33,6 +42,9 @@ import { SystemEntity, DataSourceEntity, WebSearchEntity, + FileType, + MIME_TYPE_MAPPINGS, + EXTENSION_MAPPINGS, } from "shared/types" import { LoadingSpinner } from "@/routes/_authenticated/admin/integrations/google" @@ -153,6 +165,49 @@ export const getIcon = ( } } +// Helper function to determine FileType from a file +export const getFileType = (file: File): FileType => { + // First check by MIME type + for (const [fileType, mimeTypes] of Object.entries(MIME_TYPE_MAPPINGS)) { + if ((mimeTypes as readonly string[]).includes(file.type)) { + return fileType as FileType + } + } + + // Fallback to extension checking + const fileName = file.name.toLowerCase() + for (const [fileType, extensions] of Object.entries(EXTENSION_MAPPINGS)) { + if ((extensions as readonly string[]).some(ext => fileName.endsWith(ext))) { + return fileType as FileType + } + } + + // Default fallback + return FileType.FILE +} + +// Icon mapping from FileType to SVG component +export const getFileIcon = (file: File) => { + const fileType = getFileType(file) + + switch (fileType) { + case FileType.TEXT: + return Text file + case FileType.IMAGE: + return Image file + case FileType.PDF: + return PDF file + case FileType.DOCUMENT: + return Document file + case FileType.SPREADSHEET: + return Spreadsheet file + case FileType.PRESENTATION: + return Presentation file + default: + return + } +} + export const minHeight = 320 export const LoaderContent = () => { return (