diff --git a/app/components/header/DeployChat/DeployChatButton.tsx b/app/components/header/DeployChat/DeployChatButton.tsx index d91eefec..336f8977 100644 --- a/app/components/header/DeployChat/DeployChatButton.tsx +++ b/app/components/header/DeployChat/DeployChatButton.tsx @@ -6,7 +6,7 @@ import { generateRandomId } from '~/lib/replay/ReplayProtocolClient'; import { workbenchStore } from '~/lib/stores/workbench'; import { chatStore } from '~/lib/stores/chat'; import { database } from '~/lib/persistence/chats'; -import { deployRepository } from '~/lib/replay/Deploy'; +import { deployRepository, downloadRepository } from '~/lib/replay/Deploy'; import DeployChatModal from './components/DeployChatModal'; ReactModal.setAppElement('#root'); @@ -22,14 +22,61 @@ export function DeployChatButton() { const [deploySettings, setDeploySettings] = useState(null); const [error, setError] = useState(null); const [status, setStatus] = useState(DeployStatus.NotStarted); + const [databaseFound, setDatabaseFound] = useState(false); + + const handleCheckDatabase = async () => { + const repositoryId = workbenchStore.repositoryId.get(); + if (!repositoryId) { + toast.error('No repository ID found'); + return; + } + + try { + const repositoryContents = await downloadRepository(repositoryId); + + // Convert base64 to blob + const byteCharacters = atob(repositoryContents); + const byteNumbers = new Array(byteCharacters.length); + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i); + } + const byteArray = new Uint8Array(byteNumbers); + const blob = new Blob([byteArray], { type: 'application/zip' }); + + const reader = new FileReader(); + reader.onload = (event) => { + if (!event.target?.result) { + setIsModalOpen(false); + toast.error('Could not read repository contents'); + return; + } + + const zipContents = event.target.result as string; + const directoryToFind = 'supabase'; + + if (zipContents.includes(directoryToFind)) { + setDatabaseFound(true); + } else { + setDatabaseFound(false); + } + }; + + reader.readAsText(blob); + } catch (error) { + setIsModalOpen(false); + console.error('Error downloading repository:', error); + toast.error('Failed to download repository'); + } + }; const handleOpenModal = async () => { const chatId = chatStore.currentChat.get()?.id; if (!chatId) { - toast.error('No chat open'); + toast.error('No chat ID found'); return; } + await handleCheckDatabase(); const existingSettings = await database.getChatDeploySettings(chatId); setIsModalOpen(true); @@ -146,11 +193,10 @@ export function DeployChatButton() { <> ); diff --git a/app/components/header/DeployChat/components/DeployChatModal.tsx b/app/components/header/DeployChat/components/DeployChatModal.tsx index 05571a57..e2510b17 100644 --- a/app/components/header/DeployChat/components/DeployChatModal.tsx +++ b/app/components/header/DeployChat/components/DeployChatModal.tsx @@ -1,4 +1,5 @@ import { DeployStatus } from '~/components/header/DeployChat/DeployChatButton'; +import DeploymentSuccessful from './DeploymentSuccessful'; interface DeployChatModalProps { isModalOpen: boolean; @@ -8,6 +9,7 @@ interface DeployChatModalProps { setDeploySettings: (settings: any) => void; error: string | null; handleDeploy: () => void; + databaseFound: boolean; } const DeployChatModal = ({ @@ -18,234 +20,358 @@ const DeployChatModal = ({ setDeploySettings, error, handleDeploy, + databaseFound, }: DeployChatModalProps) => { + const handleOverlayClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + setIsModalOpen(false); + } + }; + return ( <> {isModalOpen && ( -
-
+
+
e.stopPropagation()} + > {status === DeployStatus.Succeeded ? ( - <> -
Deployment Succeeded
-
-
- - - - -
-
- + ) : ( <> -

Deploy

-
Deploy this chat's app to production.
+

+ Deploy Your Application +

+
+

+ Deploy your chat application to production using Netlify{databaseFound ? ' and Supabase' : ''}. +

+

This process will:

+
+
    +
  • Create a new Netlify site or update an existing one
  • + {databaseFound &&
  • Set up your database with Supabase
  • } +
  • Configure all necessary environment variables
  • +
  • Deploy your application with production settings
  • +
+
+
+ +
+

Before you begin:

+

+ You'll need accounts with both Netlify and {databaseFound ? 'Supabase ' : ''}to deploy your + application. If you haven't already, please sign up using the links below: +

+ +
{deploySettings?.siteURL && ( -
- Existing site: + )} -
- - { - const netlify = { - authToken: e.target.value, - siteId: deploySettings?.netlify?.siteId || '', - createInfo: deploySettings?.netlify?.createInfo || undefined, - }; - setDeploySettings({ - ...deploySettings, - netlify, - }); - }} - /> - - { - const netlify = { - authToken: deploySettings?.netlify?.authToken || '', - siteId: e.target.value, - createInfo: deploySettings?.netlify?.createInfo || undefined, - }; - setDeploySettings({ - ...deploySettings, - netlify, - }); - }} - /> - - { - const createInfo = { - accountSlug: e.target.value, - siteName: deploySettings?.netlify?.createInfo?.siteName || '', - }; - const netlify = { - authToken: deploySettings?.netlify?.authToken || '', - siteId: deploySettings?.netlify?.siteId || '', - createInfo, - }; - setDeploySettings({ - ...deploySettings, - netlify, - }); - }} - /> - - { - const createInfo = { - accountSlug: deploySettings?.netlify?.createInfo?.accountSlug || '', - siteName: e.target.value, - }; - const netlify = { - authToken: deploySettings?.netlify?.authToken || '', - siteId: deploySettings?.netlify?.siteId || '', - createInfo, - }; - setDeploySettings({ - ...deploySettings, - netlify, - }); - }} - /> - - { - const supabase = { - databaseURL: e.target.value, - anonKey: deploySettings?.supabase?.anonKey || '', - serviceRoleKey: deploySettings?.supabase?.serviceRoleKey || '', - postgresURL: deploySettings?.supabase?.postgresURL || '', - }; - setDeploySettings({ - ...deploySettings, - supabase, - }); - }} - /> - - { - const supabase = { - databaseURL: deploySettings?.supabase?.databaseURL || '', - anonKey: e.target.value, - serviceRoleKey: deploySettings?.supabase?.serviceRoleKey || '', - postgresURL: deploySettings?.supabase?.postgresURL || '', - }; - setDeploySettings({ - ...deploySettings, - supabase, - }); - }} - /> - - { - const supabase = { - databaseURL: deploySettings?.supabase?.databaseURL || '', - anonKey: deploySettings?.supabase?.anonKey || '', - serviceRoleKey: e.target.value, - postgresURL: deploySettings?.supabase?.postgresURL || '', - }; - setDeploySettings({ - ...deploySettings, - supabase, - }); - }} - /> - - { - const supabase = { - databaseURL: deploySettings?.supabase?.databaseURL || '', - anonKey: deploySettings?.supabase?.anonKey || '', - serviceRoleKey: deploySettings?.supabase?.serviceRoleKey || '', - postgresURL: e.target.value, - }; - setDeploySettings({ - ...deploySettings, - supabase, - }); - }} - /> -
- -
- {status === DeployStatus.Started && ( -
- +
+
+ +
+

+ Your authentication token from Netlify account settings. Used to authorize deployments. +

- )} + { + const netlify = { + authToken: e.target.value, + siteId: deploySettings?.netlify?.siteId || '', + createInfo: deploySettings?.netlify?.createInfo || undefined, + }; + setDeploySettings({ + ...deploySettings, + netlify, + }); + }} + /> +
- {status === DeployStatus.NotStarted && ( - +
+ +
+

+ The ID of your existing Netlify site if you want to update an existing deployment. +

+
+ { + const netlify = { + authToken: deploySettings?.netlify?.authToken || '', + siteId: e.target.value, + createInfo: deploySettings?.netlify?.createInfo || undefined, + }; + setDeploySettings({ + ...deploySettings, + netlify, + }); + }} + /> +
+ +
+ +
+

+ Your Netlify account name, required when creating a new site. +

+
+ { + const createInfo = { + accountSlug: e.target.value, + siteName: deploySettings?.netlify?.createInfo?.siteName || '', + }; + const netlify = { + authToken: deploySettings?.netlify?.authToken || '', + siteId: deploySettings?.netlify?.siteId || '', + createInfo, + }; + setDeploySettings({ + ...deploySettings, + netlify, + }); + }} + /> +
+ +
+ +
+

