mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-05-01 03:02:15 +00:00
* Update DataTab.tsx ## API Key Import Fix We identified and fixed an issue with the API key import functionality in the DataTab component. The problem was that API keys were being stored in localStorage instead of cookies, and the key format was being incorrectly processed. ### Changes Made: 1. **Updated `handleImportAPIKeys` function**: - Changed to store API keys in cookies instead of localStorage - Modified to use provider names directly as keys (e.g., "OpenAI", "Google") - Added logic to skip comment fields (keys starting with "_") - Added page reload after successful import to apply changes immediately 2. **Updated `handleDownloadTemplate` function**: - Changed template format to use provider names as keys - Added explanatory comment in the template - Removed URL-related keys that weren't being used properly 3. **Fixed template format**: - Template now uses the correct format with provider names as keys - Added support for all available providers including Hyperbolic These changes ensure that when users download the template, fill it with their API keys, and import it back, the keys are properly stored in cookies with the correct format that the application expects. * backwards compatible old import template * Update the export / import settings Settings Export/Import Improvements We've completely redesigned the settings export and import functionality to ensure all application settings are properly backed up and restored: Key Improvements Comprehensive Export Format: Now captures ALL settings from both localStorage and cookies, organized into logical categories (core, providers, features, UI, connections, debug, updates) Robust Import System: Automatically detects format version and handles both new and legacy formats with detailed error handling Complete Settings Coverage: Properly exports and imports settings from ALL tabs including: Local provider configurations (Ollama, LMStudio, etc.) Cloud provider API keys (OpenAI, Anthropic, etc.) Feature toggles and preferences UI configurations and tab settings Connection settings (GitHub, Netlify) Debug configurations and logs Technical Details Added version tracking to export files for better compatibility Implemented fallback mechanisms if primary import methods fail Added detailed logging for troubleshooting import/export issues Created helper functions for safer data handling Maintained backward compatibility with older export formats Feature Settings: Feature flags and viewed features Developer mode settings Energy saver mode configurations User Preferences: User profile information Theme settings Tab configurations Connection Settings: Netlify connections Git authentication credentials Any other service connections Debug and System Settings: Debug flags and acknowledged issues Error logs and event logs Update settings and preferences * Update DataTab.tsx * Update GithubConnection.tsx revert the code back as asked * feat: enhance style to match the project * feat:small improvements * feat: add major improvements * Update Dialog.tsx * Delete DataTab.tsx.bak * feat: small updates * Update DataVisualization.tsx * feat: dark mode fix
967 lines
30 KiB
TypeScript
967 lines
30 KiB
TypeScript
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<string>('');
|
|
const [progressPercent, setProgressPercent] = useState<number>(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<string, any> = {
|
|
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<any[]>((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<any>((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,
|
|
};
|
|
}
|