mirror of
https://github.com/stackblitz-labs/bolt.diy.git
synced 2025-06-18 01:49:07 +01: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:
parent
8c70dd6123
commit
552f08acea
@ -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 (
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Connection Status Cards */}
|
||||
@ -303,6 +374,133 @@ export default function ConnectionDiagnostics() {
|
||||
</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>
|
||||
|
||||
{/* Action Buttons */}
|
||||
@ -323,22 +521,42 @@ export default function ConnectionDiagnostics() {
|
||||
|
||||
<Button
|
||||
onClick={resetGitHubConnection}
|
||||
disabled={isRunning}
|
||||
disabled={isRunning || !diagnosticResults?.localStorage.hasGithubConnection}
|
||||
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" />
|
||||
Reset GitHub Connection
|
||||
Reset GitHub
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={resetNetlifyConnection}
|
||||
disabled={isRunning}
|
||||
disabled={isRunning || !diagnosticResults?.localStorage.hasNetlifyConnection}
|
||||
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" />
|
||||
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>
|
||||
</div>
|
||||
|
||||
|
@ -149,48 +149,6 @@ export default function ConnectionsTab() {
|
||||
</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">
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<GitHubConnection />
|
||||
|
@ -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"
|
||||
>
|
||||
<div className="i-ph:key w-4 h-4" />
|
||||
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>
|
||||
<span className="mx-2">•</span>
|
||||
<span>
|
||||
@ -640,58 +639,48 @@ export default function GitHubConnection() {
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
{!connection.user ? (
|
||||
<Button
|
||||
<button
|
||||
onClick={handleConnect}
|
||||
disabled={isConnecting || !connection.token}
|
||||
variant="default"
|
||||
className="flex items-center gap-2"
|
||||
className={classNames(
|
||||
'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 ? (
|
||||
<>
|
||||
<div className="i-ph:spinner-gap animate-spin w-4 h-4" />
|
||||
<div className="i-ph:spinner-gap animate-spin" />
|
||||
Connecting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="i-ph:github-logo w-4 h-4" />
|
||||
<div className="i-ph:plug-charging w-4 h-4" />
|
||||
Connect
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
<button
|
||||
onClick={handleDisconnect}
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<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>
|
||||
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>
|
||||
>
|
||||
<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 className="flex items-center gap-2">
|
||||
<Button
|
||||
|
@ -659,92 +659,68 @@ export default function NetlifyConnection() {
|
||||
placeholder="Enter your Netlify API token"
|
||||
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',
|
||||
)}
|
||||
/>
|
||||
<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
|
||||
href="https://app.netlify.com/user/applications#personal-access-tokens"
|
||||
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"
|
||||
>
|
||||
<div className="i-ph:key w-4 h-4" />
|
||||
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>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-4">
|
||||
<Button
|
||||
<button
|
||||
onClick={handleConnect}
|
||||
disabled={isConnecting || !tokenInput}
|
||||
variant="default"
|
||||
className="flex items-center gap-2"
|
||||
className={classNames(
|
||||
'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 ? (
|
||||
<>
|
||||
<div className="i-ph:spinner-gap animate-spin w-4 h-4" />
|
||||
<div className="i-ph:spinner-gap animate-spin" />
|
||||
Connecting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CloudIcon className="w-4 h-4" />
|
||||
<div className="i-ph:plug-charging w-4 h-4" />
|
||||
Connect
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col w-full gap-4 mt-4">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Button onClick={handleDisconnect} variant="destructive" size="sm" className="flex items-center gap-2">
|
||||
<div className="i-ph:sign-out w-4 h-4" />
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
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
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircleIcon className="h-4 w-4 text-green-500" />
|
||||
<span className="text-sm text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary">
|
||||
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>
|
||||
</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 Netlify
|
||||
</span>
|
||||
</div>
|
||||
{renderStats()}
|
||||
</div>
|
||||
|
@ -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 ? (
|
||||
|
@ -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() {
|
||||
<div>
|
||||
<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">
|
||||
<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>
|
||||
<CardHeader>
|
||||
<div className="flex items-center mb-2">
|
||||
@ -756,23 +716,6 @@ export function DataTab() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
@ -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 (
|
||||
<>
|
||||
<Fragment key={normalized}>
|
||||
<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"
|
||||
onClick={(e) => {
|
||||
@ -89,7 +89,7 @@ export const AssistantMessage = memo(({ content, annotations }: AssistantMessage
|
||||
>
|
||||
{normalized}
|
||||
</code>
|
||||
</>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
@ -81,6 +81,7 @@ export function Chat() {
|
||||
position="bottom-right"
|
||||
pauseOnFocusLoss
|
||||
transition={toastAnimation}
|
||||
autoClose={3000}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -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 */}
|
||||
<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">
|
||||
<IconButton
|
||||
icon="i-ph:arrow-square-out"
|
||||
|
@ -59,7 +59,15 @@ export function useDataOperations({
|
||||
const showProgress = useCallback((message: string, percent: number) => {
|
||||
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<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
|
||||
const selectedChats = await Promise.all(
|
||||
chatIds.map(async (chatId) => {
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
try {
|
||||
const transaction = db.transaction(['chats'], 'readonly');
|
||||
const store = transaction.objectStore('chats');
|
||||
const request = store.get(chatId);
|
||||
// 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]);
|
||||
|
@ -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') {
|
||||
|
Loading…
x
Reference in New Issue
Block a user