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 { 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'; // Add this import import { streamingState } from '~/lib/stores/streaming'; import { NetlifyDeploymentLink } from '~/components/chat/NetlifyDeploymentLink.client'; interface HeaderActionButtonsProps {} export function HeaderActionButtons({}: HeaderActionButtonsProps) { const showWorkbench = useStore(workbenchStore.showWorkbench); const { showChat } = useStore(chatStore); const connection = useStore(netlifyConnection); const [activePreviewIndex] = useState(0); const previews = useStore(workbenchStore.previews); const activePreview = previews[activePreviewIndex]; const [isDeploying, setIsDeploying] = useState(false); const isSmallViewport = useViewport(1024); const canHideChat = showWorkbench || !showChat; const [isDropdownOpen, setIsDropdownOpen] = useState(false); const dropdownRef = useRef(null); const isStreaming = useStore(streamingState); useEffect(() => { function handleClickOutside(event: MouseEvent) { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { setIsDropdownOpen(false); } } document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const currentChatId = useStore(chatId); const handleDeploy = async () => { if (!connection.user || !connection.token) { toast.error('Please connect to Netlify first in the settings tab!'); return; } if (!currentChatId) { toast.error('No active chat found'); return; } 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/deploy', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ siteId: existingSiteId || undefined, files: fileContents, token: connection.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 ${connection.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'); } finally { setIsDeploying(false); } }; return (
{isDropdownOpen && (
)}
); } interface ButtonProps { active?: boolean; disabled?: boolean; children?: any; onClick?: VoidFunction; className?: string; } function Button({ active = false, disabled = false, children, onClick, className }: ButtonProps) { return ( ); }