Merge branch 'main' into main

This commit is contained in:
KevIsDev
2025-03-31 10:31:40 +01:00
committed by GitHub
44 changed files with 13636 additions and 5897 deletions

View File

@@ -153,18 +153,126 @@ ${props.summary}
logger.info(`Sending llm call to ${provider.name} with model ${modelDetails.name}`);
// console.log(systemPrompt,processedMessages);
// Store original messages for reference
const originalMessages = [...messages];
const hasMultimodalContent = originalMessages.some((msg) => Array.isArray(msg.content));
return await _streamText({
model: provider.getModelInstance({
model: modelDetails.name,
serverEnv,
apiKeys,
providerSettings,
}),
system: systemPrompt,
maxTokens: dynamicMaxTokens,
messages: convertToCoreMessages(processedMessages as any),
...options,
});
try {
if (hasMultimodalContent) {
/*
* For multimodal content, we need to preserve the original array structure
* but make sure the roles are valid and content items are properly formatted
*/
const multimodalMessages = originalMessages.map((msg) => ({
role: msg.role === 'system' || msg.role === 'user' || msg.role === 'assistant' ? msg.role : 'user',
content: Array.isArray(msg.content)
? msg.content.map((item) => {
// Ensure each content item has the correct format
if (typeof item === 'string') {
return { type: 'text', text: item };
}
if (item && typeof item === 'object') {
if (item.type === 'image' && item.image) {
return { type: 'image', image: item.image };
}
if (item.type === 'text') {
return { type: 'text', text: item.text || '' };
}
}
// Default fallback for unknown formats
return { type: 'text', text: String(item || '') };
})
: [{ type: 'text', text: typeof msg.content === 'string' ? msg.content : String(msg.content || '') }],
}));
return await _streamText({
model: provider.getModelInstance({
model: modelDetails.name,
serverEnv,
apiKeys,
providerSettings,
}),
system: systemPrompt,
maxTokens: dynamicMaxTokens,
messages: multimodalMessages as any,
...options,
});
} else {
// For non-multimodal content, we use the standard approach
const normalizedTextMessages = processedMessages.map((msg) => ({
role: msg.role === 'system' || msg.role === 'user' || msg.role === 'assistant' ? msg.role : 'user',
content: typeof msg.content === 'string' ? msg.content : String(msg.content || ''),
}));
return await _streamText({
model: provider.getModelInstance({
model: modelDetails.name,
serverEnv,
apiKeys,
providerSettings,
}),
system: systemPrompt,
maxTokens: dynamicMaxTokens,
messages: convertToCoreMessages(normalizedTextMessages),
...options,
});
}
} catch (error: any) {
// Special handling for format errors
if (error.message && error.message.includes('messages must be an array of CoreMessage or UIMessage')) {
logger.warn('Message format error detected, attempting recovery with explicit formatting...');
// Create properly formatted messages for all cases as a last resort
const fallbackMessages = processedMessages.map((msg) => {
// Determine text content with careful type handling
let textContent = '';
if (typeof msg.content === 'string') {
textContent = msg.content;
} else if (Array.isArray(msg.content)) {
// Handle array content safely
const contentArray = msg.content as any[];
textContent = contentArray
.map((contentItem) =>
typeof contentItem === 'string'
? contentItem
: contentItem?.text || contentItem?.image || String(contentItem || ''),
)
.join(' ');
} else {
textContent = String(msg.content || '');
}
return {
role: msg.role === 'system' || msg.role === 'user' || msg.role === 'assistant' ? msg.role : 'user',
content: [
{
type: 'text',
text: textContent,
},
],
};
});
// Try one more time with the fallback format
return await _streamText({
model: provider.getModelInstance({
model: modelDetails.name,
serverEnv,
apiKeys,
providerSettings,
}),
system: systemPrompt,
maxTokens: dynamicMaxTokens,
messages: fallbackMessages as any,
...options,
});
}
// If it's not a format error, re-throw the original error
throw error;
}
}

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

