import { useState, useEffect, useRef } from 'react'; import { exchangeDropboxCode, getDropboxFolder } from '../api/client'; const DROPBOX_APP_KEY = import.meta.env.VITE_DROPBOX_APP_KEY; const REDIRECT_URI = window.location.origin; // Supported file extensions (Docling supports many formats) const SUPPORTED_EXTENSIONS = ['.txt', '.md', '.pdf', '.docx', '.pptx', '.xlsx', '.html', '.htm', '.jpg', '.jpeg', '.png', '.bmp', '.tiff']; const MAX_FILE_SIZE_MB = 10; const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024; export default function CloudConnect({ onFilesStaged, stagedFiles = [], onAccessTokenChange }) { const [isSignedIn, setIsSignedIn] = useState(false); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [accessToken, setAccessToken] = useState(null); const [showPicker, setShowPicker] = useState(false); const [files, setFiles] = useState([]); const [folders, setFolders] = useState([]); const [currentPath, setCurrentPath] = useState(''); const [pathStack, setPathStack] = useState([]); const [pickerSelectedFiles, setPickerSelectedFiles] = useState([]); const [loadingFiles, setLoadingFiles] = useState(false); const popupRef = useRef(null); const popupCheckInterval = useRef(null); // Listen for OAuth callback message from popup useEffect(() => { const handleMessage = async (event) => { // Verify origin for security if (event.origin !== window.location.origin) return; if (event.data?.type === 'DROPBOX_AUTH_CODE') { const { code, error: authError } = event.data; // Close popup if (popupRef.current && !popupRef.current.closed) { popupRef.current.close(); } clearInterval(popupCheckInterval.current); if (authError) { setError(`Dropbox error: ${authError}`); setIsLoading(false); return; } if (code) { try { const data = await exchangeDropboxCode(code, REDIRECT_URI); if (data.access_token) { setAccessToken(data.access_token); setIsSignedIn(true); onAccessTokenChange?.(data.access_token); } else { setError(data.error || 'Failed to get access token'); } } catch (err) { setError(err.message); } setIsLoading(false); } } }; window.addEventListener('message', handleMessage); return () => window.removeEventListener('message', handleMessage); }, [onAccessTokenChange]); // Check if this is the OAuth callback page (popup) useEffect(() => { const params = new URLSearchParams(window.location.search); const code = params.get('code'); const errorParam = params.get('error'); // If we have code/error and we're in a popup, send message to parent if ((code || errorParam) && window.opener) { window.opener.postMessage({ type: 'DROPBOX_AUTH_CODE', code: code, error: errorParam }, window.location.origin); window.close(); } }, []); // Fetch folder contents const fetchFolder = async (path) => { setLoadingFiles(true); setError(null); try { const data = await getDropboxFolder(path, accessToken); if (data.error) { setError(data.error); setLoadingFiles(false); return; } const entries = data.entries || []; const folderItems = entries.filter(item => item['.tag'] === 'folder'); const fileItems = entries.filter(item => item['.tag'] === 'file'); setFolders(folderItems); setFiles(fileItems); } catch (err) { setError(`Failed to load files: ${err.message}`); } setLoadingFiles(false); }; // Check if file is supported const isFileSupported = (file) => { const name = file.name.toLowerCase(); return SUPPORTED_EXTENSIONS.some(ext => name.endsWith(ext)); }; // Check if file size is within limit const isFileSizeOk = (file) => { return file.size <= MAX_FILE_SIZE_BYTES; }; // Open picker const openPicker = () => { setShowPicker(true); setPickerSelectedFiles([]); setCurrentPath(''); setPathStack([]); fetchFolder(''); }; // Navigate to folder const navigateToFolder = (folder) => { setPathStack([...pathStack, { path: currentPath, name: currentPath || 'Dropbox' }]); setCurrentPath(folder.path_lower); fetchFolder(folder.path_lower); }; // Go back const goBack = () => { if (pathStack.length > 0) { const prev = pathStack[pathStack.length - 1]; setPathStack(pathStack.slice(0, -1)); setCurrentPath(prev.path); fetchFolder(prev.path); } }; // Toggle file selection in picker const toggleFile = (file) => { if (!isFileSupported(file)) return; if (!isFileSizeOk(file)) return; if (pickerSelectedFiles.find(f => f.id === file.id)) { setPickerSelectedFiles(pickerSelectedFiles.filter(f => f.id !== file.id)); } else { setPickerSelectedFiles([...pickerSelectedFiles, file]); } }; // Confirm selection - adds to staged files const confirmSelection = () => { // Merge with existing staged files, avoiding duplicates const existingIds = new Set(stagedFiles.map(f => f.id)); const newFiles = pickerSelectedFiles.filter(f => !existingIds.has(f.id)); const merged = [...stagedFiles, ...newFiles]; onFilesStaged?.(merged); setShowPicker(false); }; const handleConnect = () => { if (!DROPBOX_APP_KEY) { setError('Dropbox App Key not configured'); return; } setIsLoading(true); setError(null); const authUrl = new URL('https://www.dropbox.com/oauth2/authorize'); authUrl.searchParams.set('client_id', DROPBOX_APP_KEY); authUrl.searchParams.set('response_type', 'code'); authUrl.searchParams.set('redirect_uri', REDIRECT_URI); authUrl.searchParams.set('token_access_type', 'offline'); // Open popup window for OAuth const width = 500; const height = 700; const left = window.screenX + (window.outerWidth - width) / 2; const top = window.screenY + (window.outerHeight - height) / 2; popupRef.current = window.open( authUrl.toString(), 'dropbox-auth', `width=${width},height=${height},left=${left},top=${top},toolbar=no,menubar=no,scrollbars=yes` ); // Check if popup was blocked if (!popupRef.current || popupRef.current.closed) { setError('Popup blocked. Please allow popups for this site.'); setIsLoading(false); return; } // Monitor popup - if user closes it manually popupCheckInterval.current = setInterval(() => { if (popupRef.current && popupRef.current.closed) { clearInterval(popupCheckInterval.current); setIsLoading(false); } }, 500); }; const handleDisconnect = () => { setAccessToken(null); setIsSignedIn(false); setShowPicker(false); onAccessTokenChange?.(null); onFilesStaged?.([]); }; // Get display name for current path const getCurrentFolderName = () => { if (!currentPath) return 'Dropbox'; const parts = currentPath.split('/'); return parts[parts.length - 1] || 'Dropbox'; }; // Format file size const formatSize = (bytes) => { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; }; if (!DROPBOX_APP_KEY) { return (
Dropbox not configured
Set VITE_DROPBOX_APP_KEY in environment
PDF, DOCX, PPTX, XLSX, HTML, images (max {MAX_FILE_SIZE_MB} MB)
PDF, DOCX, PPTX, XLSX, HTML, images (max {MAX_FILE_SIZE_MB} MB)
This folder is empty