diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx index 168fbed9..a33058c2 100644 --- a/app/components/chat/BaseChat.tsx +++ b/app/components/chat/BaseChat.tsx @@ -29,7 +29,8 @@ import type { ProviderInfo } from '~/types/model'; import { ScreenshotStateManager } from './ScreenshotStateManager'; import { toast } from 'react-toastify'; import StarterTemplates from './StarterTemplates'; -import type { ActionAlert, SupabaseAlert } from '~/types/actions'; +import type { ActionAlert, SupabaseAlert, DeployAlert } from '~/types/actions'; +import DeployChatAlert from '~/components/deploy/DeployAlert'; import ChatAlert from './ChatAlert'; import type { ModelInfo } from '~/lib/modules/llm/types'; import ProgressCompilation from './ProgressCompilation'; @@ -73,6 +74,8 @@ interface BaseChatProps { clearAlert?: () => void; supabaseAlert?: SupabaseAlert; clearSupabaseAlert?: () => void; + deployAlert?: DeployAlert; + clearDeployAlert?: () => void; data?: JSONValue[] | undefined; actionRunner?: ActionRunner; } @@ -109,6 +112,8 @@ export const BaseChat = React.forwardRef( messages, actionAlert, clearAlert, + deployAlert, + clearDeployAlert, supabaseAlert, clearSupabaseAlert, data, @@ -349,6 +354,16 @@ export const BaseChat = React.forwardRef( ) : null; }} + {deployAlert && ( + clearDeployAlert?.()} + postMessage={(message: string | undefined) => { + sendMessage?.({} as any, message); + clearSupabaseAlert?.(); + }} + /> + )} {supabaseAlert && ( project.id === supabaseConn.selectedProjectId, @@ -560,6 +561,8 @@ export const ChatImpl = memo( clearAlert={() => workbenchStore.clearAlert()} supabaseAlert={supabaseAlert} clearSupabaseAlert={() => workbenchStore.clearSupabaseAlert()} + deployAlert={deployAlert} + clearDeployAlert={() => workbenchStore.clearDeployAlert()} data={chatData} /> ); diff --git a/app/components/deploy/DeployAlert.tsx b/app/components/deploy/DeployAlert.tsx new file mode 100644 index 00000000..adedb77b --- /dev/null +++ b/app/components/deploy/DeployAlert.tsx @@ -0,0 +1,197 @@ +import { AnimatePresence, motion } from 'framer-motion'; +import { classNames } from '~/utils/classNames'; +import type { DeployAlert } from '~/types/actions'; + +interface DeployAlertProps { + alert: DeployAlert; + clearAlert: () => void; + postMessage: (message: string) => void; +} + +export default function DeployChatAlert({ alert, clearAlert, postMessage }: DeployAlertProps) { + const { type, title, description, content, url, stage, buildStatus, deployStatus } = alert; + + // Determine if we should show the deployment progress + const showProgress = stage && (buildStatus || deployStatus); + + return ( + + +
+ {/* Icon */} + +
+
+ {/* Content */} +
+ + {title} + + +

{description}

+ + {/* Deployment Progress Visualization */} + {showProgress && ( +
+
+ {/* Build Step */} +
+
+ {buildStatus === 'running' ? ( +
+ ) : buildStatus === 'complete' ? ( +
+ ) : buildStatus === 'failed' ? ( +
+ ) : ( + 1 + )} +
+ Build +
+ + {/* Connector Line */} +
+ + {/* Deploy Step */} +
+
+ {deployStatus === 'running' ? ( +
+ ) : deployStatus === 'complete' ? ( +
+ ) : deployStatus === 'failed' ? ( +
+ ) : ( + 2 + )} +
+ Deploy +
+
+
+ )} + + {content && ( +
+ {content} +
+ )} + {url && type === 'success' && ( + + )} +
+ + {/* Actions */} + +
+ {type === 'error' && ( + + )} + +
+
+
+
+
+
+ ); +} diff --git a/app/components/deploy/NetlifyDeploy.client.tsx b/app/components/deploy/NetlifyDeploy.client.tsx new file mode 100644 index 00000000..f88751cc --- /dev/null +++ b/app/components/deploy/NetlifyDeploy.client.tsx @@ -0,0 +1,257 @@ +import { toast } from 'react-toastify'; +import { useStore } from '@nanostores/react'; +import { netlifyConnection } from '~/lib/stores/netlify'; +import { workbenchStore } from '~/lib/stores/workbench'; +import { webcontainer } from '~/lib/webcontainer'; +import { path } from '~/utils/path'; +import { useState } from 'react'; +import type { ActionCallbackData } from '~/lib/runtime/message-parser'; +import { chatId } from '~/lib/persistence/useChatHistory'; + +export function useNetlifyDeploy() { + const [isDeploying, setIsDeploying] = useState(false); + const netlifyConn = useStore(netlifyConnection); + const currentChatId = useStore(chatId); + + const handleNetlifyDeploy = async () => { + if (!netlifyConn.user || !netlifyConn.token) { + toast.error('Please connect to Netlify first in the settings tab!'); + return false; + } + + if (!currentChatId) { + toast.error('No active chat found'); + return false; + } + + try { + setIsDeploying(true); + + const artifact = workbenchStore.firstArtifact; + + if (!artifact) { + throw new Error('No active project found'); + } + + // Create a deployment artifact for visual feedback + const deploymentId = `deploy-artifact`; + workbenchStore.addArtifact({ + id: deploymentId, + messageId: deploymentId, + title: 'Netlify Deployment', + type: 'standalone', + }); + + const deployArtifact = workbenchStore.artifacts.get()[deploymentId]; + + // Notify that build is starting + deployArtifact.runner.handleDeployAction('building', 'running', { source: 'netlify' }); + + // Set up build action + const actionId = 'build-' + Date.now(); + const actionData: ActionCallbackData = { + messageId: 'netlify build', + artifactId: artifact.id, + actionId, + action: { + type: 'build' as const, + content: 'npm run build', + }, + }; + + // Add the action first + artifact.runner.addAction(actionData); + + // Then run it + await artifact.runner.runAction(actionData); + + if (!artifact.runner.buildOutput) { + // Notify that build failed + deployArtifact.runner.handleDeployAction('building', 'failed', { + error: 'Build failed. Check the terminal for details.', + source: 'netlify', + }); + throw new Error('Build failed'); + } + + // Notify that build succeeded and deployment is starting + deployArtifact.runner.handleDeployAction('deploying', 'running', { source: 'netlify' }); + + // Get the build files + const container = await webcontainer; + + // Remove /home/project from buildPath if it exists + const buildPath = artifact.runner.buildOutput.path.replace('/home/project', ''); + + console.log('Original buildPath', buildPath); + + // Check if the build path exists + let finalBuildPath = buildPath; + + // List of common output directories to check if the specified build path doesn't exist + const commonOutputDirs = [buildPath, '/dist', '/build', '/out', '/output', '/.next', '/public']; + + // Verify the build path exists, or try to find an alternative + let buildPathExists = false; + + for (const dir of commonOutputDirs) { + try { + await container.fs.readdir(dir); + finalBuildPath = dir; + buildPathExists = true; + console.log(`Using build directory: ${finalBuildPath}`); + break; + } catch (error) { + // Directory doesn't exist, try the next one + console.log(`Directory ${dir} doesn't exist, trying next option. ${error}`); + continue; + } + } + + if (!buildPathExists) { + throw new Error('Could not find build output directory. Please check your build configuration.'); + } + + async function getAllFiles(dirPath: string): Promise> { + const files: Record = {}; + const entries = await container.fs.readdir(dirPath, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dirPath, entry.name); + + if (entry.isFile()) { + const content = await container.fs.readFile(fullPath, 'utf-8'); + + // Remove build path prefix from the path + const deployPath = fullPath.replace(finalBuildPath, ''); + files[deployPath] = content; + } else if (entry.isDirectory()) { + const subFiles = await getAllFiles(fullPath); + Object.assign(files, subFiles); + } + } + + return files; + } + + const fileContents = await getAllFiles(finalBuildPath); + + // Use chatId instead of artifact.id + const existingSiteId = localStorage.getItem(`netlify-site-${currentChatId}`); + + const response = await fetch('/api/netlify-deploy', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + siteId: existingSiteId || undefined, + files: fileContents, + token: netlifyConn.token, + chatId: currentChatId, + }), + }); + + const data = (await response.json()) as any; + + if (!response.ok || !data.deploy || !data.site) { + console.error('Invalid deploy response:', data); + + // Notify that deployment failed + deployArtifact.runner.handleDeployAction('deploying', 'failed', { + error: data.error || 'Invalid deployment response', + source: 'netlify', + }); + throw new Error(data.error || 'Invalid deployment response'); + } + + const maxAttempts = 20; // 2 minutes timeout + let attempts = 0; + let deploymentStatus; + + while (attempts < maxAttempts) { + try { + const statusResponse = await fetch( + `https://api.netlify.com/api/v1/sites/${data.site.id}/deploys/${data.deploy.id}`, + { + headers: { + Authorization: `Bearer ${netlifyConn.token}`, + }, + }, + ); + + deploymentStatus = (await statusResponse.json()) as any; + + if (deploymentStatus.state === 'ready' || deploymentStatus.state === 'uploaded') { + break; + } + + if (deploymentStatus.state === 'error') { + // Notify that deployment failed + deployArtifact.runner.handleDeployAction('deploying', 'failed', { + error: 'Deployment failed: ' + (deploymentStatus.error_message || 'Unknown error'), + source: 'netlify', + }); + throw new Error('Deployment failed: ' + (deploymentStatus.error_message || 'Unknown error')); + } + + attempts++; + await new Promise((resolve) => setTimeout(resolve, 1000)); + } catch (error) { + console.error('Status check error:', error); + attempts++; + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + } + + if (attempts >= maxAttempts) { + // Notify that deployment timed out + deployArtifact.runner.handleDeployAction('deploying', 'failed', { + error: 'Deployment timed out', + source: 'netlify', + }); + throw new Error('Deployment timed out'); + } + + // Store the site ID if it's a new site + if (data.site) { + localStorage.setItem(`netlify-site-${currentChatId}`, data.site.id); + } + + // Notify that deployment completed successfully + deployArtifact.runner.handleDeployAction('complete', 'complete', { + url: deploymentStatus.ssl_url || deploymentStatus.url, + source: 'netlify', + }); + + toast.success( +
+ Deployed successfully!{' '} + + View site + +
, + ); + + return true; + } catch (error) { + console.error('Deploy error:', error); + toast.error(error instanceof Error ? error.message : 'Deployment failed'); + + return false; + } finally { + setIsDeploying(false); + } + }; + + return { + isDeploying, + handleNetlifyDeploy, + isConnected: !!netlifyConn.user, + }; +} diff --git a/app/components/deploy/VercelDeploy.client.tsx b/app/components/deploy/VercelDeploy.client.tsx new file mode 100644 index 00000000..a6b53a42 --- /dev/null +++ b/app/components/deploy/VercelDeploy.client.tsx @@ -0,0 +1,202 @@ +import { toast } from 'react-toastify'; +import { useStore } from '@nanostores/react'; +import { vercelConnection } from '~/lib/stores/vercel'; +import { workbenchStore } from '~/lib/stores/workbench'; +import { webcontainer } from '~/lib/webcontainer'; +import { path } from '~/utils/path'; +import { useState } from 'react'; +import type { ActionCallbackData } from '~/lib/runtime/message-parser'; +import { chatId } from '~/lib/persistence/useChatHistory'; + +export function useVercelDeploy() { + const [isDeploying, setIsDeploying] = useState(false); + const vercelConn = useStore(vercelConnection); + const currentChatId = useStore(chatId); + + const handleVercelDeploy = async () => { + if (!vercelConn.user || !vercelConn.token) { + toast.error('Please connect to Vercel first in the settings tab!'); + return false; + } + + if (!currentChatId) { + toast.error('No active chat found'); + return false; + } + + try { + setIsDeploying(true); + + const artifact = workbenchStore.firstArtifact; + + if (!artifact) { + throw new Error('No active project found'); + } + + // Create a deployment artifact for visual feedback + const deploymentId = `deploy-vercel-project`; + workbenchStore.addArtifact({ + id: deploymentId, + messageId: deploymentId, + title: 'Vercel Deployment', + type: 'standalone', + }); + + const deployArtifact = workbenchStore.artifacts.get()[deploymentId]; + + // Notify that build is starting + deployArtifact.runner.handleDeployAction('building', 'running', { source: 'vercel' }); + + const actionId = 'build-' + Date.now(); + const actionData: ActionCallbackData = { + messageId: 'vercel build', + artifactId: artifact.id, + actionId, + action: { + type: 'build' as const, + content: 'npm run build', + }, + }; + + // Add the action first + artifact.runner.addAction(actionData); + + // Then run it + await artifact.runner.runAction(actionData); + + if (!artifact.runner.buildOutput) { + // Notify that build failed + deployArtifact.runner.handleDeployAction('building', 'failed', { + error: 'Build failed. Check the terminal for details.', + source: 'vercel', + }); + throw new Error('Build failed'); + } + + // Notify that build succeeded and deployment is starting + deployArtifact.runner.handleDeployAction('deploying', 'running', { source: 'vercel' }); + + // Get the build files + const container = await webcontainer; + + // Remove /home/project from buildPath if it exists + const buildPath = artifact.runner.buildOutput.path.replace('/home/project', ''); + + // Check if the build path exists + let finalBuildPath = buildPath; + + // List of common output directories to check if the specified build path doesn't exist + const commonOutputDirs = [buildPath, '/dist', '/build', '/out', '/output', '/.next', '/public']; + + // Verify the build path exists, or try to find an alternative + let buildPathExists = false; + + for (const dir of commonOutputDirs) { + try { + await container.fs.readdir(dir); + finalBuildPath = dir; + buildPathExists = true; + console.log(`Using build directory: ${finalBuildPath}`); + break; + } catch (error) { + console.log(`Directory ${dir} doesn't exist, trying next option. ${error}`); + + // Directory doesn't exist, try the next one + continue; + } + } + + if (!buildPathExists) { + throw new Error('Could not find build output directory. Please check your build configuration.'); + } + + // Get all files recursively + async function getAllFiles(dirPath: string): Promise> { + const files: Record = {}; + const entries = await container.fs.readdir(dirPath, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dirPath, entry.name); + + if (entry.isFile()) { + const content = await container.fs.readFile(fullPath, 'utf-8'); + + // Remove build path prefix from the path + const deployPath = fullPath.replace(finalBuildPath, ''); + files[deployPath] = content; + } else if (entry.isDirectory()) { + const subFiles = await getAllFiles(fullPath); + Object.assign(files, subFiles); + } + } + + return files; + } + + const fileContents = await getAllFiles(finalBuildPath); + + // Use chatId instead of artifact.id + const existingProjectId = localStorage.getItem(`vercel-project-${currentChatId}`); + + const response = await fetch('/api/vercel-deploy', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + projectId: existingProjectId || undefined, + files: fileContents, + token: vercelConn.token, + chatId: currentChatId, + }), + }); + + const data = (await response.json()) as any; + + if (!response.ok || !data.deploy || !data.project) { + console.error('Invalid deploy response:', data); + + // Notify that deployment failed + deployArtifact.runner.handleDeployAction('deploying', 'failed', { + error: data.error || 'Invalid deployment response', + source: 'vercel', + }); + throw new Error(data.error || 'Invalid deployment response'); + } + + if (data.project) { + localStorage.setItem(`vercel-project-${currentChatId}`, data.project.id); + } + + // Notify that deployment completed successfully + deployArtifact.runner.handleDeployAction('complete', 'complete', { + url: data.deploy.url, + source: 'vercel', + }); + + toast.success( +
+ Deployed successfully to Vercel!{' '} + + View site + +
, + ); + + return true; + } catch (err) { + console.error('Vercel deploy error:', err); + toast.error(err instanceof Error ? err.message : 'Vercel deployment failed'); + + return false; + } finally { + setIsDeploying(false); + } + }; + + return { + isDeploying, + handleVercelDeploy, + isConnected: !!vercelConn.user, + }; +} diff --git a/app/components/header/HeaderActionButtons.client.tsx b/app/components/header/HeaderActionButtons.client.tsx index 56945c9e..ff211f30 100644 --- a/app/components/header/HeaderActionButtons.client.tsx +++ b/app/components/header/HeaderActionButtons.client.tsx @@ -1,19 +1,16 @@ import { useStore } from '@nanostores/react'; -import { toast } from 'react-toastify'; import useViewport from '~/lib/hooks'; import { chatStore } from '~/lib/stores/chat'; import { netlifyConnection } from '~/lib/stores/netlify'; import { vercelConnection } from '~/lib/stores/vercel'; import { workbenchStore } from '~/lib/stores/workbench'; -import { webcontainer } from '~/lib/webcontainer'; import { classNames } from '~/utils/classNames'; -import { path } from '~/utils/path'; import { useEffect, useRef, useState } from 'react'; -import type { ActionCallbackData } from '~/lib/runtime/message-parser'; -import { chatId } from '~/lib/persistence/useChatHistory'; import { streamingState } from '~/lib/stores/streaming'; 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'; interface HeaderActionButtonsProps {} @@ -32,6 +29,8 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) { const [isDropdownOpen, setIsDropdownOpen] = useState(false); const dropdownRef = useRef(null); const isStreaming = useStore(streamingState); + const { handleVercelDeploy } = useVercelDeploy(); + const { handleNetlifyDeploy } = useNetlifyDeploy(); useEffect(() => { function handleClickOutside(event: MouseEvent) { @@ -44,282 +43,24 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) { return () => document.removeEventListener('mousedown', handleClickOutside); }, []); - const currentChatId = useStore(chatId); - - const handleNetlifyDeploy = async () => { - if (!netlifyConn.user || !netlifyConn.token) { - toast.error('Please connect to Netlify first in the settings tab!'); - return; - } - - if (!currentChatId) { - toast.error('No active chat found'); - return; - } + const onVercelDeploy = async () => { + setIsDeploying(true); + setDeployingTo('vercel'); try { - setIsDeploying(true); - - const artifact = workbenchStore.firstArtifact; - - if (!artifact) { - throw new Error('No active project found'); - } - - const actionId = 'build-' + Date.now(); - const actionData: ActionCallbackData = { - messageId: 'netlify build', - artifactId: artifact.id, - actionId, - action: { - type: 'build' as const, - content: 'npm run build', - }, - }; - - // Add the action first - artifact.runner.addAction(actionData); - - // Then run it - await artifact.runner.runAction(actionData); - - if (!artifact.runner.buildOutput) { - throw new Error('Build failed'); - } - - // Get the build files - const container = await webcontainer; - - // Remove /home/project from buildPath if it exists - const buildPath = artifact.runner.buildOutput.path.replace('/home/project', ''); - - // Get all files recursively - async function getAllFiles(dirPath: string): Promise> { - const files: Record = {}; - const entries = await container.fs.readdir(dirPath, { withFileTypes: true }); - - for (const entry of entries) { - const fullPath = path.join(dirPath, entry.name); - - if (entry.isFile()) { - const content = await container.fs.readFile(fullPath, 'utf-8'); - - // Remove /dist prefix from the path - const deployPath = fullPath.replace(buildPath, ''); - files[deployPath] = content; - } else if (entry.isDirectory()) { - const subFiles = await getAllFiles(fullPath); - Object.assign(files, subFiles); - } - } - - return files; - } - - const fileContents = await getAllFiles(buildPath); - - // Use chatId instead of artifact.id - const existingSiteId = localStorage.getItem(`netlify-site-${currentChatId}`); - - // Deploy using the API route with file contents - const response = await fetch('/api/netlify-deploy', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - siteId: existingSiteId || undefined, - files: fileContents, - token: netlifyConn.token, - chatId: currentChatId, // Use chatId instead of artifact.id - }), - }); - - const data = (await response.json()) as any; - - if (!response.ok || !data.deploy || !data.site) { - console.error('Invalid deploy response:', data); - throw new Error(data.error || 'Invalid deployment response'); - } - - // Poll for deployment status - const maxAttempts = 20; // 2 minutes timeout - let attempts = 0; - let deploymentStatus; - - while (attempts < maxAttempts) { - try { - const statusResponse = await fetch( - `https://api.netlify.com/api/v1/sites/${data.site.id}/deploys/${data.deploy.id}`, - { - headers: { - Authorization: `Bearer ${netlifyConn.token}`, - }, - }, - ); - - deploymentStatus = (await statusResponse.json()) as any; - - if (deploymentStatus.state === 'ready' || deploymentStatus.state === 'uploaded') { - break; - } - - if (deploymentStatus.state === 'error') { - throw new Error('Deployment failed: ' + (deploymentStatus.error_message || 'Unknown error')); - } - - attempts++; - await new Promise((resolve) => setTimeout(resolve, 1000)); - } catch (error) { - console.error('Status check error:', error); - attempts++; - await new Promise((resolve) => setTimeout(resolve, 2000)); - } - } - - if (attempts >= maxAttempts) { - throw new Error('Deployment timed out'); - } - - // Store the site ID if it's a new site - if (data.site) { - localStorage.setItem(`netlify-site-${currentChatId}`, data.site.id); - } - - toast.success( -
- Deployed successfully!{' '} - - View site - -
, - ); - } catch (error) { - console.error('Deploy error:', error); - toast.error(error instanceof Error ? error.message : 'Deployment failed'); + await handleVercelDeploy(); } finally { setIsDeploying(false); + setDeployingTo(null); } }; - const handleVercelDeploy = async () => { - if (!vercelConn.user || !vercelConn.token) { - toast.error('Please connect to Vercel first in the settings tab!'); - return; - } - - if (!currentChatId) { - toast.error('No active chat found'); - return; - } + const onNetlifyDeploy = async () => { + setIsDeploying(true); + setDeployingTo('netlify'); try { - setIsDeploying(true); - setDeployingTo('vercel'); - - const artifact = workbenchStore.firstArtifact; - - if (!artifact) { - throw new Error('No active project found'); - } - - const actionId = 'build-' + Date.now(); - const actionData: ActionCallbackData = { - messageId: 'vercel build', - artifactId: artifact.id, - actionId, - action: { - type: 'build' as const, - content: 'npm run build', - }, - }; - - // Add the action first - artifact.runner.addAction(actionData); - - // Then run it - await artifact.runner.runAction(actionData); - - if (!artifact.runner.buildOutput) { - throw new Error('Build failed'); - } - - // Get the build files - const container = await webcontainer; - - // Remove /home/project from buildPath if it exists - const buildPath = artifact.runner.buildOutput.path.replace('/home/project', ''); - - // Get all files recursively - async function getAllFiles(dirPath: string): Promise> { - const files: Record = {}; - const entries = await container.fs.readdir(dirPath, { withFileTypes: true }); - - for (const entry of entries) { - const fullPath = path.join(dirPath, entry.name); - - if (entry.isFile()) { - const content = await container.fs.readFile(fullPath, 'utf-8'); - - // Remove /dist prefix from the path - const deployPath = fullPath.replace(buildPath, ''); - files[deployPath] = content; - } else if (entry.isDirectory()) { - const subFiles = await getAllFiles(fullPath); - Object.assign(files, subFiles); - } - } - - return files; - } - - const fileContents = await getAllFiles(buildPath); - - // Use chatId instead of artifact.id - const existingProjectId = localStorage.getItem(`vercel-project-${currentChatId}`); - - // Deploy using the API route with file contents - const response = await fetch('/api/vercel-deploy', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - projectId: existingProjectId || undefined, - files: fileContents, - token: vercelConn.token, - chatId: currentChatId, - }), - }); - - const data = (await response.json()) as any; - - if (!response.ok || !data.deploy || !data.project) { - console.error('Invalid deploy response:', data); - throw new Error(data.error || 'Invalid deployment response'); - } - - // Store the project ID if it's a new project - if (data.project) { - localStorage.setItem(`vercel-project-${currentChatId}`, data.project.id); - } - - toast.success( -
- Deployed successfully to Vercel!{' '} - - View site - -
, - ); - } catch (error) { - console.error('Vercel deploy error:', error); - toast.error(error instanceof Error ? error.message : 'Vercel deployment failed'); + await handleNetlifyDeploy(); } finally { setIsDeploying(false); setDeployingTo(null); @@ -348,7 +89,7 @@ export function HeaderActionButtons({}: HeaderActionButtonsProps) {