import { useState, useCallback } from 'react'; import { toast } from 'react-toastify'; import { ImportExportService } from '~/lib/services/importExportService'; import { useIndexedDB } from '~/lib/hooks/useIndexedDB'; import { generateId } from 'ai'; interface UseDataOperationsProps { /** * Callback to reload settings after import */ onReloadSettings?: () => void; /** * Callback to reload chats after import */ onReloadChats?: () => void; /** * Callback to reset settings to defaults */ onResetSettings?: () => void; /** * Callback to reset chats */ onResetChats?: () => void; /** * Custom database instance (optional) */ customDb?: IDBDatabase; } /** * Hook for managing data operations in the DataTab */ export function useDataOperations({ onReloadSettings, onReloadChats, onResetSettings, onResetChats, customDb, }: UseDataOperationsProps = {}) { const { db: defaultDb } = useIndexedDB(); // Use the custom database if provided, otherwise use the default const db = customDb || defaultDb; const [isExporting, setIsExporting] = useState(false); const [isImporting, setIsImporting] = useState(false); const [isResetting, setIsResetting] = useState(false); const [isDownloadingTemplate, setIsDownloadingTemplate] = useState(false); const [progressMessage, setProgressMessage] = useState(''); const [progressPercent, setProgressPercent] = useState(0); const [lastOperation, setLastOperation] = useState<{ type: string; data: any } | null>(null); /** * Show progress toast with percentage */ const showProgress = useCallback((message: string, percent: number) => { setProgressMessage(message); setProgressPercent(percent); toast.loading(`${message} (${percent}%)`, { toastId: 'operation-progress' }); }, []); /** * Export all settings to a JSON file */ const handleExportSettings = useCallback(async () => { setIsExporting(true); setProgressPercent(0); toast.loading('Preparing settings export...', { toastId: 'operation-progress' }); try { // Step 1: Export settings showProgress('Exporting settings', 25); const settingsData = await ImportExportService.exportSettings(); // Step 2: Create blob showProgress('Creating file', 50); const blob = new Blob([JSON.stringify(settingsData, null, 2)], { type: 'application/json', }); // Step 3: Download file showProgress('Downloading file', 75); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'bolt-settings.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); // Step 4: Complete showProgress('Completing export', 100); toast.success('Settings exported successfully', { toastId: 'operation-progress' }); // Save operation for potential undo setLastOperation({ type: 'export-settings', data: settingsData }); } catch (error) { console.error('Error exporting settings:', error); toast.error(`Failed to export settings: ${error instanceof Error ? error.message : 'Unknown error'}`, { toastId: 'operation-progress', }); } finally { setIsExporting(false); setProgressPercent(0); setProgressMessage(''); } }, [showProgress]); /** * Export selected settings categories to a JSON file * @param categoryIds Array of category IDs to export */ const handleExportSelectedSettings = useCallback( async (categoryIds: string[]) => { if (!categoryIds || categoryIds.length === 0) { toast.error('No settings categories selected'); return; } setIsExporting(true); setProgressPercent(0); toast.loading(`Preparing export of ${categoryIds.length} settings categories...`, { toastId: 'operation-progress', }); try { // Step 1: Export all settings showProgress('Exporting settings', 20); const allSettings = await ImportExportService.exportSettings(); // Step 2: Filter settings by category showProgress('Filtering selected categories', 40); const filteredSettings: Record = { exportDate: allSettings.exportDate, }; // Add selected categories to filtered settings categoryIds.forEach((category) => { if (allSettings[category]) { filteredSettings[category] = allSettings[category]; } }); // Step 3: Create blob showProgress('Creating file', 60); const blob = new Blob([JSON.stringify(filteredSettings, null, 2)], { type: 'application/json', }); // Step 4: Download file showProgress('Downloading file', 80); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'bolt-settings-selected.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); // Step 5: Complete showProgress('Completing export', 100); toast.success(`${categoryIds.length} settings categories exported successfully`, { toastId: 'operation-progress', }); // Save operation for potential undo setLastOperation({ type: 'export-selected-settings', data: { categoryIds, settings: filteredSettings } }); } catch (error) { console.error('Error exporting selected settings:', error); toast.error(`Failed to export selected settings: ${error instanceof Error ? error.message : 'Unknown error'}`, { toastId: 'operation-progress', }); } finally { setIsExporting(false); setProgressPercent(0); setProgressMessage(''); } }, [showProgress], ); /** * Export all chats to a JSON file */ const handleExportAllChats = useCallback(async () => { if (!db) { toast.error('Database not available'); return; } console.log('Export: Using database', { name: db.name, version: db.version, objectStoreNames: Array.from(db.objectStoreNames), }); setIsExporting(true); setProgressPercent(0); toast.loading('Preparing chats export...', { toastId: 'operation-progress' }); try { // Step 1: Export chats showProgress('Retrieving chats from database', 25); console.log('Database details:', { name: db.name, version: db.version, objectStoreNames: Array.from(db.objectStoreNames), }); // Direct database query approach for more reliable access const directChats = await new Promise((resolve, reject) => { try { console.log(`Creating transaction on '${db.name}' database, objectStore 'chats'`); const transaction = db.transaction(['chats'], 'readonly'); const store = transaction.objectStore('chats'); const request = store.getAll(); request.onsuccess = () => { console.log(`Found ${request.result ? request.result.length : 0} chats directly from database`); resolve(request.result || []); }; request.onerror = () => { console.error('Error querying chats store:', request.error); reject(request.error); }; } catch (err) { console.error('Error creating transaction:', err); reject(err); } }); // Export data with direct chats const exportData = { chats: directChats, exportDate: new Date().toISOString(), }; // Step 2: Create blob showProgress('Creating file', 50); const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json', }); // Step 3: Download file showProgress('Downloading file', 75); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'bolt-chats.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); // Step 4: Complete showProgress('Completing export', 100); toast.success(`${exportData.chats.length} chats exported successfully`, { toastId: 'operation-progress' }); // Save operation for potential undo setLastOperation({ type: 'export-all-chats', data: exportData }); } catch (error) { console.error('Error exporting chats:', error); toast.error(`Failed to export chats: ${error instanceof Error ? error.message : 'Unknown error'}`, { toastId: 'operation-progress', }); } finally { setIsExporting(false); setProgressPercent(0); setProgressMessage(''); } }, [db, showProgress]); /** * Export selected chats to a JSON file * @param chatIds Array of chat IDs to export */ const handleExportSelectedChats = useCallback( async (chatIds: string[]) => { if (!db) { toast.error('Database not available'); return; } if (!chatIds || chatIds.length === 0) { toast.error('No chats selected'); return; } setIsExporting(true); setProgressPercent(0); toast.loading(`Preparing export of ${chatIds.length} chats...`, { toastId: 'operation-progress' }); try { // Step 1: Directly query each selected chat from database showProgress('Retrieving selected chats from database', 20); console.log('Database details for selected chats:', { name: db.name, version: db.version, objectStoreNames: Array.from(db.objectStoreNames), }); // Query each chat directly from the database const selectedChats = await Promise.all( chatIds.map(async (chatId) => { return new Promise((resolve, reject) => { try { const transaction = db.transaction(['chats'], 'readonly'); const store = transaction.objectStore('chats'); const request = store.get(chatId); request.onsuccess = () => { if (request.result) { console.log(`Found chat with ID ${chatId}:`, { id: request.result.id, messageCount: request.result.messages?.length || 0, }); } else { console.log(`Chat with ID ${chatId} not found`); } resolve(request.result || null); }; request.onerror = () => { console.error(`Error retrieving chat ${chatId}:`, request.error); reject(request.error); }; } catch (err) { console.error(`Error in transaction for chat ${chatId}:`, err); reject(err); } }); }), ); // Filter out any null results (chats that weren't found) const filteredChats = selectedChats.filter((chat) => chat !== null); console.log(`Found ${filteredChats.length} selected chats out of ${chatIds.length} requested`); // Step 2: Prepare export data showProgress('Preparing export data', 40); const exportData = { chats: filteredChats, exportDate: new Date().toISOString(), }; // Step 3: Create blob showProgress('Creating file', 60); const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json', }); // Step 4: Download file showProgress('Downloading file', 80); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'bolt-chats-selected.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); // Step 5: Complete showProgress('Completing export', 100); toast.success(`${filteredChats.length} chats exported successfully`, { toastId: 'operation-progress' }); // Save operation for potential undo setLastOperation({ type: 'export-selected-chats', data: { chatIds, chats: filteredChats } }); } catch (error) { console.error('Error exporting selected chats:', error); toast.error(`Failed to export selected chats: ${error instanceof Error ? error.message : 'Unknown error'}`, { toastId: 'operation-progress', }); } finally { setIsExporting(false); setProgressPercent(0); setProgressMessage(''); } }, [db, showProgress], ); /** * Import settings from a JSON file * @param file The file to import */ const handleImportSettings = useCallback( async (file: File) => { setIsImporting(true); setProgressPercent(0); toast.loading(`Importing settings from ${file.name}...`, { toastId: 'operation-progress' }); try { // Step 1: Read file showProgress('Reading file', 20); const fileContent = await file.text(); // Step 2: Parse JSON showProgress('Parsing settings data', 40); const importedData = JSON.parse(fileContent); // Step 3: Validate data showProgress('Validating settings data', 60); // Save current settings for potential undo const currentSettings = await ImportExportService.exportSettings(); setLastOperation({ type: 'import-settings', data: { previous: currentSettings } }); // Step 4: Import settings showProgress('Applying settings', 80); await ImportExportService.importSettings(importedData); // Step 5: Complete showProgress('Completing import', 100); toast.success('Settings imported successfully', { toastId: 'operation-progress' }); if (onReloadSettings) { onReloadSettings(); } } catch (error) { console.error('Error importing settings:', error); toast.error(`Failed to import settings: ${error instanceof Error ? error.message : 'Unknown error'}`, { toastId: 'operation-progress', }); } finally { setIsImporting(false); setProgressPercent(0); setProgressMessage(''); } }, [onReloadSettings, showProgress], ); /** * Import chats from a JSON file * @param file The file to import */ const handleImportChats = useCallback( async (file: File) => { if (!db) { toast.error('Database not available'); return; } setIsImporting(true); setProgressPercent(0); toast.loading(`Importing chats from ${file.name}...`, { toastId: 'operation-progress' }); try { // Step 1: Read file showProgress('Reading file', 20); const fileContent = await file.text(); // Step 2: Parse JSON and validate structure showProgress('Parsing chat data', 40); const importedData = JSON.parse(fileContent); if (!importedData.chats || !Array.isArray(importedData.chats)) { throw new Error('Invalid chat data format: missing or invalid chats array'); } // Step 3: Validate each chat object showProgress('Validating chat data', 60); const validatedChats = importedData.chats.map((chat: any) => { if (!chat.id || !Array.isArray(chat.messages)) { throw new Error('Invalid chat format: missing required fields'); } // Ensure each message has required fields const validatedMessages = chat.messages.map((msg: any) => { if (!msg.role || !msg.content) { throw new Error('Invalid message format: missing required fields'); } return { id: msg.id || generateId(), role: msg.role, content: msg.content, name: msg.name, function_call: msg.function_call, timestamp: msg.timestamp || Date.now(), }; }); return { id: chat.id, description: chat.description || '', messages: validatedMessages, timestamp: chat.timestamp || new Date().toISOString(), urlId: chat.urlId || null, metadata: chat.metadata || null, }; }); // Step 4: Save current chats for potential undo showProgress('Preparing database transaction', 70); const currentChats = await ImportExportService.exportAllChats(db); setLastOperation({ type: 'import-chats', data: { previous: currentChats } }); // Step 5: Import chats showProgress(`Importing ${validatedChats.length} chats`, 80); const transaction = db.transaction(['chats'], 'readwrite'); const store = transaction.objectStore('chats'); let processed = 0; for (const chat of validatedChats) { store.put(chat); processed++; if (processed % 5 === 0 || processed === validatedChats.length) { showProgress( `Imported ${processed} of ${validatedChats.length} chats`, 80 + (processed / validatedChats.length) * 20, ); } } await new Promise((resolve, reject) => { transaction.oncomplete = resolve; transaction.onerror = reject; }); // Step 6: Complete showProgress('Completing import', 100); toast.success(`${validatedChats.length} chats imported successfully`, { toastId: 'operation-progress' }); if (onReloadChats) { onReloadChats(); } } catch (error) { console.error('Error importing chats:', error); toast.error(`Failed to import chats: ${error instanceof Error ? error.message : 'Unknown error'}`, { toastId: 'operation-progress', }); } finally { setIsImporting(false); setProgressPercent(0); setProgressMessage(''); } }, [db, onReloadChats, showProgress], ); /** * Import API keys from a JSON file * @param file The file to import */ const handleImportAPIKeys = useCallback( async (file: File) => { setIsImporting(true); setProgressPercent(0); toast.loading(`Importing API keys from ${file.name}...`, { toastId: 'operation-progress' }); try { // Step 1: Read file showProgress('Reading file', 20); const fileContent = await file.text(); // Step 2: Parse JSON showProgress('Parsing API keys data', 40); const importedData = JSON.parse(fileContent); // Step 3: Validate data showProgress('Validating API keys data', 60); // Get current API keys from cookies for potential undo const apiKeysStr = document.cookie.split(';').find((row) => row.trim().startsWith('apiKeys=')); const currentApiKeys = apiKeysStr ? JSON.parse(decodeURIComponent(apiKeysStr.split('=')[1])) : {}; setLastOperation({ type: 'import-api-keys', data: { previous: currentApiKeys } }); // Step 4: Import API keys showProgress('Applying API keys', 80); const newKeys = ImportExportService.importAPIKeys(importedData); const apiKeysJson = JSON.stringify(newKeys); document.cookie = `apiKeys=${apiKeysJson}; path=/; max-age=31536000`; // Step 5: Complete showProgress('Completing import', 100); // Count how many keys were imported const keyCount = Object.keys(newKeys).length; const newKeyCount = Object.keys(newKeys).filter( (key) => !currentApiKeys[key] || currentApiKeys[key] !== newKeys[key], ).length; toast.success( `${keyCount} API keys imported successfully (${newKeyCount} new/updated)\n` + 'Note: Keys are stored in browser cookies. For server-side usage, add them to your .env.local file.', { toastId: 'operation-progress', autoClose: 5000 }, ); if (onReloadSettings) { onReloadSettings(); } } catch (error) { console.error('Error importing API keys:', error); toast.error(`Failed to import API keys: ${error instanceof Error ? error.message : 'Unknown error'}`, { toastId: 'operation-progress', }); } finally { setIsImporting(false); setProgressPercent(0); setProgressMessage(''); } }, [onReloadSettings, showProgress], ); /** * Reset all settings to default values */ const handleResetSettings = useCallback(async () => { setIsResetting(true); setProgressPercent(0); toast.loading('Resetting settings...', { toastId: 'operation-progress' }); try { if (db) { // Step 1: Save current settings for potential undo showProgress('Backing up current settings', 25); const currentSettings = await ImportExportService.exportSettings(); setLastOperation({ type: 'reset-settings', data: { previous: currentSettings } }); // Step 2: Reset settings showProgress('Resetting settings to defaults', 50); await ImportExportService.resetAllSettings(db); // Step 3: Complete showProgress('Completing reset', 100); toast.success('Settings reset successfully', { toastId: 'operation-progress' }); if (onResetSettings) { onResetSettings(); } } else { toast.error('Database not available', { toastId: 'operation-progress' }); } } catch (error) { console.error('Error resetting settings:', error); toast.error(`Failed to reset settings: ${error instanceof Error ? error.message : 'Unknown error'}`, { toastId: 'operation-progress', }); } finally { setIsResetting(false); setProgressPercent(0); setProgressMessage(''); } }, [db, onResetSettings, showProgress]); /** * Reset all chats */ const handleResetChats = useCallback(async () => { if (!db) { toast.error('Database not available'); return; } setIsResetting(true); setProgressPercent(0); toast.loading('Deleting all chats...', { toastId: 'operation-progress' }); try { // Step 1: Save current chats for potential undo showProgress('Backing up current chats', 25); const currentChats = await ImportExportService.exportAllChats(db); setLastOperation({ type: 'reset-chats', data: { previous: currentChats } }); // Step 2: Delete chats showProgress('Deleting chats from database', 50); await ImportExportService.deleteAllChats(db); // Step 3: Complete showProgress('Completing deletion', 100); toast.success('All chats deleted successfully', { toastId: 'operation-progress' }); if (onResetChats) { onResetChats(); } } catch (error) { console.error('Error resetting chats:', error); toast.error(`Failed to delete chats: ${error instanceof Error ? error.message : 'Unknown error'}`, { toastId: 'operation-progress', }); } finally { setIsResetting(false); setProgressPercent(0); setProgressMessage(''); } }, [db, onResetChats, showProgress]); /** * Download API keys template */ const handleDownloadTemplate = useCallback(async () => { setIsDownloadingTemplate(true); setProgressPercent(0); toast.loading('Preparing API keys template...', { toastId: 'operation-progress' }); try { // Step 1: Create template showProgress('Creating template', 50); const templateData = ImportExportService.createAPIKeysTemplate(); // Step 2: Download file showProgress('Downloading template', 75); const blob = new Blob([JSON.stringify(templateData, null, 2)], { type: 'application/json', }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'bolt-api-keys-template.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); // Step 3: Complete showProgress('Completing download', 100); toast.success('API keys template downloaded successfully', { toastId: 'operation-progress' }); } catch (error) { console.error('Error downloading template:', error); toast.error(`Failed to download template: ${error instanceof Error ? error.message : 'Unknown error'}`, { toastId: 'operation-progress', }); } finally { setIsDownloadingTemplate(false); setProgressPercent(0); setProgressMessage(''); } }, [showProgress]); /** * Export API keys to a JSON file */ const handleExportAPIKeys = useCallback(async () => { setIsExporting(true); setProgressPercent(0); toast.loading('Preparing API keys export...', { toastId: 'operation-progress' }); try { // Step 1: Get API keys from all sources showProgress('Retrieving API keys', 25); // Create a fetch request to get API keys from server const response = await fetch('/api/export-api-keys'); if (!response.ok) { throw new Error('Failed to retrieve API keys from server'); } const apiKeys = await response.json(); // Step 2: Create blob showProgress('Creating file', 50); const blob = new Blob([JSON.stringify(apiKeys, null, 2)], { type: 'application/json', }); // Step 3: Download file showProgress('Downloading file', 75); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'bolt-api-keys.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); // Step 4: Complete showProgress('Completing export', 100); toast.success('API keys exported successfully', { toastId: 'operation-progress' }); // Save operation for potential undo setLastOperation({ type: 'export-api-keys', data: apiKeys }); } catch (error) { console.error('Error exporting API keys:', error); toast.error(`Failed to export API keys: ${error instanceof Error ? error.message : 'Unknown error'}`, { toastId: 'operation-progress', }); } finally { setIsExporting(false); setProgressPercent(0); setProgressMessage(''); } }, [showProgress]); /** * Undo the last operation if possible */ const handleUndo = useCallback(async () => { if (!lastOperation || !db) { toast.error('Nothing to undo'); return; } toast.loading('Attempting to undo last operation...', { toastId: 'operation-progress' }); try { switch (lastOperation.type) { case 'import-settings': { // Restore previous settings await ImportExportService.importSettings(lastOperation.data.previous); toast.success('Settings import undone', { toastId: 'operation-progress' }); if (onReloadSettings) { onReloadSettings(); } break; } case 'import-chats': { // Delete imported chats and restore previous state await ImportExportService.deleteAllChats(db); // Reimport previous chats const transaction = db.transaction(['chats'], 'readwrite'); const store = transaction.objectStore('chats'); for (const chat of lastOperation.data.previous.chats) { store.put(chat); } await new Promise((resolve, reject) => { transaction.oncomplete = resolve; transaction.onerror = reject; }); toast.success('Chats import undone', { toastId: 'operation-progress' }); if (onReloadChats) { onReloadChats(); } break; } case 'reset-settings': { // Restore previous settings await ImportExportService.importSettings(lastOperation.data.previous); toast.success('Settings reset undone', { toastId: 'operation-progress' }); if (onReloadSettings) { onReloadSettings(); } break; } case 'reset-chats': { // Restore previous chats const chatTransaction = db.transaction(['chats'], 'readwrite'); const chatStore = chatTransaction.objectStore('chats'); for (const chat of lastOperation.data.previous.chats) { chatStore.put(chat); } await new Promise((resolve, reject) => { chatTransaction.oncomplete = resolve; chatTransaction.onerror = reject; }); toast.success('Chats deletion undone', { toastId: 'operation-progress' }); if (onReloadChats) { onReloadChats(); } break; } case 'import-api-keys': { // Restore previous API keys const previousAPIKeys = lastOperation.data.previous; const newKeys = ImportExportService.importAPIKeys(previousAPIKeys); const apiKeysJson = JSON.stringify(newKeys); document.cookie = `apiKeys=${apiKeysJson}; path=/; max-age=31536000`; toast.success('API keys import undone', { toastId: 'operation-progress' }); if (onReloadSettings) { onReloadSettings(); } break; } default: toast.error('Cannot undo this operation', { toastId: 'operation-progress' }); } // Clear the last operation after undoing setLastOperation(null); } catch (error) { console.error('Error undoing operation:', error); toast.error(`Failed to undo: ${error instanceof Error ? error.message : 'Unknown error'}`, { toastId: 'operation-progress', }); } }, [lastOperation, db, onReloadSettings, onReloadChats]); return { isExporting, isImporting, isResetting, isDownloadingTemplate, progressMessage, progressPercent, lastOperation, handleExportSettings, handleExportSelectedSettings, handleExportAllChats, handleExportSelectedChats, handleImportSettings, handleImportChats, handleImportAPIKeys, handleResetSettings, handleResetChats, handleDownloadTemplate, handleExportAPIKeys, handleUndo, }; }