Revert "fix: resolve chat conversation hanging and stream interruption issues (#1971)"
This reverts commit e68593f22d.
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user