bolt.diy/app/lib/services/importExportService.ts
Stijnus b86fd63700
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
2025-03-29 20:43:07 +01:00

696 lines
20 KiB
TypeScript

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);
}
}
}