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 && (
+
window.location.reload()}
+ variant="outline"
+ size="sm"
+ className="mt-auto self-start hover:bg-bolt-elements-item-backgroundActive/10 hover:text-bolt-elements-textPrimary dark:hover:bg-bolt-elements-item-backgroundActive/10 dark:hover:text-bolt-elements-textPrimary transition-colors"
+ >
+
+ Connect Now
+
+ )}
+ >
+ ) : (
+
+
+
+ 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 && (
+
window.location.reload()}
+ variant="outline"
+ size="sm"
+ className="mt-auto self-start hover:bg-bolt-elements-item-backgroundActive/10 hover:text-bolt-elements-textPrimary dark:hover:bg-bolt-elements-item-backgroundActive/10 dark:hover:text-bolt-elements-textPrimary transition-colors"
+ >
+
+ Configure Now
+
+ )}
+ >
+ ) : (
+
+
+
+ Run diagnostics to check connection status
+
+
+ )}
+
{/* Action Buttons */}
@@ -323,22 +521,42 @@ export default function ConnectionDiagnostics() {
- Reset GitHub Connection
+ Reset GitHub
- Reset Netlify Connection
+ Reset Netlify
+
+
+
+
+ Reset Vercel
+
+
+
+
+ Reset Supabase
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:
-
-
-
-
- Go to Cloudflare Pages dashboard → Your project → Settings → Environment variables
-
-
- Add both of these secrets (Production environment):
-
-
- GITHUB_ACCESS_TOKEN
{' '}
- (server-side API calls)
-
-
- VITE_GITHUB_ACCESS_TOKEN
{' '}
- (client-side access)
-
-
-
-
- Add VITE_GITHUB_TOKEN_TYPE
if
- using fine-grained tokens
-
- Deploy a fresh build after adding these variables
-
-
-
-
}>
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 ? (
-
{isConnecting ? (
<>
-
+
Connecting...
>
) : (
<>
-
+
Connect
>
)}
-
+
) : (
<>
-
-
- Disconnect
-
-
-
-
-
- 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
+
-
+
-
{isConnecting ? (
<>
-
+
Connecting...
>
) : (
<>
-
+
Connect
>
)}
-
+
) : (
-
-
-
+
+
+
Disconnect
-
-
-
-
-
- Connected to Netlify
-
-
-
-
-
window.open('https://app.netlify.com', '_blank', 'noopener,noreferrer')}
- className="flex items-center gap-2 hover:bg-bolt-elements-item-backgroundActive/10 hover:text-bolt-elements-textPrimary dark:hover:text-bolt-elements-textPrimary transition-colors"
- >
-
- Dashboard
-
-
fetchNetlifyStats(connection.token)}
- disabled={fetchingStats}
- variant="outline"
- className="flex items-center gap-2 hover:bg-bolt-elements-item-backgroundActive/10 hover:text-bolt-elements-textPrimary dark:hover:text-bolt-elements-textPrimary transition-colors"
- >
- {fetchingStats ? (
- <>
-
-
- Refreshing...
-
- >
- ) : (
- <>
-
-
- Refresh Stats
-
- >
- )}
-
-
+
+
+
+ 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.
-
-
-
-
- {isExporting ? (
- <>
-
- Exporting...
- >
- ) : (
- 'Export Keys'
- )}
-
-
-
-
-
@@ -756,23 +716,6 @@ export function DataTab() {
-
- {/* Undo Last Operation */}
- {lastOperation && (
-
-
- Last action: {lastOperation.type}
-
-
- Undo
-
-
- )}
);
}
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') {