diff --git a/app/components/@settings/tabs/connections/ConnectionDiagnostics.tsx b/app/components/@settings/tabs/connections/ConnectionDiagnostics.tsx index 497d57b6..1a4c54fe 100644 --- a/app/components/@settings/tabs/connections/ConnectionDiagnostics.tsx +++ b/app/components/@settings/tabs/connections/ConnectionDiagnostics.tsx @@ -6,6 +6,20 @@ import { classNames } from '~/utils/classNames'; import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '~/components/ui/Collapsible'; import { CodeBracketIcon, ChevronDownIcon } from '@heroicons/react/24/outline'; +// Helper function to safely parse JSON +const safeJsonParse = (item: string | null) => { + if (!item) { + return null; + } + + try { + return JSON.parse(item); + } catch (e) { + console.error('Failed to parse JSON from localStorage:', e); + return null; + } +}; + /** * A diagnostics component to help troubleshoot connection issues */ @@ -24,6 +38,8 @@ export default function ConnectionDiagnostics() { const localStorageChecks = { githubConnection: localStorage.getItem('github_connection'), netlifyConnection: localStorage.getItem('netlify_connection'), + vercelConnection: localStorage.getItem('vercel_connection'), + supabaseConnection: localStorage.getItem('supabase_connection'), }; // Get diagnostic data from server @@ -35,36 +51,25 @@ export default function ConnectionDiagnostics() { const serverDiagnostics = await response.json(); - // Get GitHub token if available - const githubToken = localStorageChecks.githubConnection - ? JSON.parse(localStorageChecks.githubConnection)?.token - : null; - - const authHeaders = { + // === GitHub Checks === + const githubConnectionParsed = safeJsonParse(localStorageChecks.githubConnection); + const githubToken = githubConnectionParsed?.token; + const githubAuthHeaders = { ...(githubToken ? { Authorization: `Bearer ${githubToken}` } : {}), 'Content-Type': 'application/json', }; - console.log('Testing GitHub endpoints with token:', githubToken ? 'present' : 'missing'); - // Test GitHub API endpoints const githubEndpoints = [ { name: 'User', url: '/api/system/git-info?action=getUser' }, { name: 'Repos', url: '/api/system/git-info?action=getRepos' }, { name: 'Default', url: '/api/system/git-info' }, ]; - const githubResults = await Promise.all( githubEndpoints.map(async (endpoint) => { try { - const resp = await fetch(endpoint.url, { - headers: authHeaders, - }); - return { - endpoint: endpoint.name, - status: resp.status, - ok: resp.ok, - }; + const resp = await fetch(endpoint.url, { headers: githubAuthHeaders }); + return { endpoint: endpoint.name, status: resp.status, ok: resp.ok }; } catch (error) { return { endpoint: endpoint.name, @@ -75,23 +80,17 @@ export default function ConnectionDiagnostics() { }), ); - // Check if Netlify token works + // === Netlify Checks === + const netlifyConnectionParsed = safeJsonParse(localStorageChecks.netlifyConnection); + const netlifyToken = netlifyConnectionParsed?.token; let netlifyUserCheck = null; - const netlifyToken = localStorageChecks.netlifyConnection - ? JSON.parse(localStorageChecks.netlifyConnection || '{"token":""}').token - : ''; if (netlifyToken) { try { const netlifyResp = await fetch('https://api.netlify.com/api/v1/user', { - headers: { - Authorization: `Bearer ${netlifyToken}`, - }, + headers: { Authorization: `Bearer ${netlifyToken}` }, }); - netlifyUserCheck = { - status: netlifyResp.status, - ok: netlifyResp.ok, - }; + netlifyUserCheck = { status: netlifyResp.status, ok: netlifyResp.ok }; } catch (error) { netlifyUserCheck = { error: error instanceof Error ? error.message : String(error), @@ -100,22 +99,55 @@ export default function ConnectionDiagnostics() { } } + // === Vercel Checks === + const vercelConnectionParsed = safeJsonParse(localStorageChecks.vercelConnection); + const vercelToken = vercelConnectionParsed?.token; + let vercelUserCheck = null; + + if (vercelToken) { + try { + const vercelResp = await fetch('https://api.vercel.com/v2/user', { + headers: { Authorization: `Bearer ${vercelToken}` }, + }); + vercelUserCheck = { status: vercelResp.status, ok: vercelResp.ok }; + } catch (error) { + vercelUserCheck = { + error: error instanceof Error ? error.message : String(error), + ok: false, + }; + } + } + + // === Supabase Checks === + const supabaseConnectionParsed = safeJsonParse(localStorageChecks.supabaseConnection); + const supabaseUrl = supabaseConnectionParsed?.projectUrl; + const supabaseAnonKey = supabaseConnectionParsed?.anonKey; + let supabaseCheck = null; + + if (supabaseUrl && supabaseAnonKey) { + supabaseCheck = { ok: true, status: 200, message: 'URL and Key present in localStorage' }; + } else { + supabaseCheck = { ok: false, message: 'URL or Key missing in localStorage' }; + } + // Compile results const results = { timestamp: new Date().toISOString(), localStorage: { hasGithubConnection: Boolean(localStorageChecks.githubConnection), hasNetlifyConnection: Boolean(localStorageChecks.netlifyConnection), - githubConnectionParsed: localStorageChecks.githubConnection - ? JSON.parse(localStorageChecks.githubConnection) - : null, - netlifyConnectionParsed: localStorageChecks.netlifyConnection - ? JSON.parse(localStorageChecks.netlifyConnection) - : null, + hasVercelConnection: Boolean(localStorageChecks.vercelConnection), + hasSupabaseConnection: Boolean(localStorageChecks.supabaseConnection), + githubConnectionParsed, + netlifyConnectionParsed, + vercelConnectionParsed, + supabaseConnectionParsed, }, apiEndpoints: { github: githubResults, netlify: netlifyUserCheck, + vercel: vercelUserCheck, + supabase: supabaseCheck, }, serverDiagnostics, }; @@ -131,7 +163,20 @@ export default function ConnectionDiagnostics() { toast.error('Netlify API connection is failing. Try reconnecting.'); } - if (!results.localStorage.hasGithubConnection && !results.localStorage.hasNetlifyConnection) { + if (results.localStorage.hasVercelConnection && vercelUserCheck && !vercelUserCheck.ok) { + toast.error('Vercel API connection is failing. Try reconnecting.'); + } + + if (results.localStorage.hasSupabaseConnection && supabaseCheck && !supabaseCheck.ok) { + toast.warning('Supabase connection check failed or missing details. Verify settings.'); + } + + if ( + !results.localStorage.hasGithubConnection && + !results.localStorage.hasNetlifyConnection && + !results.localStorage.hasVercelConnection && + !results.localStorage.hasSupabaseConnection + ) { toast.info('No connection data found in browser storage.'); } } catch (error) { @@ -151,6 +196,7 @@ export default function ConnectionDiagnostics() { document.cookie = 'githubUsername=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; document.cookie = 'git:github.com=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; toast.success('GitHub connection data cleared. Please refresh the page and reconnect.'); + setDiagnosticResults(null); } catch (error) { console.error('Error clearing GitHub data:', error); toast.error('Failed to clear GitHub connection data'); @@ -163,12 +209,37 @@ export default function ConnectionDiagnostics() { localStorage.removeItem('netlify_connection'); document.cookie = 'netlifyToken=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; toast.success('Netlify connection data cleared. Please refresh the page and reconnect.'); + setDiagnosticResults(null); } catch (error) { console.error('Error clearing Netlify data:', error); toast.error('Failed to clear Netlify connection data'); } }; + // Helper to reset Vercel connection + const resetVercelConnection = () => { + try { + localStorage.removeItem('vercel_connection'); + toast.success('Vercel connection data cleared. Please refresh the page and reconnect.'); + setDiagnosticResults(null); + } catch (error) { + console.error('Error clearing Vercel data:', error); + toast.error('Failed to clear Vercel connection data'); + } + }; + + // Helper to reset Supabase connection + const resetSupabaseConnection = () => { + try { + localStorage.removeItem('supabase_connection'); + toast.success('Supabase connection data cleared. Please refresh the page and reconnect.'); + setDiagnosticResults(null); + } catch (error) { + console.error('Error clearing Supabase data:', error); + toast.error('Failed to clear Supabase connection data'); + } + }; + return (
{/* Connection Status Cards */} @@ -303,6 +374,133 @@ export default function ConnectionDiagnostics() {
)} + + {/* Vercel Connection Card */} +
+
+
+
+ Vercel Connection +
+
+ {diagnosticResults ? ( + <> +
+ + {diagnosticResults.localStorage.hasVercelConnection ? 'Connected' : 'Not Connected'} + +
+ {diagnosticResults.localStorage.hasVercelConnection && ( + <> +
+
+ User:{' '} + {diagnosticResults.localStorage.vercelConnectionParsed?.user?.username || + diagnosticResults.localStorage.vercelConnectionParsed?.user?.user?.username || + 'N/A'} +
+
+
+ API Status:{' '} + + {diagnosticResults.apiEndpoints.vercel?.ok ? 'OK' : 'Failed'} + +
+ + )} + {!diagnosticResults.localStorage.hasVercelConnection && ( + + )} + + ) : ( +
+
+
+ Run diagnostics to check connection status +
+
+ )} +
+ + {/* Supabase Connection Card */} +
+
+
+
+ Supabase Connection +
+
+ {diagnosticResults ? ( + <> +
+ + {diagnosticResults.localStorage.hasSupabaseConnection ? 'Configured' : 'Not Configured'} + +
+ {diagnosticResults.localStorage.hasSupabaseConnection && ( + <> +
+
+ Project URL: {diagnosticResults.localStorage.supabaseConnectionParsed?.projectUrl || 'N/A'} +
+
+
+ Config Status:{' '} + + {diagnosticResults.apiEndpoints.supabase?.ok ? 'OK' : 'Check Failed'} + +
+ + )} + {!diagnosticResults.localStorage.hasSupabaseConnection && ( + + )} + + ) : ( +
+
+
+ Run diagnostics to check connection status +
+
+ )} +
{/* Action Buttons */} @@ -323,22 +521,42 @@ export default function ConnectionDiagnostics() { + + + +
diff --git a/app/components/@settings/tabs/connections/ConnectionsTab.tsx b/app/components/@settings/tabs/connections/ConnectionsTab.tsx index 74e60a04..d61b6fdc 100644 --- a/app/components/@settings/tabs/connections/ConnectionsTab.tsx +++ b/app/components/@settings/tabs/connections/ConnectionsTab.tsx @@ -149,48 +149,6 @@ export default function ConnectionsTab() {
- {/* Cloudflare Deployment Note - Highly visible */} - -
-
-

Using Cloudflare Pages?

-
-

- If you're experiencing GitHub connection issues (500 errors) on Cloudflare Pages deployments, you need to - configure environment variables in your Cloudflare dashboard: -

-
-
    -
  1. - Go to Cloudflare Pages dashboard → Your project → Settings → Environment variables -
  2. -
  3. - Add both of these secrets (Production environment): -
      -
    • - GITHUB_ACCESS_TOKEN{' '} - (server-side API calls) -
    • -
    • - VITE_GITHUB_ACCESS_TOKEN{' '} - (client-side access) -
    • -
    -
  4. -
  5. - Add VITE_GITHUB_TOKEN_TYPE if - using fine-grained tokens -
  6. -
  7. Deploy a fresh build after adding these variables
  8. -
-
- -
}> diff --git a/app/components/@settings/tabs/connections/GithubConnection.tsx b/app/components/@settings/tabs/connections/GithubConnection.tsx index 03af3e43..25c498c6 100644 --- a/app/components/@settings/tabs/connections/GithubConnection.tsx +++ b/app/components/@settings/tabs/connections/GithubConnection.tsx @@ -609,10 +609,10 @@ export default function GitHubConnection() { }`} className={classNames( 'w-full px-3 py-2 rounded-lg text-sm', - 'bg-bolt-elements-background-depth-1 dark:bg-bolt-elements-background-depth-1', - 'border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor', - 'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary dark:placeholder-bolt-elements-textTertiary', - 'focus:outline-none focus:ring-1 focus:ring-bolt-elements-item-contentAccent dark:focus:ring-bolt-elements-item-contentAccent', + 'bg-[#F8F8F8] dark:bg-[#1A1A1A]', + 'border border-[#E5E5E5] dark:border-[#333333]', + 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary', + 'focus:outline-none focus:ring-1 focus:ring-bolt-elements-borderColorActive', 'disabled:opacity-50', )} /> @@ -621,11 +621,10 @@ export default function GitHubConnection() { href={`https://github.com/settings/tokens${connection.tokenType === 'fine-grained' ? '/beta' : '/new'}`} target="_blank" rel="noopener noreferrer" - className="text-bolt-elements-link-text dark:text-bolt-elements-link-text hover:text-bolt-elements-link-textHover dark:hover:text-bolt-elements-link-textHover flex items-center gap-1" + className="text-bolt-elements-borderColorActive hover:underline inline-flex items-center gap-1" > -
Get your token -
+
@@ -640,58 +639,48 @@ export default function GitHubConnection() {
{!connection.user ? ( - + ) : ( <>
- -
-
-
- - Connected to GitHub using{' '} - - {connection.tokenType === 'classic' ? 'PAT' : 'Fine-grained Token'} - - -
- {connection.rateLimit && ( -
-
- - API Limit: {connection.rateLimit.remaining.toLocaleString()}/ - {connection.rateLimit.limit.toLocaleString()} • Resets in{' '} - {Math.max(0, Math.floor((connection.rateLimit.reset * 1000 - Date.now()) / 60000))} min - -
+ className={classNames( + 'px-4 py-2 rounded-lg text-sm flex items-center gap-2', + 'bg-red-500 text-white', + 'hover:bg-red-600', )} -
+ > +
+ Disconnect + + +
+ Connected to GitHub +
+
) : (
-
- - -
- - - Connected to Netlify - -
- -
- - -
+ + +
+ Connected to Netlify +
{renderStats()}
diff --git a/app/components/@settings/tabs/connections/VercelConnection.tsx b/app/components/@settings/tabs/connections/VercelConnection.tsx index 4a442a08..85278223 100644 --- a/app/components/@settings/tabs/connections/VercelConnection.tsx +++ b/app/components/@settings/tabs/connections/VercelConnection.tsx @@ -126,9 +126,10 @@ export default function VercelConnection() { disabled={connecting || !connection.token} className={classNames( 'px-4 py-2 rounded-lg text-sm flex items-center gap-2', - 'bg-bolt-elements-borderColor text-white', - 'hover:bg-bolt-elements-borderColorActive', - 'disabled:opacity-50 disabled:cursor-not-allowed', + 'bg-[#303030] text-white', + 'hover:bg-[#5E41D0] hover:text-white', + 'disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200', + 'transform active:scale-95', )} > {connecting ? ( diff --git a/app/components/@settings/tabs/data/DataTab.tsx b/app/components/@settings/tabs/data/DataTab.tsx index 944e32ce..fda6885a 100644 --- a/app/components/@settings/tabs/data/DataTab.tsx +++ b/app/components/@settings/tabs/data/DataTab.tsx @@ -116,9 +116,6 @@ export function DataTab() { handleResetChats, handleDownloadTemplate, handleImportAPIKeys, - handleExportAPIKeys, - handleUndo, - lastOperation, } = useDataOperations({ customDb: db || undefined, // Pass the boltHistory database, converting null to undefined onReloadSettings: () => window.location.reload(), @@ -634,43 +631,6 @@ export function DataTab() {

API Keys

- - -
- -
- - - Export API Keys - -
- Export your API keys to a JSON file. - - - - - - - -
@@ -756,23 +716,6 @@ export function DataTab() {
- - {/* Undo Last Operation */} - {lastOperation && ( -
-
- Last action: {lastOperation.type} -
- -
- )}
); } diff --git a/app/components/chat/AssistantMessage.tsx b/app/components/chat/AssistantMessage.tsx index 1e3ed2d9..20e26562 100644 --- a/app/components/chat/AssistantMessage.tsx +++ b/app/components/chat/AssistantMessage.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react'; +import { memo, Fragment } from 'react'; import { Markdown } from './Markdown'; import type { JSONValue } from 'ai'; import Popover from '~/components/ui/Popover'; @@ -78,7 +78,7 @@ export const AssistantMessage = memo(({ content, annotations }: AssistantMessage {codeContext.map((x) => { const normalized = normalizedFilePath(x); return ( - <> + { @@ -89,7 +89,7 @@ export const AssistantMessage = memo(({ content, annotations }: AssistantMessage > {normalized} - + ); })}
diff --git a/app/components/chat/Chat.client.tsx b/app/components/chat/Chat.client.tsx index 9fe89c67..4f976837 100644 --- a/app/components/chat/Chat.client.tsx +++ b/app/components/chat/Chat.client.tsx @@ -81,6 +81,7 @@ export function Chat() { position="bottom-right" pauseOnFocusLoss transition={toastAnimation} + autoClose={3000} /> ); diff --git a/app/components/workbench/Preview.tsx b/app/components/workbench/Preview.tsx index aba8a24c..ec2a1cdb 100644 --- a/app/components/workbench/Preview.tsx +++ b/app/components/workbench/Preview.tsx @@ -593,7 +593,10 @@ export const Preview = memo(() => { * Intentionally disabled - we want to maintain scale of 1 * No dynamic scaling to ensure device frame matches external window exactly */ - return () => {}; + // Intentionally empty cleanup function - no cleanup needed + return () => { + // No cleanup needed + }; }, [isDeviceModeOn, showDeviceFrameInPreview, getDeviceScale, isLandscape, selectedWindowSize]); // Function to get the frame color based on dark mode @@ -711,6 +714,37 @@ export const Preview = memo(() => { title={isFullscreen ? 'Exit Full Screen' : 'Full Screen'} /> + {/* Simple preview button */} + { + if (!activePreview?.baseUrl) { + console.warn('[Preview] No active preview available'); + return; + } + + const match = activePreview.baseUrl.match( + /^https?:\/\/([^.]+)\.local-credentialless\.webcontainer-api\.io/, + ); + + if (!match) { + console.warn('[Preview] Invalid WebContainer URL:', activePreview.baseUrl); + return; + } + + const previewId = match[1]; + const previewUrl = `/webcontainer/preview/${previewId}`; + + // Open in a new window with simple parameters + window.open( + previewUrl, + `preview-${previewId}`, + 'width=1280,height=720,menubar=no,toolbar=no,location=no,status=no,resizable=yes', + ); + }} + title="Open Preview in New Window" + /> +
{ setProgressMessage(message); setProgressPercent(percent); - toast.loading(`${message} (${percent}%)`, { toastId: 'operation-progress' }); + + // Dismiss any existing progress toast before showing a new one + toast.dismiss('progress-toast'); + + toast.loading(`${message} (${percent}%)`, { + position: 'bottom-right', + autoClose: 3000, + toastId: 'progress-toast', // Use the same ID for all progress messages + }); }, []); /** @@ -68,7 +76,15 @@ export function useDataOperations({ const handleExportSettings = useCallback(async () => { setIsExporting(true); setProgressPercent(0); - toast.loading('Preparing settings export...', { toastId: 'operation-progress' }); + + // Dismiss any existing toast first + toast.dismiss('progress-toast'); + + toast.loading('Preparing settings export...', { + position: 'bottom-right', + autoClose: 3000, + toastId: 'progress-toast', + }); try { // Step 1: Export settings @@ -97,14 +113,26 @@ export function useDataOperations({ // Step 4: Complete showProgress('Completing export', 100); - toast.success('Settings exported successfully', { toastId: 'operation-progress' }); + + // Dismiss progress toast before showing success toast + toast.dismiss('progress-toast'); + + toast.success('Settings exported successfully', { + position: 'bottom-right', + autoClose: 3000, + }); // Save operation for potential undo setLastOperation({ type: 'export-settings', data: settingsData }); } catch (error) { console.error('Error exporting settings:', error); + + // Dismiss progress toast before showing error toast + toast.dismiss('progress-toast'); + toast.error(`Failed to export settings: ${error instanceof Error ? error.message : 'Unknown error'}`, { - toastId: 'operation-progress', + position: 'bottom-right', + autoClose: 3000, }); } finally { setIsExporting(false); @@ -120,14 +148,23 @@ export function useDataOperations({ const handleExportSelectedSettings = useCallback( async (categoryIds: string[]) => { if (!categoryIds || categoryIds.length === 0) { - toast.error('No settings categories selected'); + toast.error('No settings categories selected', { + position: 'bottom-right', + autoClose: 3000, + }); return; } setIsExporting(true); setProgressPercent(0); + + // Dismiss any existing toast first + toast.dismiss('progress-toast'); + toast.loading(`Preparing export of ${categoryIds.length} settings categories...`, { - toastId: 'operation-progress', + position: 'bottom-right', + autoClose: 3000, + toastId: 'progress-toast', }); try { @@ -163,7 +200,7 @@ export function useDataOperations({ const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = 'bolt-settings-selected.json'; + a.download = `bolt-settings-${categoryIds.join('-')}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); @@ -171,16 +208,29 @@ export function useDataOperations({ // Step 5: Complete showProgress('Completing export', 100); + + // Dismiss progress toast before showing success toast + toast.dismiss('progress-toast'); + toast.success(`${categoryIds.length} settings categories exported successfully`, { - toastId: 'operation-progress', + position: 'bottom-right', + autoClose: 3000, }); // Save operation for potential undo - setLastOperation({ type: 'export-selected-settings', data: { categoryIds, settings: filteredSettings } }); + setLastOperation({ + type: 'export-selected-settings', + data: { settings: filteredSettings, categories: categoryIds }, + }); } 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', + + // Dismiss progress toast before showing error toast + toast.dismiss('progress-toast'); + + toast.error(`Failed to export settings: ${error instanceof Error ? error.message : 'Unknown error'}`, { + position: 'bottom-right', + autoClose: 3000, }); } finally { setIsExporting(false); @@ -196,7 +246,10 @@ export function useDataOperations({ */ const handleExportAllChats = useCallback(async () => { if (!db) { - toast.error('Database not available'); + toast.error('Database not available', { + position: 'bottom-right', + autoClose: 3000, + }); return; } @@ -208,7 +261,15 @@ export function useDataOperations({ setIsExporting(true); setProgressPercent(0); - toast.loading('Preparing chats export...', { toastId: 'operation-progress' }); + + // Dismiss any existing toast first + toast.dismiss('progress-toast'); + + toast.loading('Preparing chats export...', { + position: 'bottom-right', + autoClose: 3000, + toastId: 'progress-toast', + }); try { // Step 1: Export chats @@ -250,6 +311,8 @@ export function useDataOperations({ exportDate: new Date().toISOString(), }; + console.log(`Preparing to export ${exportData.chats.length} chats`); + // Step 2: Create blob showProgress('Creating file', 50); @@ -271,14 +334,26 @@ export function useDataOperations({ // Step 4: Complete showProgress('Completing export', 100); - toast.success(`${exportData.chats.length} chats exported successfully`, { toastId: 'operation-progress' }); + + // Dismiss progress toast before showing success toast + toast.dismiss('progress-toast'); + + toast.success(`${exportData.chats.length} chats exported successfully`, { + position: 'bottom-right', + autoClose: 3000, + }); // Save operation for potential undo - setLastOperation({ type: 'export-all-chats', data: exportData }); + setLastOperation({ type: 'export-chats', data: exportData }); } catch (error) { console.error('Error exporting chats:', error); + + // Dismiss progress toast before showing error toast + toast.dismiss('progress-toast'); + toast.error(`Failed to export chats: ${error instanceof Error ? error.message : 'Unknown error'}`, { - toastId: 'operation-progress', + position: 'bottom-right', + autoClose: 3000, }); } finally { setIsExporting(false); @@ -294,105 +369,102 @@ export function useDataOperations({ const handleExportSelectedChats = useCallback( async (chatIds: string[]) => { if (!db) { - toast.error('Database not available'); + toast.error('Database not available', { + position: 'bottom-right', + autoClose: 3000, + }); return; } if (!chatIds || chatIds.length === 0) { - toast.error('No chats selected'); + toast.error('No chats selected', { + position: 'bottom-right', + autoClose: 3000, + }); return; } setIsExporting(true); setProgressPercent(0); - toast.loading(`Preparing export of ${chatIds.length} chats...`, { toastId: 'operation-progress' }); + + // Dismiss any existing toast first + toast.dismiss('progress-toast'); + + toast.loading(`Preparing export of ${chatIds.length} chats...`, { + position: 'bottom-right', + autoClose: 3000, + toastId: 'progress-toast', + }); try { - // Step 1: Directly query each selected chat from database - showProgress('Retrieving selected chats from database', 20); + // Step 1: Get chats from database + showProgress('Retrieving chats from database', 25); - console.log('Database details for selected chats:', { - name: db.name, - version: db.version, - objectStoreNames: Array.from(db.objectStoreNames), + const transaction = db.transaction(['chats'], 'readonly'); + const store = transaction.objectStore('chats'); + + // Create an array to store the promises for getting each chat + const chatPromises = chatIds.map((chatId) => { + return new Promise((resolve, reject) => { + const request = store.get(chatId); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); }); - // Query each chat directly from the database - const selectedChats = await Promise.all( - chatIds.map(async (chatId) => { - return new Promise((resolve, reject) => { - try { - const transaction = db.transaction(['chats'], 'readonly'); - const store = transaction.objectStore('chats'); - const request = store.get(chatId); + // Wait for all promises to resolve + const chats = await Promise.all(chatPromises); + const filteredChats = chats.filter(Boolean); // Remove any null/undefined results - 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); + console.log(`Retrieved ${filteredChats.length} chats for export`); + // Create export data const exportData = { chats: filteredChats, exportDate: new Date().toISOString(), }; - // Step 3: Create blob - showProgress('Creating file', 60); + // Step 2: Create blob + showProgress('Creating file', 50); const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json', }); - // Step 4: Download file - showProgress('Downloading file', 80); + // 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-selected.json'; + a.download = 'bolt-selected-chats.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); - // Step 5: Complete + // Step 4: Complete showProgress('Completing export', 100); - toast.success(`${filteredChats.length} chats exported successfully`, { toastId: 'operation-progress' }); + + // Dismiss progress toast before showing success toast + toast.dismiss('progress-toast'); + + toast.success(`${filteredChats.length} chats exported successfully`, { + position: 'bottom-right', + autoClose: 3000, + }); // Save operation for potential undo setLastOperation({ type: 'export-selected-chats', data: { chatIds, chats: filteredChats } }); } catch (error) { console.error('Error exporting selected chats:', error); + + // Dismiss progress toast before showing error toast + toast.dismiss('progress-toast'); + toast.error(`Failed to export selected chats: ${error instanceof Error ? error.message : 'Unknown error'}`, { - toastId: 'operation-progress', + position: 'bottom-right', + autoClose: 3000, }); } finally { setIsExporting(false); @@ -411,7 +483,15 @@ export function useDataOperations({ async (file: File) => { setIsImporting(true); setProgressPercent(0); - toast.loading(`Importing settings from ${file.name}...`, { toastId: 'operation-progress' }); + + // Dismiss any existing toast first + toast.dismiss('progress-toast'); + + toast.loading(`Importing settings from ${file.name}...`, { + position: 'bottom-right', + autoClose: 3000, + toastId: 'progress-toast', + }); try { // Step 1: Read file @@ -437,15 +517,27 @@ export function useDataOperations({ // Step 5: Complete showProgress('Completing import', 100); - toast.success('Settings imported successfully', { toastId: 'operation-progress' }); + + // Dismiss progress toast before showing success toast + toast.dismiss('progress-toast'); + + toast.success('Settings imported successfully', { + position: 'bottom-right', + autoClose: 3000, + }); if (onReloadSettings) { onReloadSettings(); } } catch (error) { console.error('Error importing settings:', error); + + // Dismiss progress toast before showing error toast + toast.dismiss('progress-toast'); + toast.error(`Failed to import settings: ${error instanceof Error ? error.message : 'Unknown error'}`, { - toastId: 'operation-progress', + position: 'bottom-right', + autoClose: 3000, }); } finally { setIsImporting(false); @@ -463,13 +555,24 @@ export function useDataOperations({ const handleImportChats = useCallback( async (file: File) => { if (!db) { - toast.error('Database not available'); + toast.error('Database not available', { + position: 'bottom-right', + autoClose: 3000, + }); return; } setIsImporting(true); setProgressPercent(0); - toast.loading(`Importing chats from ${file.name}...`, { toastId: 'operation-progress' }); + + // Dismiss any existing toast first + toast.dismiss('progress-toast'); + + toast.loading(`Importing chats from ${file.name}...`, { + position: 'bottom-right', + autoClose: 3000, + toastId: 'progress-toast', + }); try { // Step 1: Read file @@ -553,15 +656,27 @@ export function useDataOperations({ // Step 6: Complete showProgress('Completing import', 100); - toast.success(`${validatedChats.length} chats imported successfully`, { toastId: 'operation-progress' }); + + // Dismiss progress toast before showing success toast + toast.dismiss('progress-toast'); + + toast.success(`${validatedChats.length} chats imported successfully`, { + position: 'bottom-right', + autoClose: 3000, + }); if (onReloadChats) { onReloadChats(); } } catch (error) { console.error('Error importing chats:', error); + + // Dismiss progress toast before showing error toast + toast.dismiss('progress-toast'); + toast.error(`Failed to import chats: ${error instanceof Error ? error.message : 'Unknown error'}`, { - toastId: 'operation-progress', + position: 'bottom-right', + autoClose: 3000, }); } finally { setIsImporting(false); @@ -580,7 +695,15 @@ export function useDataOperations({ async (file: File) => { setIsImporting(true); setProgressPercent(0); - toast.loading(`Importing API keys from ${file.name}...`, { toastId: 'operation-progress' }); + + // Dismiss any existing toast first + toast.dismiss('progress-toast'); + + toast.loading(`Importing API keys from ${file.name}...`, { + position: 'bottom-right', + autoClose: 3000, + toastId: 'progress-toast', + }); try { // Step 1: Read file @@ -611,6 +734,9 @@ export function useDataOperations({ // Step 5: Complete showProgress('Completing import', 100); + // Dismiss progress toast before showing success toast + toast.dismiss('progress-toast'); + // Count how many keys were imported const keyCount = Object.keys(newKeys).length; const newKeyCount = Object.keys(newKeys).filter( @@ -620,7 +746,7 @@ export function useDataOperations({ 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 }, + { position: 'bottom-right', autoClose: 5000 }, ); if (onReloadSettings) { @@ -628,8 +754,13 @@ export function useDataOperations({ } } catch (error) { console.error('Error importing API keys:', error); + + // Dismiss progress toast before showing error toast + toast.dismiss('progress-toast'); + toast.error(`Failed to import API keys: ${error instanceof Error ? error.message : 'Unknown error'}`, { - toastId: 'operation-progress', + position: 'bottom-right', + autoClose: 3000, }); } finally { setIsImporting(false); @@ -646,7 +777,15 @@ export function useDataOperations({ const handleResetSettings = useCallback(async () => { setIsResetting(true); setProgressPercent(0); - toast.loading('Resetting settings...', { toastId: 'operation-progress' }); + + // Dismiss any existing toast first + toast.dismiss('progress-toast'); + + toast.loading('Resetting settings...', { + position: 'bottom-right', + autoClose: 3000, + toastId: 'progress-toast', + }); try { if (db) { @@ -662,18 +801,36 @@ export function useDataOperations({ // Step 3: Complete showProgress('Completing reset', 100); - toast.success('Settings reset successfully', { toastId: 'operation-progress' }); + + // Dismiss progress toast before showing success toast + toast.dismiss('progress-toast'); + + toast.success('Settings reset successfully', { + position: 'bottom-right', + autoClose: 3000, + }); if (onResetSettings) { onResetSettings(); } } else { - toast.error('Database not available', { toastId: 'operation-progress' }); + // Dismiss progress toast before showing error toast + toast.dismiss('progress-toast'); + + toast.error('Database not available', { + position: 'bottom-right', + autoClose: 3000, + }); } } catch (error) { console.error('Error resetting settings:', error); + + // Dismiss progress toast before showing error toast + toast.dismiss('progress-toast'); + toast.error(`Failed to reset settings: ${error instanceof Error ? error.message : 'Unknown error'}`, { - toastId: 'operation-progress', + position: 'bottom-right', + autoClose: 3000, }); } finally { setIsResetting(false); @@ -687,13 +844,24 @@ export function useDataOperations({ */ const handleResetChats = useCallback(async () => { if (!db) { - toast.error('Database not available'); + toast.error('Database not available', { + position: 'bottom-right', + autoClose: 3000, + }); return; } setIsResetting(true); setProgressPercent(0); - toast.loading('Deleting all chats...', { toastId: 'operation-progress' }); + + // Dismiss any existing toast first + toast.dismiss('progress-toast'); + + toast.loading('Deleting all chats...', { + position: 'bottom-right', + autoClose: 3000, + toastId: 'progress-toast', + }); try { // Step 1: Save current chats for potential undo @@ -708,15 +876,27 @@ export function useDataOperations({ // Step 3: Complete showProgress('Completing deletion', 100); - toast.success('All chats deleted successfully', { toastId: 'operation-progress' }); + + // Dismiss progress toast before showing success toast + toast.dismiss('progress-toast'); + + toast.success('All chats deleted successfully', { + position: 'bottom-right', + autoClose: 3000, + }); if (onResetChats) { onResetChats(); } } catch (error) { console.error('Error resetting chats:', error); + + // Dismiss progress toast before showing error toast + toast.dismiss('progress-toast'); + toast.error(`Failed to delete chats: ${error instanceof Error ? error.message : 'Unknown error'}`, { - toastId: 'operation-progress', + position: 'bottom-right', + autoClose: 3000, }); } finally { setIsResetting(false); @@ -731,7 +911,15 @@ export function useDataOperations({ const handleDownloadTemplate = useCallback(async () => { setIsDownloadingTemplate(true); setProgressPercent(0); - toast.loading('Preparing API keys template...', { toastId: 'operation-progress' }); + + // Dismiss any existing toast first + toast.dismiss('progress-toast'); + + toast.loading('Creating API keys template...', { + position: 'bottom-right', + autoClose: 3000, + toastId: 'progress-toast', + }); try { // Step 1: Create template @@ -756,11 +944,23 @@ export function useDataOperations({ // Step 3: Complete showProgress('Completing download', 100); - toast.success('API keys template downloaded successfully', { toastId: 'operation-progress' }); + + // Dismiss progress toast before showing success toast + toast.dismiss('progress-toast'); + + toast.success('Template downloaded successfully', { + position: 'bottom-right', + autoClose: 3000, + }); } catch (error) { console.error('Error downloading template:', error); + + // Dismiss progress toast before showing error toast + toast.dismiss('progress-toast'); + toast.error(`Failed to download template: ${error instanceof Error ? error.message : 'Unknown error'}`, { - toastId: 'operation-progress', + position: 'bottom-right', + autoClose: 3000, }); } finally { setIsDownloadingTemplate(false); @@ -775,7 +975,15 @@ export function useDataOperations({ const handleExportAPIKeys = useCallback(async () => { setIsExporting(true); setProgressPercent(0); - toast.loading('Preparing API keys export...', { toastId: 'operation-progress' }); + + // Dismiss any existing toast first + toast.dismiss('progress-toast'); + + toast.loading('Exporting API keys...', { + position: 'bottom-right', + autoClose: 3000, + toastId: 'progress-toast', + }); try { // Step 1: Get API keys from all sources @@ -811,14 +1019,26 @@ export function useDataOperations({ // Step 4: Complete showProgress('Completing export', 100); - toast.success('API keys exported successfully', { toastId: 'operation-progress' }); + + // Dismiss progress toast before showing success toast + toast.dismiss('progress-toast'); + + toast.success('API keys exported successfully', { + position: 'bottom-right', + autoClose: 3000, + }); // Save operation for potential undo setLastOperation({ type: 'export-api-keys', data: apiKeys }); } catch (error) { console.error('Error exporting API keys:', error); + + // Dismiss progress toast before showing error toast + toast.dismiss('progress-toast'); + toast.error(`Failed to export API keys: ${error instanceof Error ? error.message : 'Unknown error'}`, { - toastId: 'operation-progress', + position: 'bottom-right', + autoClose: 3000, }); } finally { setIsExporting(false); @@ -832,18 +1052,35 @@ export function useDataOperations({ */ const handleUndo = useCallback(async () => { if (!lastOperation || !db) { - toast.error('Nothing to undo'); + toast.error('Nothing to undo', { + position: 'bottom-right', + autoClose: 3000, + }); return; } - toast.loading('Attempting to undo last operation...', { toastId: 'operation-progress' }); + // Dismiss any existing toast first + toast.dismiss('progress-toast'); + + toast.loading('Processing undo operation...', { + position: 'bottom-right', + autoClose: 3000, + toastId: 'progress-toast', + }); try { switch (lastOperation.type) { case 'import-settings': { // Restore previous settings await ImportExportService.importSettings(lastOperation.data.previous); - toast.success('Settings import undone', { toastId: 'operation-progress' }); + + // Dismiss progress toast before showing success toast + toast.dismiss('progress-toast'); + + toast.success('Operation undone successfully', { + position: 'bottom-right', + autoClose: 3000, + }); if (onReloadSettings) { onReloadSettings(); @@ -869,7 +1106,13 @@ export function useDataOperations({ transaction.onerror = reject; }); - toast.success('Chats import undone', { toastId: 'operation-progress' }); + // Dismiss progress toast before showing success toast + toast.dismiss('progress-toast'); + + toast.success('Operation undone successfully', { + position: 'bottom-right', + autoClose: 3000, + }); if (onReloadChats) { onReloadChats(); @@ -881,7 +1124,14 @@ export function useDataOperations({ case 'reset-settings': { // Restore previous settings await ImportExportService.importSettings(lastOperation.data.previous); - toast.success('Settings reset undone', { toastId: 'operation-progress' }); + + // Dismiss progress toast before showing success toast + toast.dismiss('progress-toast'); + + toast.success('Operation undone successfully', { + position: 'bottom-right', + autoClose: 3000, + }); if (onReloadSettings) { onReloadSettings(); @@ -904,7 +1154,13 @@ export function useDataOperations({ chatTransaction.onerror = reject; }); - toast.success('Chats deletion undone', { toastId: 'operation-progress' }); + // Dismiss progress toast before showing success toast + toast.dismiss('progress-toast'); + + toast.success('Operation undone successfully', { + position: 'bottom-right', + autoClose: 3000, + }); if (onReloadChats) { onReloadChats(); @@ -919,7 +1175,14 @@ export function useDataOperations({ 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' }); + + // Dismiss progress toast before showing success toast + toast.dismiss('progress-toast'); + + toast.success('Operation undone successfully', { + position: 'bottom-right', + autoClose: 3000, + }); if (onReloadSettings) { onReloadSettings(); @@ -929,15 +1192,26 @@ export function useDataOperations({ } default: - toast.error('Cannot undo this operation', { toastId: 'operation-progress' }); + // Dismiss progress toast before showing error toast + toast.dismiss('progress-toast'); + + toast.error('Cannot undo this operation', { + position: 'bottom-right', + autoClose: 3000, + }); } // Clear the last operation after undoing setLastOperation(null); } catch (error) { console.error('Error undoing operation:', error); + + // Dismiss progress toast before showing error toast + toast.dismiss('progress-toast'); + toast.error(`Failed to undo: ${error instanceof Error ? error.message : 'Unknown error'}`, { - toastId: 'operation-progress', + position: 'bottom-right', + autoClose: 3000, }); } }, [lastOperation, db, onReloadSettings, onReloadChats]); diff --git a/app/lib/stores/previews.ts b/app/lib/stores/previews.ts index 28954844..9fedb8eb 100644 --- a/app/lib/stores/previews.ts +++ b/app/lib/stores/previews.ts @@ -153,20 +153,20 @@ export class PreviewsStore { try { // Watch for file changes - const watcher = await webcontainer.fs.watch('**/*', { persistent: true }); + webcontainer.internal.watchPaths( + { include: ['**/*'], exclude: ['**/node_modules', '.git'], includeContent: true }, + async (_events) => { + const previews = this.previews.get(); - // Use the native watch events - (watcher as any).addEventListener('change', async () => { - const previews = this.previews.get(); + for (const preview of previews) { + const previewId = this.getPreviewId(preview.baseUrl); - for (const preview of previews) { - const previewId = this.getPreviewId(preview.baseUrl); - - if (previewId) { - this.broadcastFileChange(previewId); + if (previewId) { + this.broadcastFileChange(previewId); + } } - } - }); + }, + ); // Watch for DOM changes that might affect storage if (typeof window !== 'undefined') {