feat: bolt dyi datatab (#1570)

* 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
This commit is contained in:
Stijnus
2025-03-29 20:43:07 +01:00
committed by GitHub
parent 47444970e8
commit b86fd63700
15 changed files with 6698 additions and 3940 deletions

View File

@@ -0,0 +1,966 @@
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,
};
}

View File

@@ -0,0 +1,58 @@
import { useState, useEffect } from 'react';
/**
* Hook to initialize and provide access to the IndexedDB database
*/
export function useIndexedDB() {
const [db, setDb] = useState<IDBDatabase | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const initDB = async () => {
try {
setIsLoading(true);
const request = indexedDB.open('boltDB', 1);
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
// Create object stores if they don't exist
if (!db.objectStoreNames.contains('chats')) {
const chatStore = db.createObjectStore('chats', { keyPath: 'id' });
chatStore.createIndex('updatedAt', 'updatedAt', { unique: false });
}
if (!db.objectStoreNames.contains('settings')) {
db.createObjectStore('settings', { keyPath: 'key' });
}
};
request.onsuccess = (event) => {
const database = (event.target as IDBOpenDBRequest).result;
setDb(database);
setIsLoading(false);
};
request.onerror = (event) => {
setError(new Error(`Database error: ${(event.target as IDBOpenDBRequest).error?.message}`));
setIsLoading(false);
};
} catch (err) {
setError(err instanceof Error ? err : new Error('Unknown error initializing database'));
setIsLoading(false);
}
};
initDB();
return () => {
if (db) {
db.close();
}
};
}, []);
return { db, isLoading, error };
}

View File

@@ -0,0 +1,140 @@
/**
* Functions for managing chat data in IndexedDB
*/
import type { Message } from 'ai';
import type { IChatMetadata } from './db'; // Import IChatMetadata
export interface ChatMessage {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
timestamp: number;
}
export interface Chat {
id: string;
description?: string;
messages: Message[];
timestamp: string;
urlId?: string;
metadata?: IChatMetadata;
}
/**
* Get all chats from the database
* @param db The IndexedDB database instance
* @returns A promise that resolves to an array of chats
*/
export async function getAllChats(db: IDBDatabase): Promise<Chat[]> {
console.log(`getAllChats: Using database '${db.name}', version ${db.version}`);
return new Promise((resolve, reject) => {
try {
const transaction = db.transaction(['chats'], 'readonly');
const store = transaction.objectStore('chats');
const request = store.getAll();
request.onsuccess = () => {
const result = request.result || [];
console.log(`getAllChats: Found ${result.length} chats in database '${db.name}'`);
resolve(result);
};
request.onerror = () => {
console.error(`getAllChats: Error querying database '${db.name}':`, request.error);
reject(request.error);
};
} catch (err) {
console.error(`getAllChats: Error creating transaction on database '${db.name}':`, err);
reject(err);
}
});
}
/**
* Get a chat by ID
* @param db The IndexedDB database instance
* @param id The ID of the chat to get
* @returns A promise that resolves to the chat or null if not found
*/
export async function getChatById(db: IDBDatabase, id: string): Promise<Chat | null> {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['chats'], 'readonly');
const store = transaction.objectStore('chats');
const request = store.get(id);
request.onsuccess = () => {
resolve(request.result || null);
};
request.onerror = () => {
reject(request.error);
};
});
}
/**
* Save a chat to the database
* @param db The IndexedDB database instance
* @param chat The chat to save
* @returns A promise that resolves when the chat is saved
*/
export async function saveChat(db: IDBDatabase, chat: Chat): Promise<void> {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['chats'], 'readwrite');
const store = transaction.objectStore('chats');
const request = store.put(chat);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(request.error);
};
});
}
/**
* Delete a chat by ID
* @param db The IndexedDB database instance
* @param id The ID of the chat to delete
* @returns A promise that resolves when the chat is deleted
*/
export async function deleteChat(db: IDBDatabase, id: string): Promise<void> {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['chats'], 'readwrite');
const store = transaction.objectStore('chats');
const request = store.delete(id);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(request.error);
};
});
}
/**
* Delete all chats
* @param db The IndexedDB database instance
* @returns A promise that resolves when all chats are deleted
*/
export async function deleteAllChats(db: IDBDatabase): Promise<void> {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['chats'], 'readwrite');
const store = transaction.objectStore('chats');
const request = store.clear();
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(request.error);
};
});
}

