diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 12f8a261f7..5a4ec91541 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -16,7 +16,7 @@ import { onMount, tick, getContext, createEventDispatcher } from 'svelte'; import { createPicker, getAuthToken } from '$lib/utils/google-drive-picker'; - import { pickAndDownloadFile } from '$lib/utils/onedrive-file-picker'; + import { pickAndDownloadFiles } from '$lib/utils/onedrive-file-picker'; import { KokoroWorker } from '$lib/workers/KokoroWorker'; const dispatch = createEventDispatcher(); @@ -2216,17 +2216,47 @@ }} uploadOneDriveHandler={async (authorityType) => { try { - const fileData = await pickAndDownloadFile(authorityType); - if (fileData) { - const file = new File([fileData.blob], fileData.name, { - type: fileData.blob.type || 'application/octet-stream' - }); - await uploadFileHandler(file); + const maxFileCount = $config?.file?.max_count; + const filesData = await pickAndDownloadFiles( + authorityType, + maxFileCount + ); + if (filesData && filesData.length > 0) { + if (filesData.length > 1) { + toast.success( + $i18n.t('Uploading {{count}} files from OneDrive...', { + count: filesData.length + }) + ); + } + + for (const fileData of filesData) { + const file = new File([fileData.blob], fileData.name, { + type: fileData.blob.type || 'application/octet-stream' + }); + await uploadFileHandler(file); + } } else { - console.log('No file was selected from OneDrive'); + console.log('No files were selected from OneDrive'); } } catch (error) { console.error('OneDrive Error:', error); + // Handle specific max file count error + if (error.message?.startsWith('MAX_FILE_COUNT_EXCEEDED:')) { + const [, count, max] = error.message.split(':'); + toast.error( + $i18n.t('Selected items contain {{count}} files, but maximum is {{max}}.', { + count, + max + }) + ); + } else { + toast.error( + $i18n.t('OneDrive Error: {{error}}', { + error: error.message + }) + ); + } } }} {onUpload} diff --git a/src/lib/utils/onedrive-file-picker.ts b/src/lib/utils/onedrive-file-picker.ts index 198a8bcafb..b7882a7551 100644 --- a/src/lib/utils/onedrive-file-picker.ts +++ b/src/lib/utils/onedrive-file-picker.ts @@ -180,6 +180,9 @@ interface PickerParams { search: { enabled: boolean; }; + selection?: { + mode?: 'single' | 'multiple' | 'pick'; + }; typesAndSources: { mode: string; pivots: Record; @@ -211,12 +214,18 @@ function getPickerParams(): PickerParams { search: { enabled: true }, + selection: { + mode: 'multiple' + }, typesAndSources: { - mode: 'files', + mode: 'all', pivots: { oneDrive: true, recent: true, - myOrganization: config.getAuthorityType() === 'organizations' + shared: true, + sharedLibraries: true, + myOrganization: config.getAuthorityType() === 'organizations', + site: config.getAuthorityType() === 'organizations' } } }; @@ -236,10 +245,66 @@ interface OneDriveFileInfo { driveId: string; }; '@sharePoint.endpoint': string; + folder?: { + childCount?: number; + }; + file?: { + mimeType?: string; + }; // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } +// List all items in a folder recursively +async function listFolderContentsRecursive( + folderInfo: OneDriveFileInfo, + authorityType?: 'personal' | 'organizations', + currentPath: string = '' +): Promise { + const accessToken = await getToken(undefined, authorityType); + if (!accessToken) { + throw new Error('Unable to retrieve OneDrive access token.'); + } + + const childrenUrl = `${folderInfo['@sharePoint.endpoint']}/drives/${folderInfo.parentReference.driveId}/items/${folderInfo.id}/children`; + + const response = await fetch(childrenUrl, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + + if (!response.ok) { + throw new Error(`Failed to list folder contents: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + const items: OneDriveFileInfo[] = data.value || []; + + const allFiles: OneDriveFileInfo[] = []; + + // Process all items + for (const item of items) { + // Inherit the @sharePoint.endpoint from parent folder if not present + if (!item['@sharePoint.endpoint']) { + item['@sharePoint.endpoint'] = folderInfo['@sharePoint.endpoint']; + } + + if (item.folder) { + // Recursively get files from subfolder + const subPath = currentPath ? `${currentPath}/${item.name}` : item.name; + const subfolderFiles = await listFolderContentsRecursive(item, authorityType, subPath); + allFiles.push(...subfolderFiles); + } else if (item.file) { + // It's a file, add it to the list + allFiles.push(item); + } + // Skip items that are neither files nor folders (e.g., packages, OneNote notebooks) + } + + return allFiles; +} + // Download file from OneDrive async function downloadOneDriveFile( fileInfo: OneDriveFileInfo, @@ -435,20 +500,69 @@ export async function openOneDrivePicker( }); } -// Pick and download file from OneDrive -export async function pickAndDownloadFile( - authorityType?: 'personal' | 'organizations' -): Promise<{ blob: Blob; name: string } | null> { +// Pick and download multiple files from OneDrive (with folder support) +export async function pickAndDownloadFiles( + authorityType?: 'personal' | 'organizations', + maxFileCount?: number +): Promise<{ blob: Blob; name: string }[] | null> { const pickerResult = await openOneDrivePicker(authorityType); if (!pickerResult || !pickerResult.items || pickerResult.items.length === 0) { return null; } - const selectedFile = pickerResult.items[0]; - const blob = await downloadOneDriveFile(selectedFile, authorityType); + const allFiles: OneDriveFileInfo[] = []; - return { blob, name: selectedFile.name }; + // First, expand folders to get all files + for (const item of pickerResult.items) { + if (item.folder) { + // It's a folder, get all files recursively + const folderFiles = await listFolderContentsRecursive(item, authorityType, item.name); + allFiles.push(...folderFiles); + } else { + // It's a file + allFiles.push(item); + } + } + + // Check if any files were actually found + if (allFiles.length === 0) { + throw new Error('No files found in the selected items.'); + } + + // Check if the number of files exceeds the configured maximum + if (maxFileCount && allFiles.length > maxFileCount) { + throw new Error( + `MAX_FILE_COUNT_EXCEEDED:${allFiles.length}:${maxFileCount}` + ); + } + + // Download all files with error handling + const downloadPromises = allFiles.map(async (fileInfo) => { + const blob = await downloadOneDriveFile(fileInfo, authorityType); + return { + blob, + name: fileInfo.name + }; + }); + + const results = await Promise.allSettled(downloadPromises); + const successful = results + .filter((r): r is PromiseFulfilledResult<{ blob: Blob; name: string }> => r.status === 'fulfilled') + .map((r) => r.value); + + const failed = results.filter((r) => r.status === 'rejected'); + + if (failed.length > 0) { + console.warn(`Failed to download ${failed.length} out of ${allFiles.length} files from OneDrive`); + // If all downloads failed, throw error + if (successful.length === 0) { + throw new Error('Failed to download any files from OneDrive'); + } + // If some succeeded, log warning but continue with successful ones + } + + return successful.length > 0 ? successful : null; } export { downloadOneDriveFile };