diff --git a/frontend/src/app/upload/[provider]/page.tsx b/frontend/src/app/upload/[provider]/page.tsx index c00391f2..000c9202 100644 --- a/frontend/src/app/upload/[provider]/page.tsx +++ b/frontend/src/app/upload/[provider]/page.tsx @@ -3,11 +3,11 @@ import { useState, useEffect } from "react" import { useParams, useRouter } from "next/navigation" import { Button } from "@/components/ui/button" -import { Badge } from "@/components/ui/badge" import { ArrowLeft, AlertCircle } from "lucide-react" import { GoogleDrivePicker } from "@/components/google-drive-picker" import { OneDrivePicker } from "@/components/onedrive-picker" import { useTask } from "@/contexts/task-context" +import { Toast } from "@/components/ui/toast" interface GoogleDriveFile { id: string @@ -43,15 +43,16 @@ export default function UploadProviderPage() { const params = useParams() const router = useRouter() const provider = params.provider as string - const { addTask } = useTask() + 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 [isSyncing, setIsSyncing] = useState(false) - const [syncResult, setSyncResult] = useState(null) + const [isIngesting, setIsIngesting] = useState(false) + const [currentSyncTaskId, setCurrentSyncTaskId] = useState(null) + const [showSuccessToast, setShowSuccessToast] = useState(false) useEffect(() => { const fetchConnectorInfo = async () => { @@ -130,6 +131,26 @@ export default function UploadProviderPage() { } }, [provider]) + // Watch for sync task completion and redirect + useEffect(() => { + if (!currentSyncTaskId) return + + const currentTask = tasks.find(task => task.task_id === currentSyncTaskId) + + if (currentTask && currentTask.status === 'completed') { + // Task completed successfully, show toast and redirect + setIsIngesting(false) + setShowSuccessToast(true) + 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: GoogleDriveFile[] | OneDriveFile[]) => { setSelectedFiles(files) console.log(`Selected ${files.length} files from ${provider}:`, files) @@ -139,8 +160,7 @@ export default function UploadProviderPage() { const handleSync = async (connector: CloudConnector) => { if (!connector.connectionId || selectedFiles.length === 0) return - setIsSyncing(true) - setSyncResult(null) + setIsIngesting(true) try { const syncBody: { @@ -163,26 +183,18 @@ export default function UploadProviderPage() { const result = await response.json() if (response.status === 201) { - const taskId = result.task_id - if (taskId) { + const taskIds = result.task_ids + if (taskIds && taskIds.length > 0) { + const taskId = taskIds[0] // Use the first task ID addTask(taskId) - setSyncResult({ - processed: 0, - total: selectedFiles.length, - status: 'started' - }) + setCurrentSyncTaskId(taskId) } - } else if (response.ok) { - setSyncResult(result) } else { console.error('Sync failed:', result.error) - setSyncResult({ error: result.error || 'Sync failed' }) } } catch (error) { console.error('Sync error:', error) - setSyncResult({ error: 'Network error occurred' }) - } finally { - setIsSyncing(false) + setIsIngesting(false) } } @@ -297,26 +309,18 @@ export default function UploadProviderPage() { } return ( -
-
+
+
- -
-

Select Files from {connector.name}

-

- Choose specific files from your {connector.name} account to add to your knowledge base. -

-
+

Add Cloud Knowledge

-
+
{connector.type === "google_drive" && ( {selectedFiles.length > 0 && ( -
-
+
+
-
- - {syncResult && ( -
- {syncResult.error ? ( -
Error: {syncResult.error}
- ) : syncResult.status === 'started' ? ( -
- Sync started for {syncResult.total} files. Check the task notification for progress. -
- ) : ( -
-
Processed: {syncResult.processed || 0}
-
Added: {syncResult.added || 0}
- {syncResult.errors &&
Errors: {syncResult.errors}
} -
- )} -
- )}
)} + + {/* Success toast notification */} + setShowSuccessToast(false)} + duration={20000} + />
) } \ No newline at end of file diff --git a/frontend/src/components/google-drive-picker.tsx b/frontend/src/components/google-drive-picker.tsx index 60191261..c9dee19a 100644 --- a/frontend/src/components/google-drive-picker.tsx +++ b/frontend/src/components/google-drive-picker.tsx @@ -3,7 +3,8 @@ import { useState, useEffect } from "react" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" -import { FileText, Folder, X } from "lucide-react" +import { FileText, Folder, Plus, Trash2 } from "lucide-react" +import { Card, CardContent } from "@/components/ui/card" interface GoogleDrivePickerProps { onFileSelected: (files: GoogleDriveFile[]) => void @@ -19,6 +20,9 @@ interface GoogleDriveFile { mimeType: string webViewLink?: string iconLink?: string + size?: number + modifiedTime?: string + isFolder?: boolean } interface GoogleAPI { @@ -174,17 +178,52 @@ export function GoogleDrivePicker({ } } - const pickerCallback = (data: GooglePickerData) => { + const pickerCallback = async (data: GooglePickerData) => { if (data.action === window.google.picker.Action.PICKED) { const files: GoogleDriveFile[] = data.docs.map((doc: GooglePickerDocument) => ({ id: doc[window.google.picker.Document.ID], name: doc[window.google.picker.Document.NAME], mimeType: doc[window.google.picker.Document.MIME_TYPE], webViewLink: doc[window.google.picker.Document.URL], - iconLink: doc[window.google.picker.Document.ICON_URL] + iconLink: doc[window.google.picker.Document.ICON_URL], + size: doc['sizeBytes'] ? parseInt(doc['sizeBytes']) : undefined, + modifiedTime: doc['lastEditedUtc'], + isFolder: doc[window.google.picker.Document.MIME_TYPE] === 'application/vnd.google-apps.folder' })) - onFileSelected(files) + // If size is still missing, try to fetch it via Google Drive API + if (accessToken && files.some(f => !f.size && !f.isFolder)) { + try { + const enrichedFiles = await Promise.all(files.map(async (file) => { + if (!file.size && !file.isFolder) { + try { + const response = await fetch(`https://www.googleapis.com/drive/v3/files/${file.id}?fields=size,modifiedTime`, { + headers: { + 'Authorization': `Bearer ${accessToken}` + } + }) + if (response.ok) { + const fileDetails = await response.json() + return { + ...file, + size: fileDetails.size ? parseInt(fileDetails.size) : undefined, + modifiedTime: fileDetails.modifiedTime || file.modifiedTime + } + } + } catch (error) { + console.warn('Failed to fetch file details:', error) + } + } + return file + })) + onFileSelected(enrichedFiles) + } catch (error) { + console.warn('Failed to enrich file data:', error) + onFileSelected(files) + } + } else { + onFileSelected(files) + } } setIsPickerOpen(false) @@ -218,6 +257,14 @@ export function GoogleDrivePicker({ return typeMap[mimeType] || 'Document' } + const formatFileSize = (bytes?: number) => { + if (!bytes) return '' + const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] + if (bytes === 0) return '0 B' + const i = Math.floor(Math.log(bytes) / Math.log(1024)) + return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}` + } + if (!isAuthenticated) { return (
@@ -228,29 +275,38 @@ export function GoogleDrivePicker({ return (
-
-
-

File Selection

-

- Choose specific files to sync instead of syncing everything + + +

+ Select files from Google Drive to ingest.

-
- -
+ + + {selectedFiles.length > 0 && (
-

- Selected files ({selectedFiles.length}): -

-
+
+

+ Added files +

+ +
+
{selectedFiles.map((file) => (
- +
+ {formatFileSize(file.size)} + +
))}
- +
)}
diff --git a/frontend/src/components/onedrive-picker.tsx b/frontend/src/components/onedrive-picker.tsx index b40650a7..6d4cfc78 100644 --- a/frontend/src/components/onedrive-picker.tsx +++ b/frontend/src/components/onedrive-picker.tsx @@ -3,7 +3,7 @@ import { useState, useEffect } from "react" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" -import { FileText, Folder, X } from "lucide-react" +import { FileText, Folder, Trash2, X } from "lucide-react" interface OneDrivePickerProps { onFileSelected: (files: OneDriveFile[]) => void @@ -283,7 +283,7 @@ export function OneDrivePicker({

Selected files ({selectedFiles.length}):

-
+
{selectedFiles.map((file) => (
- +
))} diff --git a/frontend/src/components/ui/toast.tsx b/frontend/src/components/ui/toast.tsx new file mode 100644 index 00000000..4d765f49 --- /dev/null +++ b/frontend/src/components/ui/toast.tsx @@ -0,0 +1,39 @@ +"use client" + +import { useState, useEffect } from 'react' +import { Check } from 'lucide-react' + +interface ToastProps { + message: string + show: boolean + onHide?: () => void + duration?: number +} + +export function Toast({ message, show, onHide, duration = 3000 }: ToastProps) { + const [isVisible, setIsVisible] = useState(show) + + useEffect(() => { + setIsVisible(show) + + if (show && duration > 0) { + const timer = setTimeout(() => { + setIsVisible(false) + onHide?.() + }, duration) + + return () => clearTimeout(timer) + } + }, [show, duration, onHide]) + + if (!isVisible) return null + + return ( +
+
+ + {message} +
+
+ ) +} \ No newline at end of file