View File

@@ -0,0 +1,695 @@
import Cookies from 'js-cookie';
import { type Message } from 'ai';
import { getAllChats, deleteChat } from '~/lib/persistence/chats';
interface ExtendedMessage extends Message {
name?: string;
function_call?: any;
timestamp?: number;
}
/**
* Service for handling import and export operations of application data
*/
export class ImportExportService {
/**
* Export all chats to a JSON file
* @param db The IndexedDB database instance
* @returns A promise that resolves to the export data
*/
static async exportAllChats(db: IDBDatabase): Promise<{ chats: any[]; exportDate: string }> {
if (!db) {
throw new Error('Database not initialized');
}
try {
// Get all chats from the database using the getAllChats helper
const chats = await getAllChats(db);
// Validate and sanitize each chat before export
const sanitizedChats = chats.map((chat) => ({
id: chat.id,
description: chat.description || '',
messages: chat.messages.map((msg: ExtendedMessage) => ({
id: msg.id,
role: msg.role,
content: msg.content,
name: msg.name,
function_call: msg.function_call,
timestamp: msg.timestamp,
})),
timestamp: chat.timestamp,
urlId: chat.urlId || null,
metadata: chat.metadata || null,
}));
console.log(`Successfully prepared ${sanitizedChats.length} chats for export`);
return {
chats: sanitizedChats,
exportDate: new Date().toISOString(),
};
} catch (error) {
console.error('Error exporting chats:', error);
throw new Error(`Failed to export chats: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Export application settings to a JSON file
* @returns A promise that resolves to the settings data
*/
static async exportSettings(): Promise<any> {
try {
// Get all cookies
const allCookies = Cookies.get();
// Create a comprehensive settings object
return {
// Core settings
core: {
// User profile and main settings
bolt_user_profile: this._safeGetItem('bolt_user_profile'),
bolt_settings: this._safeGetItem('bolt_settings'),
bolt_profile: this._safeGetItem('bolt_profile'),
theme: this._safeGetItem('theme'),
},
// Provider settings (both local and cloud)
providers: {
// Provider configurations from localStorage
provider_settings: this._safeGetItem('provider_settings'),
// API keys from cookies
apiKeys: allCookies.apiKeys,
// Selected provider and model
selectedModel: allCookies.selectedModel,
selectedProvider: allCookies.selectedProvider,
// Provider-specific settings
providers: allCookies.providers,
},
// Feature settings
features: {
// Feature flags
viewed_features: this._safeGetItem('bolt_viewed_features'),
developer_mode: this._safeGetItem('bolt_developer_mode'),
// Context optimization
contextOptimizationEnabled: this._safeGetItem('contextOptimizationEnabled'),
// Auto-select template
autoSelectTemplate: this._safeGetItem('autoSelectTemplate'),
// Latest branch
isLatestBranch: this._safeGetItem('isLatestBranch'),
// Event logs
isEventLogsEnabled: this._safeGetItem('isEventLogsEnabled'),
// Energy saver settings
energySaverMode: this._safeGetItem('energySaverMode'),
autoEnergySaver: this._safeGetItem('autoEnergySaver'),
},
// UI configuration
ui: {
// Tab configuration
bolt_tab_configuration: this._safeGetItem('bolt_tab_configuration'),
tabConfiguration: allCookies.tabConfiguration,
// Prompt settings
promptId: this._safeGetItem('promptId'),
cachedPrompt: allCookies.cachedPrompt,
},
// Connections
connections: {
// Netlify connection
netlify_connection: this._safeGetItem('netlify_connection'),
// GitHub connections
...this._getGitHubConnections(allCookies),
},
// Debug and logs
debug: {
// Debug settings
isDebugEnabled: allCookies.isDebugEnabled,
acknowledged_debug_issues: this._safeGetItem('bolt_acknowledged_debug_issues'),
acknowledged_connection_issue: this._safeGetItem('bolt_acknowledged_connection_issue'),
// Error logs
error_logs: this._safeGetItem('error_logs'),
bolt_read_logs: this._safeGetItem('bolt_read_logs'),
// Event logs
eventLogs: allCookies.eventLogs,
},
// Update settings
updates: {
update_settings: this._safeGetItem('update_settings'),
last_acknowledged_update: this._safeGetItem('bolt_last_acknowledged_version'),
},
// Chat snapshots (for chat history)
chatSnapshots: this._getChatSnapshots(),
// Raw data (for debugging and complete backup)
_raw: {
localStorage: this._getAllLocalStorage(),
cookies: allCookies,
},
// Export metadata
_meta: {
exportDate: new Date().toISOString(),
version: '2.0',
appVersion: process.env.NEXT_PUBLIC_VERSION || 'unknown',
},
};
} catch (error) {
console.error('Error exporting settings:', error);
throw error;
}
}
/**
* Import settings from a JSON file
* @param importedData The imported data
*/
static async importSettings(importedData: any): Promise<void> {
// Check if this is the new comprehensive format (v2.0)
const isNewFormat = importedData._meta?.version === '2.0';
if (isNewFormat) {
// Import using the new comprehensive format
await this._importComprehensiveFormat(importedData);
} else {
// Try to handle older formats
await this._importLegacyFormat(importedData);
}
}
/**
* Import API keys from a JSON file
* @param keys The API keys to import
*/
static importAPIKeys(keys: Record<string, any>): Record<string, string> {
// Get existing keys from cookies
const existingKeys = (() => {
const storedApiKeys = Cookies.get('apiKeys');
return storedApiKeys ? JSON.parse(storedApiKeys) : {};
})();
// Validate and save each key
const newKeys = { ...existingKeys };
Object.entries(keys).forEach(([key, value]) => {
// Skip comment fields
if (key.startsWith('_')) {
return;
}
// Skip base URL fields (they should be set in .env.local)
if (key.includes('_API_BASE_URL')) {
return;
}
if (typeof value !== 'string') {
throw new Error(`Invalid value for key: ${key}`);
}
// Handle both old and new template formats
let normalizedKey = key;
// Check if this is the old format (e.g., "Anthropic_API_KEY")
if (key.includes('_API_KEY')) {
// Extract the provider name from the old format
normalizedKey = key.replace('_API_KEY', '');
}
/*
* Only add non-empty keys
* Use the normalized key in the correct format
* (e.g., "OpenAI", "Google", "Anthropic")
*/
if (value) {
newKeys[normalizedKey] = value;
}
});
return newKeys;
}
/**
* Create an API keys template
* @returns The API keys template
*/
static createAPIKeysTemplate(): Record<string, any> {
/*
* Create a template with provider names as keys
* This matches how the application stores API keys in cookies
*/
const template = {
Anthropic: '',
OpenAI: '',
Google: '',
Groq: '',
HuggingFace: '',
OpenRouter: '',
Deepseek: '',
Mistral: '',
OpenAILike: '',
Together: '',
xAI: '',
Perplexity: '',
Cohere: '',
AzureOpenAI: '',
};
// Add a comment to explain the format
return {
_comment:
"Fill in your API keys for each provider. Keys will be stored with the provider name (e.g., 'OpenAI'). The application also supports the older format with keys like 'OpenAI_API_KEY' for backward compatibility.",
...template,
};
}
/**
* Reset all settings to default values
* @param db The IndexedDB database instance
*/
static async resetAllSettings(db: IDBDatabase): Promise<void> {
// 1. Clear all localStorage items related to application settings
const localStorageKeysToPreserve: string[] = ['debug_mode']; // Keys to preserve if needed
// Get all localStorage keys
const allLocalStorageKeys = Object.keys(localStorage);
// Clear all localStorage items except those to preserve
allLocalStorageKeys.forEach((key) => {
if (!localStorageKeysToPreserve.includes(key)) {
try {
localStorage.removeItem(key);
} catch (err) {
console.error(`Error removing localStorage item ${key}:`, err);
}
}
});
// 2. Clear all cookies related to application settings
const cookiesToPreserve: string[] = []; // Cookies to preserve if needed
// Get all cookies
const allCookies = Cookies.get();
const cookieKeys = Object.keys(allCookies);
// Clear all cookies except those to preserve
cookieKeys.forEach((key) => {
if (!cookiesToPreserve.includes(key)) {
try {
Cookies.remove(key);
} catch (err) {
console.error(`Error removing cookie ${key}:`, err);
}
}
});
// 3. Clear all data from IndexedDB
if (!db) {
console.warn('Database not initialized, skipping IndexedDB reset');
} else {
// Get all chats and delete them
const chats = await getAllChats(db);
const deletePromises = chats.map((chat) => deleteChat(db, chat.id));
await Promise.all(deletePromises);
}
// 4. Clear any chat snapshots
const snapshotKeys = Object.keys(localStorage).filter((key) => key.startsWith('snapshot:'));
snapshotKeys.forEach((key) => {
try {
localStorage.removeItem(key);
} catch (err) {
console.error(`Error removing snapshot ${key}:`, err);
}
});
}
/**
* Delete all chats from the database
* @param db The IndexedDB database instance
*/
static async deleteAllChats(db: IDBDatabase): Promise<void> {
// Clear chat history from localStorage
localStorage.removeItem('bolt_chat_history');
// Clear chats from IndexedDB
if (!db) {
throw new Error('Database not initialized');
}
// Get all chats and delete them one by one
const chats = await getAllChats(db);
const deletePromises = chats.map((chat) => deleteChat(db, chat.id));
await Promise.all(deletePromises);
}
// Private helper methods
/**
* Import settings from a comprehensive format
* @param data The imported data
*/
private static async _importComprehensiveFormat(data: any): Promise<void> {
// Import core settings
if (data.core) {
Object.entries(data.core).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
try {
this._safeSetItem(key, value);
} catch (err) {
console.error(`Error importing core setting ${key}:`, err);
}
}
});
}
// Import provider settings
if (data.providers) {
// Import provider_settings to localStorage
if (data.providers.provider_settings) {
try {
this._safeSetItem('provider_settings', data.providers.provider_settings);
} catch (err) {
console.error('Error importing provider settings:', err);
}
}
// Import API keys and other provider cookies
const providerCookies = ['apiKeys', 'selectedModel', 'selectedProvider', 'providers'];
providerCookies.forEach((key) => {
if (data.providers[key]) {
try {
this._safeSetCookie(key, data.providers[key]);
} catch (err) {
console.error(`Error importing provider cookie ${key}:`, err);
}
}
});
}
// Import feature settings
if (data.features) {
Object.entries(data.features).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
try {
this._safeSetItem(key, value);
} catch (err) {
console.error(`Error importing feature setting ${key}:`, err);
}
}
});
}
// Import UI configuration
if (data.ui) {
// Import localStorage UI settings
if (data.ui.bolt_tab_configuration) {
try {
this._safeSetItem('bolt_tab_configuration', data.ui.bolt_tab_configuration);
} catch (err) {
console.error('Error importing tab configuration:', err);
}
}
if (data.ui.promptId) {
try {
this._safeSetItem('promptId', data.ui.promptId);
} catch (err) {
console.error('Error importing prompt ID:', err);
}
}
// Import UI cookies
const uiCookies = ['tabConfiguration', 'cachedPrompt'];
uiCookies.forEach((key) => {
if (data.ui[key]) {
try {
this._safeSetCookie(key, data.ui[key]);
} catch (err) {
console.error(`Error importing UI cookie ${key}:`, err);
}
}
});
}
// Import connections
if (data.connections) {
// Import Netlify connection
if (data.connections.netlify_connection) {
try {
this._safeSetItem('netlify_connection', data.connections.netlify_connection);
} catch (err) {
console.error('Error importing Netlify connection:', err);
}
}
// Import GitHub connections
Object.entries(data.connections).forEach(([key, value]) => {
if (key.startsWith('github_') && value !== null && value !== undefined) {
try {
this._safeSetItem(key, value);
} catch (err) {
console.error(`Error importing GitHub connection ${key}:`, err);
}
}
});
}
// Import debug settings
if (data.debug) {
// Import debug localStorage settings
const debugLocalStorageKeys = [
'bolt_acknowledged_debug_issues',
'bolt_acknowledged_connection_issue',
'error_logs',
'bolt_read_logs',
];
debugLocalStorageKeys.forEach((key) => {
if (data.debug[key] !== null && data.debug[key] !== undefined) {
try {
this._safeSetItem(key, data.debug[key]);
} catch (err) {
console.error(`Error importing debug setting ${key}:`, err);
}
}
});
// Import debug cookies
const debugCookies = ['isDebugEnabled', 'eventLogs'];
debugCookies.forEach((key) => {
if (data.debug[key]) {
try {
this._safeSetCookie(key, data.debug[key]);
} catch (err) {
console.error(`Error importing debug cookie ${key}:`, err);
}
}
});
}
// Import update settings
if (data.updates) {
if (data.updates.update_settings) {
try {
this._safeSetItem('update_settings', data.updates.update_settings);
} catch (err) {
console.error('Error importing update settings:', err);
}
}
if (data.updates.last_acknowledged_update) {
try {
this._safeSetItem('bolt_last_acknowledged_version', data.updates.last_acknowledged_update);
} catch (err) {
console.error('Error importing last acknowledged update:', err);
}
}
}
// Import chat snapshots
if (data.chatSnapshots) {
Object.entries(data.chatSnapshots).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
try {
this._safeSetItem(key, value);
} catch (err) {
console.error(`Error importing chat snapshot ${key}:`, err);
}
}
});
}
}
/**
* Import settings from a legacy format
* @param data The imported data
*/
private static async _importLegacyFormat(data: any): Promise<void> {
/**
* Handle legacy format (v1.0 or earlier)
* This is a simplified version that tries to import whatever is available
*/
// Try to import settings directly
Object.entries(data).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
// Skip metadata fields
if (key === 'exportDate' || key === 'version' || key === 'appVersion') {
return;
}
try {
// Try to determine if this should be a cookie or localStorage item
const isCookie = [
'apiKeys',
'selectedModel',
'selectedProvider',
'providers',
'tabConfiguration',
'cachedPrompt',
'isDebugEnabled',
'eventLogs',
].includes(key);
if (isCookie) {
this._safeSetCookie(key, value);
} else {
this._safeSetItem(key, value);
}
} catch (err) {
console.error(`Error importing legacy setting ${key}:`, err);
}
}
});
}
/**
* Safely get an item from localStorage
* @param key The key to get
* @returns The value or null if not found
*/
private static _safeGetItem(key: string): any {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch (err) {
console.error(`Error getting localStorage item ${key}:`, err);
return null;
}
}
/**
* Get all localStorage items
* @returns All localStorage items
*/
private static _getAllLocalStorage(): Record<string, any> {
const result: Record<string, any> = {};
try {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key) {
try {
const value = localStorage.getItem(key);
result[key] = value ? JSON.parse(value) : null;
} catch {
result[key] = null;
}
}
}
} catch (err) {
console.error('Error getting all localStorage items:', err);
}
return result;
}
/**
* Get GitHub connections from cookies
* @param _cookies The cookies object
* @returns GitHub connections
*/
private static _getGitHubConnections(_cookies: Record<string, string>): Record<string, any> {
const result: Record<string, any> = {};
// Get GitHub connections from localStorage
const localStorageKeys = Object.keys(localStorage).filter((key) => key.startsWith('github_'));
localStorageKeys.forEach((key) => {
try {
const value = localStorage.getItem(key);
result[key] = value ? JSON.parse(value) : null;
} catch (err) {
console.error(`Error getting GitHub connection ${key}:`, err);
result[key] = null;
}
});
return result;
}
/**
* Get chat snapshots from localStorage
* @returns Chat snapshots
*/
private static _getChatSnapshots(): Record<string, any> {
const result: Record<string, any> = {};
// Get chat snapshots from localStorage
const snapshotKeys = Object.keys(localStorage).filter((key) => key.startsWith('snapshot:'));
snapshotKeys.forEach((key) => {
try {
const value = localStorage.getItem(key);
result[key] = value ? JSON.parse(value) : null;
} catch (err) {
console.error(`Error getting chat snapshot ${key}:`, err);
result[key] = null;
}
});
return result;
}
/**
* Safely set an item in localStorage
* @param key The key to set
* @param value The value to set
*/
private static _safeSetItem(key: string, value: any): void {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (err) {
console.error(`Error setting localStorage item ${key}:`, err);
}
}
/**
* Safely set a cookie
* @param key The key to set
* @param value The value to set
*/
private static _safeSetCookie(key: string, value: any): void {
try {
Cookies.set(key, typeof value === 'string' ? value : JSON.stringify(value), { expires: 365 });
} catch (err) {
console.error(`Error setting cookie ${key}:`, err);
}
}
}