@@ -50,6 +50,11 @@ export function useGit() {
fileData.current = {};
/*
* Skip Git initialization for now - let isomorphic-git handle it
* This avoids potential issues with our manual initialization
*/
const headers: {
[x: string]: string;
} = {
@@ -72,18 +77,23 @@ export function useGit() {
singleBranch: true,
corsProxy: '/api/git-proxy',
headers,
onProgress: (event) => {
console.log('Git clone progress:', event);
},
onAuth: (url) => {
let auth = lookupSavedPassword(url);
if (auth) {
console.log('Using saved authentication for', url);
return auth;
}
console.log('Repository requires authentication:', url);
if (confirm('This repo is password protected. Ready to enter a username & password?')) {
auth = {
username: prompt('Enter username'),
password: prompt('Enter password'),
username: prompt('Enter username') || '',
password: prompt('Enter password') || '',
};
return auth;
} else {
@@ -91,10 +101,12 @@ export function useGit() {
}
},
onAuthFailure: (url, _auth) => {
console.error(`Authentication failed for ${url}`);
toast.error(`Error Authenticating with ${url.split('/')[2]}`);
throw `Error Authenticating with ${url.split('/')[2]}`;
},
onAuthSuccess: (url, auth) => {
console.log(`Authentication successful for ${url}`);
saveGitAuth(url, auth);
},
});
@@ -136,18 +148,26 @@ const getFs = (
throw error;
}
},
writeFile: async (path: string, data: any, options: any) => {
const encoding = options.encoding;
writeFile: async (path: string, data: any, options: any = {}) => {
const relativePath = pathUtils.relative(webcontainer.workdir, path);
if (record.current) {
record.current[relativePath] = { data, encoding };
record.current[relativePath] = { data, encoding: options?.encoding };
}
try {
const result = await webcontainer.fs.writeFile(relativePath, data, { ...options, encoding });
// Handle encoding properly based on data type
if (data instanceof Uint8Array) {
// For binary data, don't pass encoding
const result = await webcontainer.fs.writeFile(relativePath, data);
return result;
} else {
// For text data, use the encoding if provided
const encoding = options?.encoding || 'utf8';
const result = await webcontainer.fs.writeFile(relativePath, data, encoding);
return result;
return result;
}
} catch (error) {
throw error;
}
@@ -208,33 +228,80 @@ const getFs = (
stat: async (path: string) => {
try {
const relativePath = pathUtils.relative(webcontainer.workdir, path);
const resp = await webcontainer.fs.readdir(pathUtils.dirname(relativePath), { withFileTypes: true });
const name = pathUtils.basename(relativePath);
const fileInfo = resp.find((x) => x.name == name);
const dirPath = pathUtils.dirname(relativePath);
const fileName = pathUtils.basename(relativePath);
// Special handling for .git/index file
if (relativePath === '.git/index') {
return {
isFile: () => true,
isDirectory: () => false,
isSymbolicLink: () => false,
size: 12, // Size of our empty index
mode: 0o100644, // Regular file
mtimeMs: Date.now(),
ctimeMs: Date.now(),
birthtimeMs: Date.now(),
atimeMs: Date.now(),
uid: 1000,
gid: 1000,
dev: 1,
ino: 1,
nlink: 1,
rdev: 0,
blksize: 4096,
blocks: 1,
mtime: new Date(),
ctime: new Date(),
birthtime: new Date(),
atime: new Date(),
};
}
const resp = await webcontainer.fs.readdir(dirPath, { withFileTypes: true });
const fileInfo = resp.find((x) => x.name === fileName);
if (!fileInfo) {
throw new Error(`ENOENT: no such file or directory, stat '${path}'`);
const err = new Error(`ENOENT: no such file or directory, stat '${path}'`) as NodeJS.ErrnoException;
err.code = 'ENOENT';
err.errno = -2;
err.syscall = 'stat';
err.path = path;
throw err;
}
return {
isFile: () => fileInfo.isFile(),
isDirectory: () => fileInfo.isDirectory(),
isSymbolicLink: () => false,
size: 1,
mode: 0o666, // Default permissions
size: fileInfo.isDirectory() ? 4096 : 1,
mode: fileInfo.isDirectory() ? 0o040755 : 0o100644, // Directory or regular file
mtimeMs: Date.now(),
ctimeMs: Date.now(),
birthtimeMs: Date.now(),
atimeMs: Date.now(),
uid: 1000,
gid: 1000,
dev: 1,
ino: 1,
nlink: 1,
rdev: 0,
blksize: 4096,
blocks: 8,
mtime: new Date(),
ctime: new Date(),
birthtime: new Date(),
atime: new Date(),
};
} catch (error: any) {
console.log(error?.message);
if (!error.code) {
error.code = 'ENOENT';
error.errno = -2;
error.syscall = 'stat';
error.path = path;
}
const err = new Error(`ENOENT: no such file or directory, stat '${path}'`) as NodeJS.ErrnoException;
err.code = 'ENOENT';
err.errno = -2;
err.syscall = 'stat';
err.path = path;
throw err;
throw error;
}
},
lstat: async (path: string) => {

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

View File

@@ -1,15 +1,18 @@
import { atom } from 'nanostores';
import type { NetlifyConnection } from '~/types/netlify';
import type { NetlifyConnection, NetlifyUser } from '~/types/netlify';
import { logStore } from './logs';
import { toast } from 'react-toastify';
// Initialize with stored connection or defaults
// Initialize with stored connection or environment variable
const storedConnection = typeof window !== 'undefined' ? localStorage.getItem('netlify_connection') : null;
const envToken = import.meta.env.VITE_NETLIFY_ACCESS_TOKEN;
// If we have an environment token but no stored connection, initialize with the env token
const initialConnection: NetlifyConnection = storedConnection
? JSON.parse(storedConnection)
: {
user: null,
token: '',
token: envToken || '',
stats: undefined,
};
@@ -17,6 +20,52 @@ export const netlifyConnection = atom<NetlifyConnection>(initialConnection);
export const isConnecting = atom<boolean>(false);
export const isFetchingStats = atom<boolean>(false);
// Function to initialize Netlify connection with environment token
export async function initializeNetlifyConnection() {
const currentState = netlifyConnection.get();
// If we already have a connection, don't override it
if (currentState.user || !envToken) {
return;
}
try {
isConnecting.set(true);
const response = await fetch('https://api.netlify.com/api/v1/user', {
headers: {
Authorization: `Bearer ${envToken}`,
},
});
if (!response.ok) {
throw new Error(`Failed to connect to Netlify: ${response.statusText}`);
}
const userData = await response.json();
// Update the connection state
const connectionData: Partial<NetlifyConnection> = {
user: userData as NetlifyUser,
token: envToken,
};
// Store in localStorage for persistence
localStorage.setItem('netlify_connection', JSON.stringify(connectionData));
// Update the store
updateNetlifyConnection(connectionData);
// Fetch initial stats
await fetchNetlifyStats(envToken);
} catch (error) {
console.error('Error initializing Netlify connection:', error);
logStore.logError('Failed to initialize Netlify connection', { error });
} finally {
isConnecting.set(false);
}
}
export const updateNetlifyConnection = (updates: Partial<NetlifyConnection>) => {
const currentState = netlifyConnection.get();
const newState = { ...currentState, ...updates };