Merge branch 'main' into main
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
966
app/lib/hooks/useDataOperations.ts
Normal file
966
app/lib/hooks/useDataOperations.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
58
app/lib/hooks/useIndexedDB.ts
Normal file
58
app/lib/hooks/useIndexedDB.ts
Normal 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 };
|
||||
}
|
||||
140
app/lib/persistence/chats.ts
Normal file
140
app/lib/persistence/chats.ts
Normal 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);
|
||||
};
|
||||
});
|
||||
}
|
||||
695
app/lib/services/importExportService.ts
Normal file
695
app/lib/services/importExportService.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user