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:
Stijnus 2025-04-08 13:06:43 +02:00 committed by GitHub
parent 8c70dd6123
commit 552f08acea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 757 additions and 363 deletions

View File

@ -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>

View File

@ -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 />

View File

@ -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

View File

@ -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>

View File

@ -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 ? (

View File

@ -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>
);
}

View File

@ -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>

View File

@ -81,6 +81,7 @@ export function Chat() {
position="bottom-right"
pauseOnFocusLoss
transition={toastAnimation}
autoClose={3000}
/>
</>
);

View File

@ -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"

View File

@ -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]);

View File

@ -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') {