+ The desired name for your new Netlify site. Will be part of your site's URL. +

+
+ { + const createInfo = { + accountSlug: deploySettings?.netlify?.createInfo?.accountSlug || '', + siteName: e.target.value, + }; + const netlify = { + authToken: deploySettings?.netlify?.authToken || '', + siteId: deploySettings?.netlify?.siteId || '', + createInfo, + }; + setDeploySettings({ + ...deploySettings, + netlify, + }); + }} + /> +
+ + {databaseFound && ( + <> +
+ +
+

+ The URL of your Supabase project, used to connect to your database. +

+
+ { + const supabase = { + databaseURL: e.target.value, + anonKey: deploySettings?.supabase?.anonKey || '', + serviceRoleKey: deploySettings?.supabase?.serviceRoleKey || '', + postgresURL: deploySettings?.supabase?.postgresURL || '', + }; + setDeploySettings({ + ...deploySettings, + supabase, + }); + }} + /> +
+ +
+ +
+

+ Public API key for client-side database access with restricted permissions. +

+
+ { + const supabase = { + databaseURL: deploySettings?.supabase?.databaseURL || '', + anonKey: e.target.value, + serviceRoleKey: deploySettings?.supabase?.serviceRoleKey || '', + postgresURL: deploySettings?.supabase?.postgresURL || '', + }; + setDeploySettings({ + ...deploySettings, + supabase, + }); + }} + /> +
+ +
+ +
+

