Revert "fix: resolve chat conversation hanging and stream interruption issues (#1971)"

This reverts commit e68593f22d.
This commit is contained in:
Stijnus
2025-09-07 00:14:13 +02:00
committed by Stijnus
parent e68593f22d
commit 37217a5c7b
61 changed files with 1432 additions and 8811 deletions

View File

@@ -1,29 +1,277 @@
import { useState } from 'react';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { useStore } from '@nanostores/react';
import { netlifyConnection } from '~/lib/stores/netlify';
import { vercelConnection } from '~/lib/stores/vercel';
import { isGitLabConnected } from '~/lib/stores/gitlabConnection';
import { workbenchStore } from '~/lib/stores/workbench';
import { streamingState } from '~/lib/stores/streaming';
import { DeployDialog } from './DeployDialog';
import { classNames } from '~/utils/classNames';
import { useState } from 'react';
import { NetlifyDeploymentLink } from '~/components/chat/NetlifyDeploymentLink.client';
import { VercelDeploymentLink } from '~/components/chat/VercelDeploymentLink.client';
import { useVercelDeploy } from '~/components/deploy/VercelDeploy.client';
import { useNetlifyDeploy } from '~/components/deploy/NetlifyDeploy.client';
import { useGitHubDeploy } from '~/components/deploy/GitHubDeploy.client';
import { useGitLabDeploy } from '~/components/deploy/GitLabDeploy.client';
import { GitHubDeploymentDialog } from '~/components/deploy/GitHubDeploymentDialog';
import { GitLabDeploymentDialog } from '~/components/deploy/GitLabDeploymentDialog';
export const DeployButton = () => {
const [isDialogOpen, setIsDialogOpen] = useState(false);
interface DeployButtonProps {
onVercelDeploy?: () => Promise<void>;
onNetlifyDeploy?: () => Promise<void>;
onGitHubDeploy?: () => Promise<void>;
onGitLabDeploy?: () => Promise<void>;
}
export const DeployButton = ({
onVercelDeploy,
onNetlifyDeploy,
onGitHubDeploy,
onGitLabDeploy,
}: DeployButtonProps) => {
const netlifyConn = useStore(netlifyConnection);
const vercelConn = useStore(vercelConnection);
const gitlabIsConnected = useStore(isGitLabConnected);
const [activePreviewIndex] = useState(0);
const previews = useStore(workbenchStore.previews);
const activePreview = previews[activePreviewIndex];
const [isDeploying, setIsDeploying] = useState(false);
const [deployingTo, setDeployingTo] = useState<'netlify' | 'vercel' | 'github' | 'gitlab' | null>(null);
const isStreaming = useStore(streamingState);
const { handleVercelDeploy } = useVercelDeploy();
const { handleNetlifyDeploy } = useNetlifyDeploy();
const { handleGitHubDeploy } = useGitHubDeploy();
const { handleGitLabDeploy } = useGitLabDeploy();
const [showGitHubDeploymentDialog, setShowGitHubDeploymentDialog] = useState(false);
const [showGitLabDeploymentDialog, setShowGitLabDeploymentDialog] = useState(false);
const [githubDeploymentFiles, setGithubDeploymentFiles] = useState<Record<string, string> | null>(null);
const [gitlabDeploymentFiles, setGitlabDeploymentFiles] = useState<Record<string, string> | null>(null);
const [githubProjectName, setGithubProjectName] = useState('');
const [gitlabProjectName, setGitlabProjectName] = useState('');
const handleVercelDeployClick = async () => {
setIsDeploying(true);
setDeployingTo('vercel');
try {
if (onVercelDeploy) {
await onVercelDeploy();
} else {
await handleVercelDeploy();
}
} finally {
setIsDeploying(false);
setDeployingTo(null);
}
};
const handleNetlifyDeployClick = async () => {
setIsDeploying(true);
setDeployingTo('netlify');
try {
if (onNetlifyDeploy) {
await onNetlifyDeploy();
} else {
await handleNetlifyDeploy();
}
} finally {
setIsDeploying(false);
setDeployingTo(null);
}
};
const handleGitHubDeployClick = async () => {
setIsDeploying(true);
setDeployingTo('github');
try {
if (onGitHubDeploy) {
await onGitHubDeploy();
} else {
const result = await handleGitHubDeploy();
if (result && result.success && result.files) {
setGithubDeploymentFiles(result.files);
setGithubProjectName(result.projectName);
setShowGitHubDeploymentDialog(true);
}
}
} finally {
setIsDeploying(false);
setDeployingTo(null);
}
};
const handleGitLabDeployClick = async () => {
setIsDeploying(true);
setDeployingTo('gitlab');
try {
if (onGitLabDeploy) {
await onGitLabDeploy();
} else {
const result = await handleGitLabDeploy();
if (result && result.success && result.files) {
setGitlabDeploymentFiles(result.files);
setGitlabProjectName(result.projectName);
setShowGitLabDeploymentDialog(true);
}
}
} finally {
setIsDeploying(false);
setDeployingTo(null);
}
};
return (
<>
<button
onClick={() => setIsDialogOpen(true)}
disabled={!activePreview || isStreaming}
className="px-4 py-1.5 rounded-lg bg-accent-500 text-white hover:bg-accent-600 disabled:opacity-50 disabled:cursor-not-allowed transition-all flex items-center gap-2 text-sm font-medium"
title="Deploy your project"
>
<span className="i-ph:rocket-launch text-lg" />
Deploy
</button>
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden text-sm">
<DropdownMenu.Root>
<DropdownMenu.Trigger
disabled={isDeploying || !activePreview || isStreaming}
className="rounded-md items-center justify-center [&:is(:disabled,.disabled)]:cursor-not-allowed [&:is(:disabled,.disabled)]:opacity-60 px-3 py-1.5 text-xs bg-accent-500 text-white hover:text-bolt-elements-item-contentAccent [&:not(:disabled,.disabled)]:hover:bg-bolt-elements-button-primary-backgroundHover outline-accent-500 flex gap-1.7"
>
{isDeploying ? `Deploying to ${deployingTo}...` : 'Deploy'}
<span className={classNames('i-ph:caret-down transition-transform')} />
</DropdownMenu.Trigger>
<DropdownMenu.Content
className={classNames(
'z-[250]',
'bg-bolt-elements-background-depth-2',
'rounded-lg shadow-lg',
'border border-bolt-elements-borderColor',
'animate-in fade-in-0 zoom-in-95',
'py-1',
)}
sideOffset={5}
align="end"
>
<DropdownMenu.Item
className={classNames(
'cursor-pointer flex items-center w-full px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive gap-2 rounded-md group relative',
{
'opacity-60 cursor-not-allowed': isDeploying || !activePreview || !netlifyConn.user,
},
)}
disabled={isDeploying || !activePreview || !netlifyConn.user}
onClick={handleNetlifyDeployClick}
>
<img
className="w-5 h-5"
height="24"
width="24"
crossOrigin="anonymous"
src="https://cdn.simpleicons.org/netlify"
/>
<span className="mx-auto">
{!netlifyConn.user ? 'No Netlify Account Connected' : 'Deploy to Netlify'}
</span>
{netlifyConn.user && <NetlifyDeploymentLink />}
</DropdownMenu.Item>
<DeployDialog isOpen={isDialogOpen} onClose={() => setIsDialogOpen(false)} />
<DropdownMenu.Item
className={classNames(
'cursor-pointer flex items-center w-full px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive gap-2 rounded-md group relative',
{
'opacity-60 cursor-not-allowed': isDeploying || !activePreview || !vercelConn.user,
},
)}
disabled={isDeploying || !activePreview || !vercelConn.user}
onClick={handleVercelDeployClick}
>
<img
className="w-5 h-5 bg-black p-1 rounded"
height="24"
width="24"
crossOrigin="anonymous"
src="https://cdn.simpleicons.org/vercel/white"
alt="vercel"
/>
<span className="mx-auto">{!vercelConn.user ? 'No Vercel Account Connected' : 'Deploy to Vercel'}</span>
{vercelConn.user && <VercelDeploymentLink />}
</DropdownMenu.Item>
<DropdownMenu.Item
className={classNames(
'cursor-pointer flex items-center w-full px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive gap-2 rounded-md group relative',
{
'opacity-60 cursor-not-allowed': isDeploying || !activePreview,
},
)}
disabled={isDeploying || !activePreview}
onClick={handleGitHubDeployClick}
>
<img
className="w-5 h-5"
height="24"
width="24"
crossOrigin="anonymous"
src="https://cdn.simpleicons.org/github"
alt="github"
/>
<span className="mx-auto">Deploy to GitHub</span>
</DropdownMenu.Item>
<DropdownMenu.Item
className={classNames(
'cursor-pointer flex items-center w-full px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive gap-2 rounded-md group relative',
{
'opacity-60 cursor-not-allowed': isDeploying || !activePreview || !gitlabIsConnected,
},
)}
disabled={isDeploying || !activePreview || !gitlabIsConnected}
onClick={handleGitLabDeployClick}
>
<img
className="w-5 h-5"
height="24"
width="24"
crossOrigin="anonymous"
src="https://cdn.simpleicons.org/gitlab"
alt="gitlab"
/>
<span className="mx-auto">{!gitlabIsConnected ? 'No GitLab Account Connected' : 'Deploy to GitLab'}</span>
</DropdownMenu.Item>
<DropdownMenu.Item
disabled
className="flex items-center w-full rounded-md px-4 py-2 text-sm text-bolt-elements-textTertiary gap-2 opacity-60 cursor-not-allowed"
>
<img
className="w-5 h-5"
height="24"
width="24"
crossOrigin="anonymous"
src="https://cdn.simpleicons.org/cloudflare"
alt="cloudflare"
/>
<span className="mx-auto">Deploy to Cloudflare (Coming Soon)</span>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
{/* GitHub Deployment Dialog */}
{showGitHubDeploymentDialog && githubDeploymentFiles && (
<GitHubDeploymentDialog
isOpen={showGitHubDeploymentDialog}
onClose={() => setShowGitHubDeploymentDialog(false)}
projectName={githubProjectName}
files={githubDeploymentFiles}
/>
)}
{/* GitLab Deployment Dialog */}
{showGitLabDeploymentDialog && gitlabDeploymentFiles && (
<GitLabDeploymentDialog
isOpen={showGitLabDeploymentDialog}
onClose={() => setShowGitLabDeploymentDialog(false)}
projectName={gitlabProjectName}
files={gitlabDeploymentFiles}
/>
)}
</>
);
};

View File

@@ -1,466 +0,0 @@
import React, { useState } from 'react';
import * as RadixDialog from '@radix-ui/react-dialog';
import { Dialog, DialogTitle, DialogDescription } from '~/components/ui/Dialog';
import { useStore } from '@nanostores/react';
import { netlifyConnection, updateNetlifyConnection } from '~/lib/stores/netlify';
import { vercelConnection } from '~/lib/stores/vercel';
import { useNetlifyDeploy } from './NetlifyDeploy.client';
import { useVercelDeploy } from './VercelDeploy.client';
import { useGitHubDeploy } from './GitHubDeploy.client';
import { GitHubDeploymentDialog } from './GitHubDeploymentDialog';
import { toast } from 'react-toastify';
import { classNames } from '~/utils/classNames';
interface DeployDialogProps {
isOpen: boolean;
onClose: () => void;
}
interface DeployProvider {
id: 'netlify' | 'vercel' | 'github' | 'cloudflare';
name: string;
iconClass: string;
iconColor?: string;
connected: boolean;
comingSoon?: boolean;
description: string;
features: string[];
}
const NetlifyConnectForm: React.FC<{ onSuccess: () => void }> = ({ onSuccess }) => {
const [token, setToken] = useState('');
const [isConnecting, setIsConnecting] = useState(false);
const handleConnect = async () => {
if (!token.trim()) {
toast.error('Please enter your Netlify API token');
return;
}
setIsConnecting(true);
try {
// Validate token with Netlify API
const response = await fetch('https://api.netlify.com/api/v1/user', {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error('Invalid token or authentication failed');
}
const userData = (await response.json()) as any;
// Update the connection store
updateNetlifyConnection({
user: userData,
token,
});
toast.success(`Connected to Netlify as ${userData.email || userData.name || 'User'}`);
onSuccess();
} catch (error) {
console.error('Netlify connection error:', error);
toast.error('Failed to connect to Netlify. Please check your token.');
} finally {
setIsConnecting(false);
}
};
return (
<div className="space-y-4 max-h-[60vh] overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-bolt-elements-borderColor scrollbar-track-transparent">
<div>
<h3 className="text-lg font-semibold text-bolt-elements-textPrimary mb-2">Connect to Netlify</h3>
<p className="text-sm text-bolt-elements-textSecondary mb-4">
To deploy your project to Netlify, you need to connect your account using a Personal Access Token.
</p>
</div>
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-bolt-elements-textPrimary mb-1">Personal Access Token</label>
<input
type="password"
value={token}
onChange={(e) => setToken(e.target.value)}
placeholder="Enter your Netlify API token"
className={classNames(
'w-full px-3 py-2 rounded-lg text-sm',
'bg-bolt-elements-background-depth-1',
'border border-bolt-elements-borderColor',
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
'focus:outline-none focus:ring-2 focus:ring-accent-500 focus:border-transparent',
'disabled:opacity-50',
)}
disabled={isConnecting}
/>
</div>
<div className="flex items-center justify-between">
<a
href="https://app.netlify.com/user/applications#personal-access-tokens"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-accent-500 hover:text-accent-600 inline-flex items-center gap-1"
>
Get your token from Netlify
<span className="i-ph:arrow-square-out text-xs" />
</a>
</div>
<div className="bg-bolt-elements-background-depth-2 rounded-lg p-3 space-y-2">
<p className="text-xs text-bolt-elements-textSecondary font-medium">How to get your token:</p>
<ol className="text-xs text-bolt-elements-textSecondary space-y-1 list-decimal list-inside">
<li>Go to your Netlify account settings</li>
<li>Navigate to "Applications" "Personal access tokens"</li>
<li>Click "New access token"</li>
<li>Give it a descriptive name (e.g., "bolt.diy deployment")</li>
<li>Copy the token and paste it here</li>
</ol>
</div>
<div className="flex gap-3">
<button
onClick={handleConnect}
disabled={isConnecting || !token.trim()}
className={classNames(
'flex-1 px-4 py-2 rounded-lg font-medium transition-all',
'bg-accent-500 text-white',
'hover:bg-accent-600',
'disabled:opacity-50 disabled:cursor-not-allowed',
'flex items-center justify-center gap-2',
)}
>
{isConnecting ? (
<>
<span className="i-svg-spinners:3-dots-scale" />
Connecting...
</>
) : (
<>
<span className="i-ph:plug-charging" />
Connect Account
</>
)}
</button>
</div>
</div>
</div>
);
};
export const DeployDialog: React.FC<DeployDialogProps> = ({ isOpen, onClose }) => {
const netlifyConn = useStore(netlifyConnection);
const vercelConn = useStore(vercelConnection);
const [selectedProvider, setSelectedProvider] = useState<'netlify' | 'vercel' | 'github' | null>(null);
const [isDeploying, setIsDeploying] = useState(false);
const [showGitHubDialog, setShowGitHubDialog] = useState(false);
const [githubFiles, setGithubFiles] = useState<Record<string, string> | null>(null);
const [githubProjectName, setGithubProjectName] = useState('');
const { handleNetlifyDeploy } = useNetlifyDeploy();
const { handleVercelDeploy } = useVercelDeploy();
const { handleGitHubDeploy } = useGitHubDeploy();
const providers: DeployProvider[] = [
{
id: 'netlify',
name: 'Netlify',
iconClass: 'i-simple-icons:netlify',
iconColor: 'text-[#00C7B7]',
connected: !!netlifyConn.user,
description: 'Deploy your site with automatic SSL, global CDN, and continuous deployment',
features: [
'Automatic SSL certificates',
'Global CDN',
'Instant rollbacks',
'Deploy previews',
'Form handling',
'Serverless functions',
],
},
{
id: 'vercel',
name: 'Vercel',
iconClass: 'i-simple-icons:vercel',
connected: !!vercelConn.user,
description: 'Deploy with the platform built for frontend developers',
features: [
'Zero-config deployments',
'Edge Functions',
'Analytics',
'Web Vitals monitoring',
'Preview deployments',
'Automatic HTTPS',
],
},
{
id: 'github',
name: 'GitHub',
iconClass: 'i-simple-icons:github',
connected: true, // GitHub doesn't require separate auth
description: 'Deploy to GitHub Pages or create a repository',
features: [
'Free hosting with GitHub Pages',
'Version control integration',
'Collaborative development',
'Actions & Workflows',
'Issue tracking',
'Pull requests',
],
},
{
id: 'cloudflare',
name: 'Cloudflare Pages',
iconClass: 'i-simple-icons:cloudflare',
iconColor: 'text-[#F38020]',
connected: false,
comingSoon: true,
description: "Deploy on Cloudflare's global network",
features: [
'Unlimited bandwidth',
'DDoS protection',
'Web Analytics',
'Edge Workers',
'Custom domains',
'Automatic builds',
],
},
];
const handleDeploy = async (provider: 'netlify' | 'vercel' | 'github') => {
setIsDeploying(true);
try {
let success = false;
if (provider === 'netlify') {
success = await handleNetlifyDeploy();
} else if (provider === 'vercel') {
success = await handleVercelDeploy();
} else if (provider === 'github') {
const result = await handleGitHubDeploy();
if (result && typeof result === 'object' && result.success && result.files) {
setGithubFiles(result.files);
setGithubProjectName(result.projectName);
setShowGitHubDialog(true);
onClose();
return;
}
success = result && typeof result === 'object' ? result.success : false;
}
if (success) {
toast.success(
`Successfully deployed to ${provider === 'netlify' ? 'Netlify' : provider === 'vercel' ? 'Vercel' : 'GitHub'}`,
);
onClose();
}
} catch (error) {
console.error('Deployment error:', error);
toast.error(
`Failed to deploy to ${provider === 'netlify' ? 'Netlify' : provider === 'vercel' ? 'Vercel' : 'GitHub'}`,
);
} finally {
setIsDeploying(false);
}
};
const renderProviderContent = () => {
if (!selectedProvider) {
return (
<div className="grid gap-4">
{providers.map((provider) => (
<button
key={provider.id}
onClick={() =>
!provider.comingSoon && setSelectedProvider(provider.id as 'netlify' | 'vercel' | 'github')
}
disabled={provider.comingSoon}
className={classNames(
'p-4 rounded-lg border-2 transition-all text-left',
'hover:border-accent-500 hover:bg-bolt-elements-background-depth-2',
provider.comingSoon
? 'border-bolt-elements-borderColor opacity-50 cursor-not-allowed'
: 'border-bolt-elements-borderColor cursor-pointer',
)}
>
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-lg bg-bolt-elements-background-depth-1 flex items-center justify-center flex-shrink-0">
<span
className={classNames(
provider.iconClass,
provider.iconColor || 'text-bolt-elements-textPrimary',
'text-2xl',
)}
/>
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<h3 className="font-semibold text-bolt-elements-textPrimary">{provider.name}</h3>
{provider.connected && (
<span className="text-xs px-2 py-0.5 rounded-full bg-green-500/20 text-green-500">Connected</span>
)}
{provider.comingSoon && (
<span className="text-xs px-2 py-0.5 rounded-full bg-bolt-elements-background-depth-3 text-bolt-elements-textTertiary">
Coming Soon
</span>
)}
</div>
<p className="text-sm text-bolt-elements-textSecondary mb-2">{provider.description}</p>
<div className="flex flex-wrap gap-2">
{provider.features.slice(0, 3).map((feature, index) => (
<span
key={index}
className="text-xs px-2 py-1 rounded bg-bolt-elements-background-depth-1 text-bolt-elements-textTertiary"
>
{feature}
</span>
))}
{provider.features.length > 3 && (
<span className="text-xs px-2 py-1 text-bolt-elements-textTertiary">
+{provider.features.length - 3} more
</span>
)}
</div>
</div>
</div>
</button>
))}
</div>
);
}
const provider = providers.find((p) => p.id === selectedProvider);
if (!provider) {
return null;
}
// If provider is not connected, show connection form
if (!provider.connected) {
if (selectedProvider === 'netlify') {
return (
<NetlifyConnectForm
onSuccess={() => {
handleDeploy('netlify');
}}
/>
);
}
// Add Vercel connection form here if needed
return <div>Vercel connection form coming soon...</div>;
}
// If connected, show deployment confirmation
return (
<div className="space-y-4">
<div className="flex items-center gap-3 p-4 bg-bolt-elements-background-depth-2 rounded-lg">
<span
className={classNames(
provider.iconClass,
provider.iconColor || 'text-bolt-elements-textPrimary',
'text-3xl',
)}
/>
<div className="flex-1">
<h3 className="font-semibold text-bolt-elements-textPrimary">{provider.name}</h3>
<p className="text-sm text-bolt-elements-textSecondary">Ready to deploy to your {provider.name} account</p>
</div>
<span className="text-xs px-2 py-0.5 rounded-full bg-green-500/20 text-green-500">Connected</span>
</div>
<div className="bg-bolt-elements-background-depth-2 rounded-lg p-4 space-y-3">
<h4 className="text-sm font-medium text-bolt-elements-textPrimary">Deployment Features:</h4>
<ul className="space-y-2">
{provider.features.map((feature, index) => (
<li key={index} className="flex items-start gap-2 text-sm text-bolt-elements-textSecondary">
<span className="i-ph:check-circle text-green-500 mt-0.5" />
<span>{feature}</span>
</li>
))}
</ul>
</div>
<div className="flex gap-3">
<button
onClick={() => setSelectedProvider(null)}
className="px-4 py-2 rounded-lg border border-bolt-elements-borderColor text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-2"
>
Back
</button>
<button
onClick={() => handleDeploy(selectedProvider as 'netlify' | 'vercel' | 'github')}
disabled={isDeploying}
className={classNames(
'flex-1 px-4 py-2 rounded-lg font-medium transition-all',
'bg-accent-500 text-white',
'hover:bg-accent-600',
'disabled:opacity-50 disabled:cursor-not-allowed',
'flex items-center justify-center gap-2',
)}
>
{isDeploying ? (
<>
<span className="i-svg-spinners:3-dots-scale" />
Deploying...
</>
) : (
<>
<span className="i-ph:rocket-launch" />
Deploy Now
</>
)}
</button>
</div>
</div>
);
};
return (
<>
<RadixDialog.Root open={isOpen} onOpenChange={(open) => !open && onClose()}>
<Dialog className="max-w-2xl max-h-[90vh] flex flex-col">
<div className="p-6 flex flex-col max-h-[90vh]">
<div className="flex-shrink-0">
<DialogTitle className="text-xl font-bold mb-1">Deploy Your Project</DialogTitle>
<DialogDescription className="mb-6">
Choose a deployment platform to publish your project to the web
</DialogDescription>
</div>
<div className="flex-1 overflow-y-auto min-h-0 pr-2 -mr-2 scrollbar-thin scrollbar-thumb-bolt-elements-borderColor scrollbar-track-transparent hover:scrollbar-thumb-bolt-elements-textTertiary">
{renderProviderContent()}
</div>
{!selectedProvider && (
<div className="flex-shrink-0 mt-6 pt-6 border-t border-bolt-elements-borderColor">
<button
onClick={onClose}
className="w-full px-4 py-2 rounded-lg border border-bolt-elements-borderColor text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-2"
>
Cancel
</button>
</div>
)}
</div>
</Dialog>
</RadixDialog.Root>
{/* GitHub Deployment Dialog */}
{showGitHubDialog && githubFiles && (
<GitHubDeploymentDialog
isOpen={showGitHubDialog}
onClose={() => setShowGitHubDialog(false)}
projectName={githubProjectName}
files={githubFiles}
/>
)}
</>
);
};

View File

@@ -1,210 +0,0 @@
/**
* Enhanced Deploy Button with Quick Deploy Option
* Contributed by Keoma Wright
*
* This component provides both authenticated and quick deployment options
*/
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { useStore } from '@nanostores/react';
import { netlifyConnection } from '~/lib/stores/netlify';
import { vercelConnection } from '~/lib/stores/vercel';
import { workbenchStore } from '~/lib/stores/workbench';
import { streamingState } from '~/lib/stores/streaming';
import { classNames } from '~/utils/classNames';
import { useState } from 'react';
import { NetlifyDeploymentLink } from '~/components/chat/NetlifyDeploymentLink.client';
import { VercelDeploymentLink } from '~/components/chat/VercelDeploymentLink.client';
import { useVercelDeploy } from '~/components/deploy/VercelDeploy.client';
import { useNetlifyDeploy } from '~/components/deploy/NetlifyDeploy.client';
import { QuickNetlifyDeploy } from '~/components/deploy/QuickNetlifyDeploy.client';
import * as Dialog from '@radix-ui/react-dialog';
interface EnhancedDeployButtonProps {
onVercelDeploy?: () => Promise<void>;
onNetlifyDeploy?: () => Promise<void>;
}
export const EnhancedDeployButton = ({ onVercelDeploy, onNetlifyDeploy }: EnhancedDeployButtonProps) => {
const netlifyConn = useStore(netlifyConnection);
const vercelConn = useStore(vercelConnection);
const [activePreviewIndex] = useState(0);
const previews = useStore(workbenchStore.previews);
const activePreview = previews[activePreviewIndex];
const [isDeploying, setIsDeploying] = useState(false);
const [deployingTo, setDeployingTo] = useState<'netlify' | 'vercel' | 'quick' | null>(null);
const [showQuickDeploy, setShowQuickDeploy] = useState(false);
const isStreaming = useStore(streamingState);
const { handleVercelDeploy } = useVercelDeploy();
const { handleNetlifyDeploy } = useNetlifyDeploy();
const handleVercelDeployClick = async () => {
setIsDeploying(true);
setDeployingTo('vercel');
try {
if (onVercelDeploy) {
await onVercelDeploy();
} else {
await handleVercelDeploy();
}
} finally {
setIsDeploying(false);
setDeployingTo(null);
}
};
const handleNetlifyDeployClick = async () => {
setIsDeploying(true);
setDeployingTo('netlify');
try {
if (onNetlifyDeploy) {
await onNetlifyDeploy();
} else {
await handleNetlifyDeploy();
}
} finally {
setIsDeploying(false);
setDeployingTo(null);
}
};
return (
<>
<div className="flex border border-bolt-elements-borderColor rounded-md overflow-hidden text-sm">
<DropdownMenu.Root>
<DropdownMenu.Trigger
disabled={isDeploying || !activePreview || isStreaming}
className="rounded-md items-center justify-center [&:is(:disabled,.disabled)]:cursor-not-allowed [&:is(:disabled,.disabled)]:opacity-60 px-3 py-1.5 text-xs bg-accent-500 text-white hover:text-bolt-elements-item-contentAccent [&:not(:disabled,.disabled)]:hover:bg-bolt-elements-button-primary-backgroundHover outline-accent-500 flex gap-1.7"
>
{isDeploying ? `Deploying${deployingTo ? ` to ${deployingTo}` : ''}...` : 'Deploy'}
<span className={classNames('i-ph:caret-down transition-transform')} />
</DropdownMenu.Trigger>
<DropdownMenu.Content
className={classNames(
'z-[250]',
'bg-bolt-elements-background-depth-2',
'rounded-lg shadow-lg',
'border border-bolt-elements-borderColor',
'animate-in fade-in-0 zoom-in-95',
'py-1',
)}
sideOffset={5}
align="end"
>
{/* Quick Deploy Option - Always Available */}
<DropdownMenu.Item
className={classNames(
'cursor-pointer flex items-center w-full px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive gap-2 rounded-md group relative',
{
'opacity-60 cursor-not-allowed': isDeploying || !activePreview,
},
)}
disabled={isDeploying || !activePreview}
onClick={() => setShowQuickDeploy(true)}
>
<div className="relative">
<img
className="w-5 h-5"
height="24"
width="24"
crossOrigin="anonymous"
src="https://cdn.simpleicons.org/netlify"
alt="Netlify Quick Deploy"
/>
<span className="absolute -top-1 -right-1 bg-green-500 text-white text-[8px] px-1 rounded">NEW</span>
</div>
<span className="mx-auto font-medium">Quick Deploy to Netlify (No Login)</span>
</DropdownMenu.Item>
<DropdownMenu.Separator className="h-px bg-bolt-elements-borderColor my-1" />
{/* Authenticated Netlify Deploy */}
<DropdownMenu.Item
className={classNames(
'cursor-pointer flex items-center w-full px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive gap-2 rounded-md group relative',
{
'opacity-60 cursor-not-allowed': isDeploying || !activePreview || !netlifyConn.user,
},
)}
disabled={isDeploying || !activePreview || !netlifyConn.user}
onClick={handleNetlifyDeployClick}
>
<img
className="w-5 h-5"
height="24"
width="24"
crossOrigin="anonymous"
src="https://cdn.simpleicons.org/netlify"
/>
<span className="mx-auto">
{!netlifyConn.user ? 'No Netlify Account Connected' : 'Deploy to Netlify'}
</span>
{netlifyConn.user && <NetlifyDeploymentLink />}
</DropdownMenu.Item>
{/* Vercel Deploy */}
<DropdownMenu.Item
className={classNames(
'cursor-pointer flex items-center w-full px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive gap-2 rounded-md group relative',
{
'opacity-60 cursor-not-allowed': isDeploying || !activePreview || !vercelConn.user,
},
)}
disabled={isDeploying || !activePreview || !vercelConn.user}
onClick={handleVercelDeployClick}
>
<img
className="w-5 h-5 bg-black p-1 rounded"
height="24"
width="24"
crossOrigin="anonymous"
src="https://cdn.simpleicons.org/vercel/white"
alt="vercel"
/>
<span className="mx-auto">{!vercelConn.user ? 'No Vercel Account Connected' : 'Deploy to Vercel'}</span>
{vercelConn.user && <VercelDeploymentLink />}
</DropdownMenu.Item>
{/* Cloudflare - Coming Soon */}
<DropdownMenu.Item
disabled
className="flex items-center w-full rounded-md px-4 py-2 text-sm text-bolt-elements-textTertiary gap-2 opacity-60 cursor-not-allowed"
>
<img
className="w-5 h-5"
height="24"
width="24"
crossOrigin="anonymous"
src="https://cdn.simpleicons.org/cloudflare"
alt="cloudflare"
/>
<span className="mx-auto">Deploy to Cloudflare (Coming Soon)</span>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
{/* Quick Deploy Dialog */}
<Dialog.Root open={showQuickDeploy} onOpenChange={setShowQuickDeploy}>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50 z-[999] animate-in fade-in-0" />
<Dialog.Content className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-[1000] w-full max-w-2xl animate-in fade-in-0 zoom-in-95">
<div className="bg-bolt-elements-background rounded-lg shadow-xl border border-bolt-elements-borderColor">
<div className="flex items-center justify-between p-4 border-b border-bolt-elements-borderColor">
<h2 className="text-lg font-semibold text-bolt-elements-textPrimary">Quick Deploy to Netlify</h2>
<Dialog.Close className="p-1 rounded hover:bg-bolt-elements-item-backgroundActive transition-colors">
<span className="i-ph:x text-lg text-bolt-elements-textSecondary" />
</Dialog.Close>
</div>
<div className="p-4">
<QuickNetlifyDeploy />
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
</>
);
};

View File

@@ -1,362 +0,0 @@
/**
* Quick Netlify Deployment Component
* Contributed by Keoma Wright
*
* This component provides a streamlined one-click deployment to Netlify
* with automatic build detection and configuration.
*/
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useStore } from '@nanostores/react';
import { workbenchStore } from '~/lib/stores/workbench';
import { webcontainer } from '~/lib/webcontainer';
import { path } from '~/utils/path';
import { chatId } from '~/lib/persistence/useChatHistory';
import type { ActionCallbackData } from '~/lib/runtime/message-parser';
interface QuickDeployConfig {
framework?: 'react' | 'vue' | 'angular' | 'svelte' | 'next' | 'nuxt' | 'gatsby' | 'static';
buildCommand?: string;
outputDirectory?: string;
nodeVersion?: string;
}
export function QuickNetlifyDeploy() {
const [isDeploying, setIsDeploying] = useState(false);
const [deployUrl, setDeployUrl] = useState<string | null>(null);
const [showAdvanced, setShowAdvanced] = useState(false);
const currentChatId = useStore(chatId);
const detectFramework = async (): Promise<QuickDeployConfig> => {
try {
const container = await webcontainer;
// Read package.json to detect framework
let packageJson: any = {};
try {
const packageContent = await container.fs.readFile('/package.json', 'utf-8');
packageJson = JSON.parse(packageContent);
} catch {
console.log('No package.json found, assuming static site');
return {
framework: 'static',
buildCommand: '',
outputDirectory: '/',
nodeVersion: '18',
};
}
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
const scripts = packageJson.scripts || {};
// Detect framework based on dependencies
const config: QuickDeployConfig = {
nodeVersion: '18',
};
if (deps.next) {
config.framework = 'next';
config.buildCommand = scripts.build || 'npm run build';
config.outputDirectory = '.next';
} else if (deps.nuxt || deps.nuxt3) {
config.framework = 'nuxt';
config.buildCommand = scripts.build || 'npm run build';
config.outputDirectory = '.output/public';
} else if (deps.gatsby) {
config.framework = 'gatsby';
config.buildCommand = scripts.build || 'npm run build';
config.outputDirectory = 'public';
} else if (deps['@angular/core']) {
config.framework = 'angular';
config.buildCommand = scripts.build || 'npm run build';
config.outputDirectory = 'dist';
} else if (deps.vue) {
config.framework = 'vue';
config.buildCommand = scripts.build || 'npm run build';
config.outputDirectory = 'dist';
} else if (deps.svelte) {
config.framework = 'svelte';
config.buildCommand = scripts.build || 'npm run build';
config.outputDirectory = 'public';
} else if (deps.react) {
config.framework = 'react';
config.buildCommand = scripts.build || 'npm run build';
config.outputDirectory = 'build';
// Check for Vite
if (deps.vite) {
config.outputDirectory = 'dist';
}
} else {
config.framework = 'static';
config.buildCommand = scripts.build || '';
config.outputDirectory = '/';
}
return config;
} catch (error) {
console.error('Error detecting framework:', error);
return {
framework: 'static',
buildCommand: '',
outputDirectory: '/',
nodeVersion: '18',
};
}
};
const handleQuickDeploy = async (): Promise<string | null> => {
if (!currentChatId) {
toast.error('No active project found');
return null;
}
try {
setIsDeploying(true);
setDeployUrl(null);
const artifact = workbenchStore.firstArtifact;
if (!artifact) {
throw new Error('No active project found');
}
// Detect framework and configuration
const config = await detectFramework();
toast.info(`Detected ${config.framework || 'static'} project. Starting deployment...`);
// Create deployment artifact for visual feedback
const deploymentId = `quick-deploy-${Date.now()}`;
workbenchStore.addArtifact({
id: deploymentId,
messageId: deploymentId,
title: 'Quick Netlify Deployment',
type: 'standalone',
});
const deployArtifact = workbenchStore.artifacts.get()[deploymentId];
// Build the project if needed
if (config.buildCommand) {
deployArtifact.runner.handleDeployAction('building', 'running', { source: 'netlify' });
const actionId = 'build-' + Date.now();
const actionData: ActionCallbackData = {
messageId: 'quick-netlify-build',
artifactId: artifact.id,
actionId,
action: {
type: 'build' as const,
content: config.buildCommand,
},
};
artifact.runner.addAction(actionData);
await artifact.runner.runAction(actionData);
if (!artifact.runner.buildOutput) {
deployArtifact.runner.handleDeployAction('building', 'failed', {
error: 'Build failed. Check the terminal for details.',
source: 'netlify',
});
throw new Error('Build failed');
}
}
// Prepare deployment
deployArtifact.runner.handleDeployAction('deploying', 'running', { source: 'netlify' });
const container = await webcontainer;
// Determine the output directory
let outputPath = config.outputDirectory || '/';
if (artifact.runner.buildOutput && artifact.runner.buildOutput.path) {
outputPath = artifact.runner.buildOutput.path.replace('/home/project', '');
}
// Collect files for deployment
async function getAllFiles(dirPath: string): Promise<Record<string, string>> {
const files: Record<string, string> = {};
try {
const entries = await container.fs.readdir(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
// Skip node_modules and other build artifacts
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '.cache') {
continue;
}
if (entry.isFile()) {
try {
const content = await container.fs.readFile(fullPath, 'utf-8');
const deployPath = fullPath.replace(outputPath, '');
files[deployPath] = content;
} catch (e) {
console.warn(`Could not read file ${fullPath}:`, e);
}
} else if (entry.isDirectory()) {
const subFiles = await getAllFiles(fullPath);
Object.assign(files, subFiles);
}
}
} catch (e) {
console.error(`Error reading directory ${dirPath}:`, e);
}
return files;
}
const fileContents = await getAllFiles(outputPath);
// Create netlify.toml configuration
const netlifyConfig = `
[build]
publish = "${config.outputDirectory || '/'}"
${config.buildCommand ? `command = "${config.buildCommand}"` : ''}
[build.environment]
NODE_VERSION = "${config.nodeVersion}"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-XSS-Protection = "1; mode=block"
X-Content-Type-Options = "nosniff"
`;
fileContents['/netlify.toml'] = netlifyConfig;
// Deploy to Netlify using the quick deploy endpoint
const response = await fetch('/api/netlify-quick-deploy', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
files: fileContents,
chatId: currentChatId,
framework: config.framework,
}),
});
const data = (await response.json()) as { success: boolean; url?: string; siteId?: string; error?: string };
if (!response.ok || !data.success) {
deployArtifact.runner.handleDeployAction('deploying', 'failed', {
error: data.error || 'Deployment failed',
source: 'netlify',
});
throw new Error(data.error || 'Deployment failed');
}
// Deployment successful
setDeployUrl(data.url || null);
deployArtifact.runner.handleDeployAction('complete', 'complete', {
url: data.url || '',
source: 'netlify',
});
toast.success('Deployment successful! Your app is live.');
// Store deployment info
if (data.siteId) {
localStorage.setItem(`netlify-quick-site-${currentChatId}`, data.siteId);
}
return data.url || null;
} catch (error) {
console.error('Quick deploy error:', error);
toast.error(error instanceof Error ? error.message : 'Deployment failed');
return null;
} finally {
setIsDeploying(false);
}
};
return (
<div className="flex flex-col gap-4 p-4 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-background">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<img className="w-5 h-5" src="https://cdn.simpleicons.org/netlify" alt="Netlify" />
<h3 className="text-lg font-semibold text-bolt-elements-textPrimary">Quick Deploy to Netlify</h3>
</div>
{deployUrl && (
<a
href={deployUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-bolt-elements-link-text hover:text-bolt-elements-link-textHover underline"
>
View Live Site
</a>
)}
</div>
<p className="text-sm text-bolt-elements-textSecondary">
Deploy your project to Netlify instantly with automatic framework detection and configuration.
</p>
<button
onClick={handleQuickDeploy}
disabled={isDeploying}
className="px-6 py-3 rounded-lg bg-accent-500 text-white font-medium hover:bg-accent-600 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 flex items-center justify-center gap-2"
>
{isDeploying ? (
<>
<span className="i-ph:spinner-gap animate-spin w-5 h-5" />
Deploying...
</>
) : (
<>
<span className="i-ph:rocket-launch w-5 h-5" />
Deploy Now
</>
)}
</button>
{deployUrl && (
<div className="p-3 rounded-lg bg-green-500/10 border border-green-500/20">
<p className="text-sm text-green-600 dark:text-green-400">
Your app is live at:{' '}
<a href={deployUrl} target="_blank" rel="noopener noreferrer" className="underline font-medium">
{deployUrl}
</a>
</p>
</div>
)}
<button
onClick={() => setShowAdvanced(!showAdvanced)}
className="text-sm text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors flex items-center gap-1"
>
<span className={`i-ph:caret-right transform transition-transform ${showAdvanced ? 'rotate-90' : ''}`} />
Advanced Options
</button>
{showAdvanced && (
<div className="p-3 rounded-lg bg-bolt-elements-background-depth-2 text-sm text-bolt-elements-textSecondary space-y-2">
<p> Automatic framework detection (React, Vue, Next.js, etc.)</p>
<p> Smart build command configuration</p>
<p> Optimized output directory selection</p>
<p> SSL/HTTPS enabled by default</p>
<p> Global CDN distribution</p>
</div>
)}
</div>
);
}