mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
feat: update connectiontab and datatab security fix (#1614)
* feat: update connectiontab and datatab security fix # Connection Components and Diagnostics Updates ## GitHub Connection Component Changes - Updated the disconnect button styling to match Vercel's design: - Changed from `<Button>` component to native `<button>` element - Added red background (`bg-red-500`) with hover effect (`hover:bg-red-600`) - Updated icon from `i-ph:sign-out` to `i-ph:plug` - Simplified text to just "Disconnect" - Added connection status indicator with check-circle icon and "Connected to GitHub" text ## ConnectionDiagnostics Tab Updates ### Added New Connection Diagnostics - Implemented diagnostics for Vercel and Supabase connections - Added new helper function `safeJsonParse` for safer JSON parsing operations ### Diagnostic Checks Added - **Vercel Diagnostics:** - LocalStorage token verification - API endpoint connectivity test - Connection status validation - Reset functionality for Vercel connection - **Supabase Diagnostics:** - LocalStorage credentials verification - API endpoint connectivity test - Connection status validation - Reset functionality for Supabase connection ### UI Enhancements - Added new status cards for Vercel and Supabase - Implemented reset buttons with consistent styling - Added loading states during diagnostics - Enhanced error handling and user feedback ### Function Updates - Extended `runDiagnostics` function to include Vercel and Supabase checks - Added new reset helper functions for each connection type - Improved error handling and status reporting - Enhanced toast notifications for better user feedback ### Visual Consistency - Matched styling of new diagnostic cards with existing GitHub and Netlify cards - Consistent use of icons and status indicators - Uniform button styling across all connection types - Maintained consistent spacing and layout patterns ### Code Structure - Organized diagnostic checks into clear, separate sections - Improved error handling and type safety - Enhanced code readability and maintainability - Added comprehensive status compilation for all connections These changes ensure a consistent user experience across all connection types while providing robust diagnostic capabilities for troubleshooting connection issues. # DataTab.tsx Changes ## Code Cleanup - Removed unused variables from useDataOperations hook: - Removed `handleExportAPIKeys` - Removed `handleUndo` - Removed `lastOperation` This change improves code quality by removing unused variables and resolves ESLint warnings without affecting any functionality. * Test commit to verify pre-commit hook
This commit is contained in:
@@ -6,6 +6,20 @@ import { classNames } from '~/utils/classNames';
|
|||||||
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '~/components/ui/Collapsible';
|
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '~/components/ui/Collapsible';
|
||||||
import { CodeBracketIcon, ChevronDownIcon } from '@heroicons/react/24/outline';
|
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
|
* A diagnostics component to help troubleshoot connection issues
|
||||||
*/
|
*/
|
||||||
@@ -24,6 +38,8 @@ export default function ConnectionDiagnostics() {
|
|||||||
const localStorageChecks = {
|
const localStorageChecks = {
|
||||||
githubConnection: localStorage.getItem('github_connection'),
|
githubConnection: localStorage.getItem('github_connection'),
|
||||||
netlifyConnection: localStorage.getItem('netlify_connection'),
|
netlifyConnection: localStorage.getItem('netlify_connection'),
|
||||||
|
vercelConnection: localStorage.getItem('vercel_connection'),
|
||||||
|
supabaseConnection: localStorage.getItem('supabase_connection'),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get diagnostic data from server
|
// Get diagnostic data from server
|
||||||
@@ -35,36 +51,25 @@ export default function ConnectionDiagnostics() {
|
|||||||
|
|
||||||
const serverDiagnostics = await response.json();
|
const serverDiagnostics = await response.json();
|
||||||
|
|
||||||
// Get GitHub token if available
|
// === GitHub Checks ===
|
||||||
const githubToken = localStorageChecks.githubConnection
|
const githubConnectionParsed = safeJsonParse(localStorageChecks.githubConnection);
|
||||||
? JSON.parse(localStorageChecks.githubConnection)?.token
|
const githubToken = githubConnectionParsed?.token;
|
||||||
: null;
|
const githubAuthHeaders = {
|
||||||
|
|
||||||
const authHeaders = {
|
|
||||||
...(githubToken ? { Authorization: `Bearer ${githubToken}` } : {}),
|
...(githubToken ? { Authorization: `Bearer ${githubToken}` } : {}),
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Testing GitHub endpoints with token:', githubToken ? 'present' : 'missing');
|
console.log('Testing GitHub endpoints with token:', githubToken ? 'present' : 'missing');
|
||||||
|
|
||||||
// Test GitHub API endpoints
|
|
||||||
const githubEndpoints = [
|
const githubEndpoints = [
|
||||||
{ name: 'User', url: '/api/system/git-info?action=getUser' },
|
{ name: 'User', url: '/api/system/git-info?action=getUser' },
|
||||||
{ name: 'Repos', url: '/api/system/git-info?action=getRepos' },
|
{ name: 'Repos', url: '/api/system/git-info?action=getRepos' },
|
||||||
{ name: 'Default', url: '/api/system/git-info' },
|
{ name: 'Default', url: '/api/system/git-info' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const githubResults = await Promise.all(
|
const githubResults = await Promise.all(
|
||||||
githubEndpoints.map(async (endpoint) => {
|
githubEndpoints.map(async (endpoint) => {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(endpoint.url, {
|
const resp = await fetch(endpoint.url, { headers: githubAuthHeaders });
|
||||||
headers: authHeaders,
|
return { endpoint: endpoint.name, status: resp.status, ok: resp.ok };
|
||||||
});
|
|
||||||
return {
|
|
||||||
endpoint: endpoint.name,
|
|
||||||
status: resp.status,
|
|
||||||
ok: resp.ok,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
endpoint: endpoint.name,
|
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;
|
let netlifyUserCheck = null;
|
||||||
const netlifyToken = localStorageChecks.netlifyConnection
|
|
||||||
? JSON.parse(localStorageChecks.netlifyConnection || '{"token":""}').token
|
|
||||||
: '';
|
|
||||||
|
|
||||||
if (netlifyToken) {
|
if (netlifyToken) {
|
||||||
try {
|
try {
|
||||||
const netlifyResp = await fetch('https://api.netlify.com/api/v1/user', {
|
const netlifyResp = await fetch('https://api.netlify.com/api/v1/user', {
|
||||||
headers: {
|
headers: { Authorization: `Bearer ${netlifyToken}` },
|
||||||
Authorization: `Bearer ${netlifyToken}`,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
netlifyUserCheck = {
|
netlifyUserCheck = { status: netlifyResp.status, ok: netlifyResp.ok };
|
||||||
status: netlifyResp.status,
|
|
||||||
ok: netlifyResp.ok,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
netlifyUserCheck = {
|
netlifyUserCheck = {
|
||||||
error: error instanceof Error ? error.message : String(error),
|
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
|
// Compile results
|
||||||
const results = {
|
const results = {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
localStorage: {
|
localStorage: {
|
||||||
hasGithubConnection: Boolean(localStorageChecks.githubConnection),
|
hasGithubConnection: Boolean(localStorageChecks.githubConnection),
|
||||||
hasNetlifyConnection: Boolean(localStorageChecks.netlifyConnection),
|
hasNetlifyConnection: Boolean(localStorageChecks.netlifyConnection),
|
||||||
githubConnectionParsed: localStorageChecks.githubConnection
|
hasVercelConnection: Boolean(localStorageChecks.vercelConnection),
|
||||||
? JSON.parse(localStorageChecks.githubConnection)
|
hasSupabaseConnection: Boolean(localStorageChecks.supabaseConnection),
|
||||||
: null,
|
githubConnectionParsed,
|
||||||
netlifyConnectionParsed: localStorageChecks.netlifyConnection
|
netlifyConnectionParsed,
|
||||||
? JSON.parse(localStorageChecks.netlifyConnection)
|
vercelConnectionParsed,
|
||||||
: null,
|
supabaseConnectionParsed,
|
||||||
},
|
},
|
||||||
apiEndpoints: {
|
apiEndpoints: {
|
||||||
github: githubResults,
|
github: githubResults,
|
||||||
netlify: netlifyUserCheck,
|
netlify: netlifyUserCheck,
|
||||||
|
vercel: vercelUserCheck,
|
||||||
|
supabase: supabaseCheck,
|
||||||
},
|
},
|
||||||
serverDiagnostics,
|
serverDiagnostics,
|
||||||
};
|
};
|
||||||
@@ -131,7 +163,20 @@ export default function ConnectionDiagnostics() {
|
|||||||
toast.error('Netlify API connection is failing. Try reconnecting.');
|
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.');
|
toast.info('No connection data found in browser storage.');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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 = '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=/;';
|
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.');
|
toast.success('GitHub connection data cleared. Please refresh the page and reconnect.');
|
||||||
|
setDiagnosticResults(null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error clearing GitHub data:', error);
|
console.error('Error clearing GitHub data:', error);
|
||||||
toast.error('Failed to clear GitHub connection data');
|
toast.error('Failed to clear GitHub connection data');
|
||||||
@@ -163,12 +209,37 @@ export default function ConnectionDiagnostics() {
|
|||||||
localStorage.removeItem('netlify_connection');
|
localStorage.removeItem('netlify_connection');
|
||||||
document.cookie = 'netlifyToken=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
|
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.');
|
toast.success('Netlify connection data cleared. Please refresh the page and reconnect.');
|
||||||
|
setDiagnosticResults(null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error clearing Netlify data:', error);
|
console.error('Error clearing Netlify data:', error);
|
||||||
toast.error('Failed to clear Netlify connection data');
|
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 (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
{/* Connection Status Cards */}
|
{/* Connection Status Cards */}
|
||||||
@@ -303,6 +374,133 @@ export default function ConnectionDiagnostics() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Vercel Connection Card */}
|
||||||
|
<div className="p-4 rounded-lg bg-bolt-elements-background dark:bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor hover:border-bolt-elements-borderColorActive/70 dark:hover:border-bolt-elements-borderColorActive/70 transition-all duration-200 h-[180px] flex flex-col">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="i-si:vercel text-bolt-elements-item-contentAccent dark:text-bolt-elements-item-contentAccent w-4 h-4" />
|
||||||
|
<div className="text-sm font-medium text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary">
|
||||||
|
Vercel Connection
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{diagnosticResults ? (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-2 mt-2">
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
'text-xl font-semibold',
|
||||||
|
diagnosticResults.localStorage.hasVercelConnection
|
||||||
|
? 'text-green-500 dark:text-green-400'
|
||||||
|
: 'text-red-500 dark:text-red-400',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{diagnosticResults.localStorage.hasVercelConnection ? 'Connected' : 'Not Connected'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{diagnosticResults.localStorage.hasVercelConnection && (
|
||||||
|
<>
|
||||||
|
<div className="text-xs text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondary mt-2 flex items-center gap-1.5">
|
||||||
|
<div className="i-ph:user w-3.5 h-3.5 text-bolt-elements-item-contentAccent dark:text-bolt-elements-item-contentAccent" />
|
||||||
|
User:{' '}
|
||||||
|
{diagnosticResults.localStorage.vercelConnectionParsed?.user?.username ||
|
||||||
|
diagnosticResults.localStorage.vercelConnectionParsed?.user?.user?.username ||
|
||||||
|
'N/A'}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondary mt-2 flex items-center gap-1.5">
|
||||||
|
<div className="i-ph:check-circle w-3.5 h-3.5 text-bolt-elements-item-contentAccent dark:text-bolt-elements-item-contentAccent" />
|
||||||
|
API Status:{' '}
|
||||||
|
<Badge
|
||||||
|
variant={diagnosticResults.apiEndpoints.vercel?.ok ? 'default' : 'destructive'}
|
||||||
|
className="ml-1"
|
||||||
|
>
|
||||||
|
{diagnosticResults.apiEndpoints.vercel?.ok ? 'OK' : 'Failed'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!diagnosticResults.localStorage.hasVercelConnection && (
|
||||||
|
<Button
|
||||||
|
onClick={() => 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"
|
||||||
|
>
|
||||||
|
<div className="i-ph:plug w-3.5 h-3.5 mr-1" />
|
||||||
|
Connect Now
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<div className="text-sm text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondary flex items-center gap-2">
|
||||||
|
<div className="i-ph:info w-4 h-4" />
|
||||||
|
Run diagnostics to check connection status
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Supabase Connection Card */}
|
||||||
|
<div className="p-4 rounded-lg bg-bolt-elements-background dark:bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor hover:border-bolt-elements-borderColorActive/70 dark:hover:border-bolt-elements-borderColorActive/70 transition-all duration-200 h-[180px] flex flex-col">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="i-si:supabase text-bolt-elements-item-contentAccent dark:text-bolt-elements-item-contentAccent w-4 h-4" />
|
||||||
|
<div className="text-sm font-medium text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary">
|
||||||
|
Supabase Connection
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{diagnosticResults ? (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-2 mt-2">
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
'text-xl font-semibold',
|
||||||
|
diagnosticResults.localStorage.hasSupabaseConnection
|
||||||
|
? 'text-green-500 dark:text-green-400'
|
||||||
|
: 'text-red-500 dark:text-red-400',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{diagnosticResults.localStorage.hasSupabaseConnection ? 'Configured' : 'Not Configured'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{diagnosticResults.localStorage.hasSupabaseConnection && (
|
||||||
|
<>
|
||||||
|
<div className="text-xs text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondary mt-2 flex items-center gap-1.5 truncate">
|
||||||
|
<div className="i-ph:link w-3.5 h-3.5 text-bolt-elements-item-contentAccent dark:text-bolt-elements-item-contentAccent flex-shrink-0" />
|
||||||
|
Project URL: {diagnosticResults.localStorage.supabaseConnectionParsed?.projectUrl || 'N/A'}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondary mt-2 flex items-center gap-1.5">
|
||||||
|
<div className="i-ph:check-circle w-3.5 h-3.5 text-bolt-elements-item-contentAccent dark:text-bolt-elements-item-contentAccent" />
|
||||||
|
Config Status:{' '}
|
||||||
|
<Badge
|
||||||
|
variant={diagnosticResults.apiEndpoints.supabase?.ok ? 'default' : 'destructive'}
|
||||||
|
className="ml-1"
|
||||||
|
>
|
||||||
|
{diagnosticResults.apiEndpoints.supabase?.ok ? 'OK' : 'Check Failed'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!diagnosticResults.localStorage.hasSupabaseConnection && (
|
||||||
|
<Button
|
||||||
|
onClick={() => 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"
|
||||||
|
>
|
||||||
|
<div className="i-ph:plug w-3.5 h-3.5 mr-1" />
|
||||||
|
Configure Now
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<div className="text-sm text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondary flex items-center gap-2">
|
||||||
|
<div className="i-ph:info w-4 h-4" />
|
||||||
|
Run diagnostics to check connection status
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
@@ -323,22 +521,42 @@ export default function ConnectionDiagnostics() {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={resetGitHubConnection}
|
onClick={resetGitHubConnection}
|
||||||
disabled={isRunning}
|
disabled={isRunning || !diagnosticResults?.localStorage.hasGithubConnection}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="flex items-center gap-2 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"
|
className="flex items-center gap-2 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 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<div className="i-ph:github-logo w-4 h-4" />
|
<div className="i-ph:github-logo w-4 h-4" />
|
||||||
Reset GitHub Connection
|
Reset GitHub
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={resetNetlifyConnection}
|
onClick={resetNetlifyConnection}
|
||||||
disabled={isRunning}
|
disabled={isRunning || !diagnosticResults?.localStorage.hasNetlifyConnection}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="flex items-center gap-2 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"
|
className="flex items-center gap-2 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 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<div className="i-si:netlify w-4 h-4" />
|
<div className="i-si:netlify w-4 h-4" />
|
||||||
Reset Netlify Connection
|
Reset Netlify
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={resetVercelConnection}
|
||||||
|
disabled={isRunning || !diagnosticResults?.localStorage.hasVercelConnection}
|
||||||
|
variant="outline"
|
||||||
|
className="flex items-center gap-2 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 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<div className="i-si:vercel w-4 h-4" />
|
||||||
|
Reset Vercel
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={resetSupabaseConnection}
|
||||||
|
disabled={isRunning || !diagnosticResults?.localStorage.hasSupabaseConnection}
|
||||||
|
variant="outline"
|
||||||
|
className="flex items-center gap-2 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 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<div className="i-si:supabase w-4 h-4" />
|
||||||
|
Reset Supabase
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -149,48 +149,6 @@ export default function ConnectionsTab() {
|
|||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Cloudflare Deployment Note - Highly visible */}
|
|
||||||
<motion.div
|
|
||||||
className="bg-gradient-to-r from-blue-50 to-blue-100 dark:from-blue-950/40 dark:to-blue-900/30 border border-blue-200 dark:border-blue-800/50 rounded-lg shadow-sm p-4 mb-6"
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.3 }}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 mb-2 text-blue-700 dark:text-blue-400">
|
|
||||||
<div className="i-ph:cloud-bold w-5 h-5" />
|
|
||||||
<h3 className="text-base font-medium">Using Cloudflare Pages?</h3>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-blue-700 dark:text-blue-300 mb-2">
|
|
||||||
If you're experiencing GitHub connection issues (500 errors) on Cloudflare Pages deployments, you need to
|
|
||||||
configure environment variables in your Cloudflare dashboard:
|
|
||||||
</p>
|
|
||||||
<div className="bg-white/80 dark:bg-slate-900/60 rounded-md p-3 text-sm border border-blue-200 dark:border-blue-800/50">
|
|
||||||
<ol className="list-decimal list-inside pl-2 text-blue-700 dark:text-blue-300 space-y-2">
|
|
||||||
<li>
|
|
||||||
Go to <strong>Cloudflare Pages dashboard → Your project → Settings → Environment variables</strong>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Add <strong>both</strong> of these secrets (Production environment):
|
|
||||||
<ul className="list-disc list-inside pl-4 mt-1 mb-1">
|
|
||||||
<li>
|
|
||||||
<code className="px-1 py-0.5 bg-blue-100 dark:bg-blue-800/40 rounded">GITHUB_ACCESS_TOKEN</code>{' '}
|
|
||||||
(server-side API calls)
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<code className="px-1 py-0.5 bg-blue-100 dark:bg-blue-800/40 rounded">VITE_GITHUB_ACCESS_TOKEN</code>{' '}
|
|
||||||
(client-side access)
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Add <code className="px-1 py-0.5 bg-blue-100 dark:bg-blue-800/40 rounded">VITE_GITHUB_TOKEN_TYPE</code> if
|
|
||||||
using fine-grained tokens
|
|
||||||
</li>
|
|
||||||
<li>Deploy a fresh build after adding these variables</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-6">
|
<div className="grid grid-cols-1 gap-6">
|
||||||
<Suspense fallback={<LoadingFallback />}>
|
<Suspense fallback={<LoadingFallback />}>
|
||||||
<GitHubConnection />
|
<GitHubConnection />
|
||||||
|
|||||||
@@ -609,10 +609,10 @@ export default function GitHubConnection() {
|
|||||||
}`}
|
}`}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'w-full px-3 py-2 rounded-lg text-sm',
|
'w-full px-3 py-2 rounded-lg text-sm',
|
||||||
'bg-bolt-elements-background-depth-1 dark:bg-bolt-elements-background-depth-1',
|
'bg-[#F8F8F8] dark:bg-[#1A1A1A]',
|
||||||
'border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor',
|
'border border-[#E5E5E5] dark:border-[#333333]',
|
||||||
'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary dark:placeholder-bolt-elements-textTertiary',
|
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
||||||
'focus:outline-none focus:ring-1 focus:ring-bolt-elements-item-contentAccent dark:focus:ring-bolt-elements-item-contentAccent',
|
'focus:outline-none focus:ring-1 focus:ring-bolt-elements-borderColorActive',
|
||||||
'disabled:opacity-50',
|
'disabled:opacity-50',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -621,11 +621,10 @@ export default function GitHubConnection() {
|
|||||||
href={`https://github.com/settings/tokens${connection.tokenType === 'fine-grained' ? '/beta' : '/new'}`}
|
href={`https://github.com/settings/tokens${connection.tokenType === 'fine-grained' ? '/beta' : '/new'}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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"
|
||||||
>
|
>
|
||||||
<div className="i-ph:key w-4 h-4" />
|
|
||||||
Get your token
|
Get your token
|
||||||
<div className="i-ph:arrow-square-out w-3 h-3" />
|
<div className="i-ph:arrow-square-out w-4 h-4" />
|
||||||
</a>
|
</a>
|
||||||
<span className="mx-2">•</span>
|
<span className="mx-2">•</span>
|
||||||
<span>
|
<span>
|
||||||
@@ -640,58 +639,48 @@ export default function GitHubConnection() {
|
|||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
{!connection.user ? (
|
{!connection.user ? (
|
||||||
<Button
|
<button
|
||||||
onClick={handleConnect}
|
onClick={handleConnect}
|
||||||
disabled={isConnecting || !connection.token}
|
disabled={isConnecting || !connection.token}
|
||||||
variant="default"
|
className={classNames(
|
||||||
className="flex items-center gap-2"
|
'px-4 py-2 rounded-lg text-sm flex items-center gap-2',
|
||||||
|
'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',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{isConnecting ? (
|
{isConnecting ? (
|
||||||
<>
|
<>
|
||||||
<div className="i-ph:spinner-gap animate-spin w-4 h-4" />
|
<div className="i-ph:spinner-gap animate-spin" />
|
||||||
Connecting...
|
Connecting...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="i-ph:github-logo w-4 h-4" />
|
<div className="i-ph:plug-charging w-4 h-4" />
|
||||||
Connect
|
Connect
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between w-full">
|
<div className="flex items-center justify-between w-full">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Button
|
<button
|
||||||
onClick={handleDisconnect}
|
onClick={handleDisconnect}
|
||||||
variant="destructive"
|
className={classNames(
|
||||||
size="sm"
|
'px-4 py-2 rounded-lg text-sm flex items-center gap-2',
|
||||||
className="flex items-center gap-2"
|
'bg-red-500 text-white',
|
||||||
>
|
'hover:bg-red-600',
|
||||||
<div className="i-ph:sign-out w-4 h-4" />
|
|
||||||
Disconnect
|
|
||||||
</Button>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="i-ph:check-circle w-4 h-4 text-bolt-elements-icon-success dark:text-bolt-elements-icon-success" />
|
|
||||||
<span className="text-sm text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary">
|
|
||||||
Connected to GitHub using{' '}
|
|
||||||
<span className="text-bolt-elements-item-contentAccent dark:text-bolt-elements-item-contentAccent font-medium">
|
|
||||||
{connection.tokenType === 'classic' ? 'PAT' : 'Fine-grained Token'}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{connection.rateLimit && (
|
|
||||||
<div className="flex items-center gap-2 text-xs text-bolt-elements-textSecondary">
|
|
||||||
<div className="i-ph:chart-line-up w-3.5 h-3.5 text-bolt-elements-icon-success" />
|
|
||||||
<span>
|
|
||||||
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
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
>
|
||||||
|
<div className="i-ph:plug w-4 h-4" />
|
||||||
|
Disconnect
|
||||||
|
</button>
|
||||||
|
<span className="text-sm text-bolt-elements-textSecondary flex items-center gap-1">
|
||||||
|
<div className="i-ph:check-circle w-4 h-4 text-green-500" />
|
||||||
|
Connected to GitHub
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -659,92 +659,68 @@ export default function NetlifyConnection() {
|
|||||||
placeholder="Enter your Netlify API token"
|
placeholder="Enter your Netlify API token"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'w-full px-3 py-2 rounded-lg text-sm',
|
'w-full px-3 py-2 rounded-lg text-sm',
|
||||||
'bg-bolt-elements-background-depth-1 dark:bg-bolt-elements-background-depth-1',
|
'bg-[#F8F8F8] dark:bg-[#1A1A1A]',
|
||||||
'border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor',
|
'border border-[#E5E5E5] dark:border-[#333333]',
|
||||||
'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary dark:placeholder-bolt-elements-textTertiary',
|
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
||||||
'focus:outline-none focus:ring-1 focus:ring-bolt-elements-item-contentAccent dark:focus:ring-bolt-elements-item-contentAccent',
|
'focus:outline-none focus:ring-1 focus:ring-bolt-elements-borderColorActive',
|
||||||
|
'disabled:opacity-50',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="mt-2 text-sm text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondary">
|
<div className="mt-2 text-sm text-bolt-elements-textSecondary">
|
||||||
<a
|
<a
|
||||||
href="https://app.netlify.com/user/applications#personal-access-tokens"
|
href="https://app.netlify.com/user/applications#personal-access-tokens"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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"
|
||||||
>
|
>
|
||||||
<div className="i-ph:key w-4 h-4" />
|
|
||||||
Get your token
|
Get your token
|
||||||
<div className="i-ph:arrow-square-out w-3 h-3" />
|
<div className="i-ph:arrow-square-out w-4 h-4" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between mt-4">
|
<div className="flex items-center justify-between mt-4">
|
||||||
<Button
|
<button
|
||||||
onClick={handleConnect}
|
onClick={handleConnect}
|
||||||
disabled={isConnecting || !tokenInput}
|
disabled={isConnecting || !tokenInput}
|
||||||
variant="default"
|
className={classNames(
|
||||||
className="flex items-center gap-2"
|
'px-4 py-2 rounded-lg text-sm flex items-center gap-2',
|
||||||
|
'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',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{isConnecting ? (
|
{isConnecting ? (
|
||||||
<>
|
<>
|
||||||
<div className="i-ph:spinner-gap animate-spin w-4 h-4" />
|
<div className="i-ph:spinner-gap animate-spin" />
|
||||||
Connecting...
|
Connecting...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<CloudIcon className="w-4 h-4" />
|
<div className="i-ph:plug-charging w-4 h-4" />
|
||||||
Connect
|
Connect
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col w-full gap-4 mt-4">
|
<div className="flex flex-col w-full gap-4 mt-4">
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Button onClick={handleDisconnect} variant="destructive" size="sm" className="flex items-center gap-2">
|
<button
|
||||||
<div className="i-ph:sign-out w-4 h-4" />
|
onClick={handleDisconnect}
|
||||||
|
className={classNames(
|
||||||
|
'px-4 py-2 rounded-lg text-sm flex items-center gap-2',
|
||||||
|
'bg-red-500 text-white',
|
||||||
|
'hover:bg-red-600',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="i-ph:plug w-4 h-4" />
|
||||||
Disconnect
|
Disconnect
|
||||||
</Button>
|
</button>
|
||||||
|
<span className="text-sm text-bolt-elements-textSecondary flex items-center gap-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="i-ph:check-circle w-4 h-4 text-green-500" />
|
||||||
<CheckCircleIcon className="h-4 w-4 text-green-500" />
|
Connected to Netlify
|
||||||
<span className="text-sm text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary">
|
</span>
|
||||||
Connected to Netlify
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2 ml-auto">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => 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"
|
|
||||||
>
|
|
||||||
<div className="i-ph:layout-dashboard w-4 h-4" />
|
|
||||||
Dashboard
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => 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 ? (
|
|
||||||
<>
|
|
||||||
<div className="i-ph:spinner-gap w-4 h-4 animate-spin text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary" />
|
|
||||||
<span className="text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary">
|
|
||||||
Refreshing...
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ArrowPathIcon className="h-4 w-4 text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary" />
|
|
||||||
<span className="text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary">
|
|
||||||
Refresh Stats
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{renderStats()}
|
{renderStats()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -126,9 +126,10 @@ export default function VercelConnection() {
|
|||||||
disabled={connecting || !connection.token}
|
disabled={connecting || !connection.token}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'px-4 py-2 rounded-lg text-sm flex items-center gap-2',
|
'px-4 py-2 rounded-lg text-sm flex items-center gap-2',
|
||||||
'bg-bolt-elements-borderColor text-white',
|
'bg-[#303030] text-white',
|
||||||
'hover:bg-bolt-elements-borderColorActive',
|
'hover:bg-[#5E41D0] hover:text-white',
|
||||||
'disabled:opacity-50 disabled:cursor-not-allowed',
|
'disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200',
|
||||||
|
'transform active:scale-95',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{connecting ? (
|
{connecting ? (
|
||||||
|
|||||||
@@ -116,9 +116,6 @@ export function DataTab() {
|
|||||||
handleResetChats,
|
handleResetChats,
|
||||||
handleDownloadTemplate,
|
handleDownloadTemplate,
|
||||||
handleImportAPIKeys,
|
handleImportAPIKeys,
|
||||||
handleExportAPIKeys,
|
|
||||||
handleUndo,
|
|
||||||
lastOperation,
|
|
||||||
} = useDataOperations({
|
} = useDataOperations({
|
||||||
customDb: db || undefined, // Pass the boltHistory database, converting null to undefined
|
customDb: db || undefined, // Pass the boltHistory database, converting null to undefined
|
||||||
onReloadSettings: () => window.location.reload(),
|
onReloadSettings: () => window.location.reload(),
|
||||||
@@ -634,43 +631,6 @@ export function DataTab() {
|
|||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-semibold mb-4 text-bolt-elements-textPrimary">API Keys</h2>
|
<h2 className="text-xl font-semibold mb-4 text-bolt-elements-textPrimary">API Keys</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<div className="flex items-center mb-2">
|
|
||||||
<motion.div className="text-accent-500 mr-2" whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
|
||||||
<div className="i-ph-download-duotone w-5 h-5" />
|
|
||||||
</motion.div>
|
|
||||||
<CardTitle className="text-lg group-hover:text-bolt-elements-item-contentAccent transition-colors">
|
|
||||||
Export API Keys
|
|
||||||
</CardTitle>
|
|
||||||
</div>
|
|
||||||
<CardDescription>Export your API keys to a JSON file.</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardFooter>
|
|
||||||
<motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} className="w-full">
|
|
||||||
<Button
|
|
||||||
onClick={handleExportAPIKeys}
|
|
||||||
disabled={isExporting}
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className={classNames(
|
|
||||||
'hover:text-bolt-elements-item-contentAccent hover:border-bolt-elements-item-backgroundAccent hover:bg-bolt-elements-item-backgroundAccent transition-colors w-full justify-center',
|
|
||||||
isExporting ? 'cursor-not-allowed' : '',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isExporting ? (
|
|
||||||
<>
|
|
||||||
<div className="i-ph-spinner-gap-bold animate-spin w-4 h-4 mr-2" />
|
|
||||||
Exporting...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
'Export Keys'
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</motion.div>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center mb-2">
|
<div className="flex items-center mb-2">
|
||||||
@@ -756,23 +716,6 @@ export function DataTab() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Undo Last Operation */}
|
|
||||||
{lastOperation && (
|
|
||||||
<div className="fixed bottom-4 right-4 bg-bolt-elements-bg-depth-3 text-bolt-elements-textPrimary p-4 rounded-lg shadow-lg flex items-center gap-3 z-50">
|
|
||||||
<div className="text-sm">
|
|
||||||
<span className="font-medium">Last action:</span> {lastOperation.type}
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
onClick={handleUndo}
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="border-bolt-elements-borderColor text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundAccent hover:text-bolt-elements-item-contentAccent"
|
|
||||||
>
|
|
||||||
Undo
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo } from 'react';
|
import { memo, Fragment } from 'react';
|
||||||
import { Markdown } from './Markdown';
|
import { Markdown } from './Markdown';
|
||||||
import type { JSONValue } from 'ai';
|
import type { JSONValue } from 'ai';
|
||||||
import Popover from '~/components/ui/Popover';
|
import Popover from '~/components/ui/Popover';
|
||||||
@@ -78,7 +78,7 @@ export const AssistantMessage = memo(({ content, annotations }: AssistantMessage
|
|||||||
{codeContext.map((x) => {
|
{codeContext.map((x) => {
|
||||||
const normalized = normalizedFilePath(x);
|
const normalized = normalizedFilePath(x);
|
||||||
return (
|
return (
|
||||||
<>
|
<Fragment key={normalized}>
|
||||||
<code
|
<code
|
||||||
className="bg-bolt-elements-artifacts-inlineCode-background text-bolt-elements-artifacts-inlineCode-text px-1.5 py-1 rounded-md text-bolt-elements-item-contentAccent hover:underline cursor-pointer"
|
className="bg-bolt-elements-artifacts-inlineCode-background text-bolt-elements-artifacts-inlineCode-text px-1.5 py-1 rounded-md text-bolt-elements-item-contentAccent hover:underline cursor-pointer"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@@ -89,7 +89,7 @@ export const AssistantMessage = memo(({ content, annotations }: AssistantMessage
|
|||||||
>
|
>
|
||||||
{normalized}
|
{normalized}
|
||||||
</code>
|
</code>
|
||||||
</>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export function Chat() {
|
|||||||
position="bottom-right"
|
position="bottom-right"
|
||||||
pauseOnFocusLoss
|
pauseOnFocusLoss
|
||||||
transition={toastAnimation}
|
transition={toastAnimation}
|
||||||
|
autoClose={3000}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -593,7 +593,10 @@ export const Preview = memo(() => {
|
|||||||
* Intentionally disabled - we want to maintain scale of 1
|
* Intentionally disabled - we want to maintain scale of 1
|
||||||
* No dynamic scaling to ensure device frame matches external window exactly
|
* 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]);
|
}, [isDeviceModeOn, showDeviceFrameInPreview, getDeviceScale, isLandscape, selectedWindowSize]);
|
||||||
|
|
||||||
// Function to get the frame color based on dark mode
|
// 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'}
|
title={isFullscreen ? 'Exit Full Screen' : 'Full Screen'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Simple preview button */}
|
||||||
|
<IconButton
|
||||||
|
icon="i-ph:browser"
|
||||||
|
onClick={() => {
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex items-center relative">
|
<div className="flex items-center relative">
|
||||||
<IconButton
|
<IconButton
|
||||||
icon="i-ph:arrow-square-out"
|
icon="i-ph:arrow-square-out"
|
||||||
|
|||||||
@@ -59,7 +59,15 @@ export function useDataOperations({
|
|||||||
const showProgress = useCallback((message: string, percent: number) => {
|
const showProgress = useCallback((message: string, percent: number) => {
|
||||||
setProgressMessage(message);
|
setProgressMessage(message);
|
||||||
setProgressPercent(percent);
|
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 () => {
|
const handleExportSettings = useCallback(async () => {
|
||||||
setIsExporting(true);
|
setIsExporting(true);
|
||||||
setProgressPercent(0);
|
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 {
|
try {
|
||||||
// Step 1: Export settings
|
// Step 1: Export settings
|
||||||
@@ -97,14 +113,26 @@ export function useDataOperations({
|
|||||||
|
|
||||||
// Step 4: Complete
|
// Step 4: Complete
|
||||||
showProgress('Completing export', 100);
|
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
|
// Save operation for potential undo
|
||||||
setLastOperation({ type: 'export-settings', data: settingsData });
|
setLastOperation({ type: 'export-settings', data: settingsData });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error exporting settings:', 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'}`, {
|
toast.error(`Failed to export settings: ${error instanceof Error ? error.message : 'Unknown error'}`, {
|
||||||
toastId: 'operation-progress',
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsExporting(false);
|
setIsExporting(false);
|
||||||
@@ -120,14 +148,23 @@ export function useDataOperations({
|
|||||||
const handleExportSelectedSettings = useCallback(
|
const handleExportSelectedSettings = useCallback(
|
||||||
async (categoryIds: string[]) => {
|
async (categoryIds: string[]) => {
|
||||||
if (!categoryIds || categoryIds.length === 0) {
|
if (!categoryIds || categoryIds.length === 0) {
|
||||||
toast.error('No settings categories selected');
|
toast.error('No settings categories selected', {
|
||||||
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsExporting(true);
|
setIsExporting(true);
|
||||||
setProgressPercent(0);
|
setProgressPercent(0);
|
||||||
|
|
||||||
|
// Dismiss any existing toast first
|
||||||
|
toast.dismiss('progress-toast');
|
||||||
|
|
||||||
toast.loading(`Preparing export of ${categoryIds.length} settings categories...`, {
|
toast.loading(`Preparing export of ${categoryIds.length} settings categories...`, {
|
||||||
toastId: 'operation-progress',
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
|
toastId: 'progress-toast',
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -163,7 +200,7 @@ export function useDataOperations({
|
|||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = 'bolt-settings-selected.json';
|
a.download = `bolt-settings-${categoryIds.join('-')}.json`;
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
@@ -171,16 +208,29 @@ export function useDataOperations({
|
|||||||
|
|
||||||
// Step 5: Complete
|
// Step 5: Complete
|
||||||
showProgress('Completing export', 100);
|
showProgress('Completing export', 100);
|
||||||
|
|
||||||
|
// Dismiss progress toast before showing success toast
|
||||||
|
toast.dismiss('progress-toast');
|
||||||
|
|
||||||
toast.success(`${categoryIds.length} settings categories exported successfully`, {
|
toast.success(`${categoryIds.length} settings categories exported successfully`, {
|
||||||
toastId: 'operation-progress',
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save operation for potential undo
|
// 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) {
|
} catch (error) {
|
||||||
console.error('Error exporting selected settings:', 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 {
|
} finally {
|
||||||
setIsExporting(false);
|
setIsExporting(false);
|
||||||
@@ -196,7 +246,10 @@ export function useDataOperations({
|
|||||||
*/
|
*/
|
||||||
const handleExportAllChats = useCallback(async () => {
|
const handleExportAllChats = useCallback(async () => {
|
||||||
if (!db) {
|
if (!db) {
|
||||||
toast.error('Database not available');
|
toast.error('Database not available', {
|
||||||
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,7 +261,15 @@ export function useDataOperations({
|
|||||||
|
|
||||||
setIsExporting(true);
|
setIsExporting(true);
|
||||||
setProgressPercent(0);
|
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 {
|
try {
|
||||||
// Step 1: Export chats
|
// Step 1: Export chats
|
||||||
@@ -250,6 +311,8 @@ export function useDataOperations({
|
|||||||
exportDate: new Date().toISOString(),
|
exportDate: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log(`Preparing to export ${exportData.chats.length} chats`);
|
||||||
|
|
||||||
// Step 2: Create blob
|
// Step 2: Create blob
|
||||||
showProgress('Creating file', 50);
|
showProgress('Creating file', 50);
|
||||||
|
|
||||||
@@ -271,14 +334,26 @@ export function useDataOperations({
|
|||||||
|
|
||||||
// Step 4: Complete
|
// Step 4: Complete
|
||||||
showProgress('Completing export', 100);
|
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
|
// Save operation for potential undo
|
||||||
setLastOperation({ type: 'export-all-chats', data: exportData });
|
setLastOperation({ type: 'export-chats', data: exportData });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error exporting chats:', 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'}`, {
|
toast.error(`Failed to export chats: ${error instanceof Error ? error.message : 'Unknown error'}`, {
|
||||||
toastId: 'operation-progress',
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsExporting(false);
|
setIsExporting(false);
|
||||||
@@ -294,105 +369,102 @@ export function useDataOperations({
|
|||||||
const handleExportSelectedChats = useCallback(
|
const handleExportSelectedChats = useCallback(
|
||||||
async (chatIds: string[]) => {
|
async (chatIds: string[]) => {
|
||||||
if (!db) {
|
if (!db) {
|
||||||
toast.error('Database not available');
|
toast.error('Database not available', {
|
||||||
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chatIds || chatIds.length === 0) {
|
if (!chatIds || chatIds.length === 0) {
|
||||||
toast.error('No chats selected');
|
toast.error('No chats selected', {
|
||||||
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsExporting(true);
|
setIsExporting(true);
|
||||||
setProgressPercent(0);
|
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 {
|
try {
|
||||||
// Step 1: Directly query each selected chat from database
|
// Step 1: Get chats from database
|
||||||
showProgress('Retrieving selected chats from database', 20);
|
showProgress('Retrieving chats from database', 25);
|
||||||
|
|
||||||
console.log('Database details for selected chats:', {
|
const transaction = db.transaction(['chats'], 'readonly');
|
||||||
name: db.name,
|
const store = transaction.objectStore('chats');
|
||||||
version: db.version,
|
|
||||||
objectStoreNames: Array.from(db.objectStoreNames),
|
// Create an array to store the promises for getting each chat
|
||||||
|
const chatPromises = chatIds.map((chatId) => {
|
||||||
|
return new Promise<any>((resolve, reject) => {
|
||||||
|
const request = store.get(chatId);
|
||||||
|
request.onsuccess = () => resolve(request.result);
|
||||||
|
request.onerror = () => reject(request.error);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Query each chat directly from the database
|
// Wait for all promises to resolve
|
||||||
const selectedChats = await Promise.all(
|
const chats = await Promise.all(chatPromises);
|
||||||
chatIds.map(async (chatId) => {
|
const filteredChats = chats.filter(Boolean); // Remove any null/undefined results
|
||||||
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 = () => {
|
console.log(`Retrieved ${filteredChats.length} chats for export`);
|
||||||
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);
|
|
||||||
|
|
||||||
|
// Create export data
|
||||||
const exportData = {
|
const exportData = {
|
||||||
chats: filteredChats,
|
chats: filteredChats,
|
||||||
exportDate: new Date().toISOString(),
|
exportDate: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Step 3: Create blob
|
// Step 2: Create blob
|
||||||
showProgress('Creating file', 60);
|
showProgress('Creating file', 50);
|
||||||
|
|
||||||
const blob = new Blob([JSON.stringify(exportData, null, 2)], {
|
const blob = new Blob([JSON.stringify(exportData, null, 2)], {
|
||||||
type: 'application/json',
|
type: 'application/json',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Step 4: Download file
|
// Step 3: Download file
|
||||||
showProgress('Downloading file', 80);
|
showProgress('Downloading file', 75);
|
||||||
|
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = 'bolt-chats-selected.json';
|
a.download = 'bolt-selected-chats.json';
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
// Step 5: Complete
|
// Step 4: Complete
|
||||||
showProgress('Completing export', 100);
|
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
|
// Save operation for potential undo
|
||||||
setLastOperation({ type: 'export-selected-chats', data: { chatIds, chats: filteredChats } });
|
setLastOperation({ type: 'export-selected-chats', data: { chatIds, chats: filteredChats } });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error exporting selected chats:', 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'}`, {
|
toast.error(`Failed to export selected chats: ${error instanceof Error ? error.message : 'Unknown error'}`, {
|
||||||
toastId: 'operation-progress',
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsExporting(false);
|
setIsExporting(false);
|
||||||
@@ -411,7 +483,15 @@ export function useDataOperations({
|
|||||||
async (file: File) => {
|
async (file: File) => {
|
||||||
setIsImporting(true);
|
setIsImporting(true);
|
||||||
setProgressPercent(0);
|
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 {
|
try {
|
||||||
// Step 1: Read file
|
// Step 1: Read file
|
||||||
@@ -437,15 +517,27 @@ export function useDataOperations({
|
|||||||
|
|
||||||
// Step 5: Complete
|
// Step 5: Complete
|
||||||
showProgress('Completing import', 100);
|
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) {
|
if (onReloadSettings) {
|
||||||
onReloadSettings();
|
onReloadSettings();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error importing settings:', 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'}`, {
|
toast.error(`Failed to import settings: ${error instanceof Error ? error.message : 'Unknown error'}`, {
|
||||||
toastId: 'operation-progress',
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsImporting(false);
|
setIsImporting(false);
|
||||||
@@ -463,13 +555,24 @@ export function useDataOperations({
|
|||||||
const handleImportChats = useCallback(
|
const handleImportChats = useCallback(
|
||||||
async (file: File) => {
|
async (file: File) => {
|
||||||
if (!db) {
|
if (!db) {
|
||||||
toast.error('Database not available');
|
toast.error('Database not available', {
|
||||||
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsImporting(true);
|
setIsImporting(true);
|
||||||
setProgressPercent(0);
|
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 {
|
try {
|
||||||
// Step 1: Read file
|
// Step 1: Read file
|
||||||
@@ -553,15 +656,27 @@ export function useDataOperations({
|
|||||||
|
|
||||||
// Step 6: Complete
|
// Step 6: Complete
|
||||||
showProgress('Completing import', 100);
|
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) {
|
if (onReloadChats) {
|
||||||
onReloadChats();
|
onReloadChats();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error importing chats:', 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'}`, {
|
toast.error(`Failed to import chats: ${error instanceof Error ? error.message : 'Unknown error'}`, {
|
||||||
toastId: 'operation-progress',
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsImporting(false);
|
setIsImporting(false);
|
||||||
@@ -580,7 +695,15 @@ export function useDataOperations({
|
|||||||
async (file: File) => {
|
async (file: File) => {
|
||||||
setIsImporting(true);
|
setIsImporting(true);
|
||||||
setProgressPercent(0);
|
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 {
|
try {
|
||||||
// Step 1: Read file
|
// Step 1: Read file
|
||||||
@@ -611,6 +734,9 @@ export function useDataOperations({
|
|||||||
// Step 5: Complete
|
// Step 5: Complete
|
||||||
showProgress('Completing import', 100);
|
showProgress('Completing import', 100);
|
||||||
|
|
||||||
|
// Dismiss progress toast before showing success toast
|
||||||
|
toast.dismiss('progress-toast');
|
||||||
|
|
||||||
// Count how many keys were imported
|
// Count how many keys were imported
|
||||||
const keyCount = Object.keys(newKeys).length;
|
const keyCount = Object.keys(newKeys).length;
|
||||||
const newKeyCount = Object.keys(newKeys).filter(
|
const newKeyCount = Object.keys(newKeys).filter(
|
||||||
@@ -620,7 +746,7 @@ export function useDataOperations({
|
|||||||
toast.success(
|
toast.success(
|
||||||
`${keyCount} API keys imported successfully (${newKeyCount} new/updated)\n` +
|
`${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.',
|
'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) {
|
if (onReloadSettings) {
|
||||||
@@ -628,8 +754,13 @@ export function useDataOperations({
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error importing API keys:', 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'}`, {
|
toast.error(`Failed to import API keys: ${error instanceof Error ? error.message : 'Unknown error'}`, {
|
||||||
toastId: 'operation-progress',
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsImporting(false);
|
setIsImporting(false);
|
||||||
@@ -646,7 +777,15 @@ export function useDataOperations({
|
|||||||
const handleResetSettings = useCallback(async () => {
|
const handleResetSettings = useCallback(async () => {
|
||||||
setIsResetting(true);
|
setIsResetting(true);
|
||||||
setProgressPercent(0);
|
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 {
|
try {
|
||||||
if (db) {
|
if (db) {
|
||||||
@@ -662,18 +801,36 @@ export function useDataOperations({
|
|||||||
|
|
||||||
// Step 3: Complete
|
// Step 3: Complete
|
||||||
showProgress('Completing reset', 100);
|
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) {
|
if (onResetSettings) {
|
||||||
onResetSettings();
|
onResetSettings();
|
||||||
}
|
}
|
||||||
} else {
|
} 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) {
|
} catch (error) {
|
||||||
console.error('Error resetting settings:', 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'}`, {
|
toast.error(`Failed to reset settings: ${error instanceof Error ? error.message : 'Unknown error'}`, {
|
||||||
toastId: 'operation-progress',
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsResetting(false);
|
setIsResetting(false);
|
||||||
@@ -687,13 +844,24 @@ export function useDataOperations({
|
|||||||
*/
|
*/
|
||||||
const handleResetChats = useCallback(async () => {
|
const handleResetChats = useCallback(async () => {
|
||||||
if (!db) {
|
if (!db) {
|
||||||
toast.error('Database not available');
|
toast.error('Database not available', {
|
||||||
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsResetting(true);
|
setIsResetting(true);
|
||||||
setProgressPercent(0);
|
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 {
|
try {
|
||||||
// Step 1: Save current chats for potential undo
|
// Step 1: Save current chats for potential undo
|
||||||
@@ -708,15 +876,27 @@ export function useDataOperations({
|
|||||||
|
|
||||||
// Step 3: Complete
|
// Step 3: Complete
|
||||||
showProgress('Completing deletion', 100);
|
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) {
|
if (onResetChats) {
|
||||||
onResetChats();
|
onResetChats();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error resetting chats:', 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'}`, {
|
toast.error(`Failed to delete chats: ${error instanceof Error ? error.message : 'Unknown error'}`, {
|
||||||
toastId: 'operation-progress',
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsResetting(false);
|
setIsResetting(false);
|
||||||
@@ -731,7 +911,15 @@ export function useDataOperations({
|
|||||||
const handleDownloadTemplate = useCallback(async () => {
|
const handleDownloadTemplate = useCallback(async () => {
|
||||||
setIsDownloadingTemplate(true);
|
setIsDownloadingTemplate(true);
|
||||||
setProgressPercent(0);
|
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 {
|
try {
|
||||||
// Step 1: Create template
|
// Step 1: Create template
|
||||||
@@ -756,11 +944,23 @@ export function useDataOperations({
|
|||||||
|
|
||||||
// Step 3: Complete
|
// Step 3: Complete
|
||||||
showProgress('Completing download', 100);
|
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) {
|
} catch (error) {
|
||||||
console.error('Error downloading template:', 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'}`, {
|
toast.error(`Failed to download template: ${error instanceof Error ? error.message : 'Unknown error'}`, {
|
||||||
toastId: 'operation-progress',
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsDownloadingTemplate(false);
|
setIsDownloadingTemplate(false);
|
||||||
@@ -775,7 +975,15 @@ export function useDataOperations({
|
|||||||
const handleExportAPIKeys = useCallback(async () => {
|
const handleExportAPIKeys = useCallback(async () => {
|
||||||
setIsExporting(true);
|
setIsExporting(true);
|
||||||
setProgressPercent(0);
|
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 {
|
try {
|
||||||
// Step 1: Get API keys from all sources
|
// Step 1: Get API keys from all sources
|
||||||
@@ -811,14 +1019,26 @@ export function useDataOperations({
|
|||||||
|
|
||||||
// Step 4: Complete
|
// Step 4: Complete
|
||||||
showProgress('Completing export', 100);
|
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
|
// Save operation for potential undo
|
||||||
setLastOperation({ type: 'export-api-keys', data: apiKeys });
|
setLastOperation({ type: 'export-api-keys', data: apiKeys });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error exporting API keys:', 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'}`, {
|
toast.error(`Failed to export API keys: ${error instanceof Error ? error.message : 'Unknown error'}`, {
|
||||||
toastId: 'operation-progress',
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsExporting(false);
|
setIsExporting(false);
|
||||||
@@ -832,18 +1052,35 @@ export function useDataOperations({
|
|||||||
*/
|
*/
|
||||||
const handleUndo = useCallback(async () => {
|
const handleUndo = useCallback(async () => {
|
||||||
if (!lastOperation || !db) {
|
if (!lastOperation || !db) {
|
||||||
toast.error('Nothing to undo');
|
toast.error('Nothing to undo', {
|
||||||
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
|
});
|
||||||
return;
|
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 {
|
try {
|
||||||
switch (lastOperation.type) {
|
switch (lastOperation.type) {
|
||||||
case 'import-settings': {
|
case 'import-settings': {
|
||||||
// Restore previous settings
|
// Restore previous settings
|
||||||
await ImportExportService.importSettings(lastOperation.data.previous);
|
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) {
|
if (onReloadSettings) {
|
||||||
onReloadSettings();
|
onReloadSettings();
|
||||||
@@ -869,7 +1106,13 @@ export function useDataOperations({
|
|||||||
transaction.onerror = reject;
|
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) {
|
if (onReloadChats) {
|
||||||
onReloadChats();
|
onReloadChats();
|
||||||
@@ -881,7 +1124,14 @@ export function useDataOperations({
|
|||||||
case 'reset-settings': {
|
case 'reset-settings': {
|
||||||
// Restore previous settings
|
// Restore previous settings
|
||||||
await ImportExportService.importSettings(lastOperation.data.previous);
|
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) {
|
if (onReloadSettings) {
|
||||||
onReloadSettings();
|
onReloadSettings();
|
||||||
@@ -904,7 +1154,13 @@ export function useDataOperations({
|
|||||||
chatTransaction.onerror = reject;
|
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) {
|
if (onReloadChats) {
|
||||||
onReloadChats();
|
onReloadChats();
|
||||||
@@ -919,7 +1175,14 @@ export function useDataOperations({
|
|||||||
const newKeys = ImportExportService.importAPIKeys(previousAPIKeys);
|
const newKeys = ImportExportService.importAPIKeys(previousAPIKeys);
|
||||||
const apiKeysJson = JSON.stringify(newKeys);
|
const apiKeysJson = JSON.stringify(newKeys);
|
||||||
document.cookie = `apiKeys=${apiKeysJson}; path=/; max-age=31536000`;
|
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) {
|
if (onReloadSettings) {
|
||||||
onReloadSettings();
|
onReloadSettings();
|
||||||
@@ -929,15 +1192,26 @@ export function useDataOperations({
|
|||||||
}
|
}
|
||||||
|
|
||||||
default:
|
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
|
// Clear the last operation after undoing
|
||||||
setLastOperation(null);
|
setLastOperation(null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error undoing operation:', 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'}`, {
|
toast.error(`Failed to undo: ${error instanceof Error ? error.message : 'Unknown error'}`, {
|
||||||
toastId: 'operation-progress',
|
position: 'bottom-right',
|
||||||
|
autoClose: 3000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [lastOperation, db, onReloadSettings, onReloadChats]);
|
}, [lastOperation, db, onReloadSettings, onReloadChats]);
|
||||||
|
|||||||
@@ -153,20 +153,20 @@ export class PreviewsStore {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Watch for file changes
|
// 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
|
for (const preview of previews) {
|
||||||
(watcher as any).addEventListener('change', async () => {
|
const previewId = this.getPreviewId(preview.baseUrl);
|
||||||
const previews = this.previews.get();
|
|
||||||
|
|
||||||
for (const preview of previews) {
|
if (previewId) {
|
||||||
const previewId = this.getPreviewId(preview.baseUrl);
|
this.broadcastFileChange(previewId);
|
||||||
|
}
|
||||||
if (previewId) {
|
|
||||||
this.broadcastFileChange(previewId);
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
// Watch for DOM changes that might affect storage
|
// Watch for DOM changes that might affect storage
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
|
|||||||
Reference in New Issue
Block a user