+ Admin API key for server-side operations with full database access. +

+
+ { + const supabase = { + databaseURL: deploySettings?.supabase?.databaseURL || '', + anonKey: deploySettings?.supabase?.anonKey || '', + serviceRoleKey: e.target.value, + postgresURL: deploySettings?.supabase?.postgresURL || '', + }; + setDeploySettings({ + ...deploySettings, + supabase, + }); + }} + /> +
+ +
+ +
+

+ Direct connection URL to your Postgres database for advanced operations. +

+
+ { + const supabase = { + databaseURL: deploySettings?.supabase?.databaseURL || '', + anonKey: deploySettings?.supabase?.anonKey || '', + serviceRoleKey: deploySettings?.supabase?.serviceRoleKey || '', + postgresURL: e.target.value, + }; + setDeploySettings({ + ...deploySettings, + supabase, + }); + }} + /> +
+ )} -
- {error &&
{error}
} +
+ {status === DeployStatus.Started ? ( +
+ + Deploying your application... +
+ ) : ( + <> + + + + )} +
+ + {error && ( +
+

Deployment Error

+

{error}

+
+ )} )}
diff --git a/app/components/header/DeployChat/components/DeploymentSuccessful.tsx b/app/components/header/DeployChat/components/DeploymentSuccessful.tsx new file mode 100644 index 00000000..d56d986a --- /dev/null +++ b/app/components/header/DeployChat/components/DeploymentSuccessful.tsx @@ -0,0 +1,33 @@ +const DeploymentSuccessful = ({ + deploySettings, + setIsModalOpen, +}: { + deploySettings: any; + setIsModalOpen: (isOpen: boolean) => void; +}) => { + return ( + <> +

Deployment Succeeded! 🎉

+
+

+ Your application has been successfully deployed. You can now access it at the URL below. +

+
+ + + + +
+
+ + ); +}; + +export default DeploymentSuccessful; diff --git a/app/components/header/DownloadButton.tsx b/app/components/header/DownloadButton.tsx index a2bc727a..22a23b3c 100644 --- a/app/components/header/DownloadButton.tsx +++ b/app/components/header/DownloadButton.tsx @@ -51,6 +51,7 @@ export function DownloadButton() { onClick={handleDownload} >
+ Download Repo ); diff --git a/app/components/header/Header.tsx b/app/components/header/Header.tsx index 1d004407..c0b33596 100644 --- a/app/components/header/Header.tsx +++ b/app/components/header/Header.tsx @@ -45,10 +45,10 @@ export function Header() { {chatStarted && ( <> - + {() => } - + {() => }