From 64d3bc7d9f06fd37a83ae0e16dfe23f613ccd425 Mon Sep 17 00:00:00 2001 From: Strider Wilson Date: Tue, 3 Jun 2025 15:00:56 -0400 Subject: [PATCH] Organize header components and update feedback modal --- .../header/DeployChat/DeployChatButton.tsx | 166 ++++++++ .../DeployChat/components/DeployChatModal.tsx | 259 ++++++++++++ app/components/header/DeployChatButton.tsx | 389 ------------------ app/components/header/Feedback.tsx | 160 ------- .../header/Feedback/FeedbackButton.tsx | 85 ++++ .../Feedback/components/FeedbackModal.tsx | 116 ++++++ app/components/header/Header.tsx | 4 +- 7 files changed, 628 insertions(+), 551 deletions(-) create mode 100644 app/components/header/DeployChat/DeployChatButton.tsx create mode 100644 app/components/header/DeployChat/components/DeployChatModal.tsx delete mode 100644 app/components/header/DeployChatButton.tsx delete mode 100644 app/components/header/Feedback.tsx create mode 100644 app/components/header/Feedback/FeedbackButton.tsx create mode 100644 app/components/header/Feedback/components/FeedbackModal.tsx diff --git a/app/components/header/DeployChat/DeployChatButton.tsx b/app/components/header/DeployChat/DeployChatButton.tsx new file mode 100644 index 00000000..d91eefec --- /dev/null +++ b/app/components/header/DeployChat/DeployChatButton.tsx @@ -0,0 +1,166 @@ +import { toast } from 'react-toastify'; +import ReactModal from 'react-modal'; +import { useState } from 'react'; +import type { DeploySettingsDatabase } from '~/lib/replay/Deploy'; +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 DeployChatModal from './components/DeployChatModal'; + +ReactModal.setAppElement('#root'); + +export enum DeployStatus { + NotStarted, + Started, + Succeeded, +} + +export function DeployChatButton() { + const [isModalOpen, setIsModalOpen] = useState(false); + const [deploySettings, setDeploySettings] = useState(null); + const [error, setError] = useState(null); + const [status, setStatus] = useState(DeployStatus.NotStarted); + + const handleOpenModal = async () => { + const chatId = chatStore.currentChat.get()?.id; + if (!chatId) { + toast.error('No chat open'); + return; + } + + const existingSettings = await database.getChatDeploySettings(chatId); + + setIsModalOpen(true); + setStatus(DeployStatus.NotStarted); + + if (existingSettings) { + setDeploySettings(existingSettings); + } else { + setDeploySettings({}); + } + }; + + const handleDeploy = async () => { + setError(null); + + const chatId = chatStore.currentChat.get()?.id; + if (!chatId) { + setError('No chat open'); + return; + } + + if (!deploySettings?.netlify?.authToken) { + setError('Netlify Auth Token is required'); + return; + } + + if (deploySettings?.netlify?.siteId) { + if (deploySettings.netlify.createInfo) { + setError('Cannot specify both a Netlify Site ID and a Netlify Account Slug'); + return; + } + } else if (!deploySettings?.netlify?.createInfo) { + setError('Either a Netlify Site ID or a Netlify Account Slug is required'); + return; + } else { + // Add a default site name if one isn't provided. + if (!deploySettings.netlify.createInfo?.siteName) { + deploySettings.netlify.createInfo.siteName = `nut-app-${generateRandomId()}`; + } + } + + if ( + deploySettings?.supabase?.databaseURL || + deploySettings?.supabase?.anonKey || + deploySettings?.supabase?.serviceRoleKey || + deploySettings?.supabase?.postgresURL + ) { + if (!deploySettings.supabase.databaseURL) { + setError('Supabase Database URL is required'); + return; + } + if (!deploySettings.supabase.anonKey) { + setError('Supabase Anonymous Key is required'); + return; + } + if (!deploySettings.supabase.serviceRoleKey) { + setError('Supabase Service Role Key is required'); + return; + } + if (!deploySettings.supabase.postgresURL) { + setError('Supabase Postgres URL is required'); + return; + } + } + + const repositoryId = workbenchStore.repositoryId.get(); + if (!repositoryId) { + setError('No repository ID found'); + return; + } + + setStatus(DeployStatus.Started); + + // Write out to the database before we start trying to deploy. + await database.updateChatDeploySettings(chatId, deploySettings); + + console.log('DeploymentStarting', repositoryId, deploySettings); + + const result = await deployRepository(repositoryId, deploySettings); + + console.log('DeploymentResult', repositoryId, deploySettings, result); + + if (result.error) { + setStatus(DeployStatus.NotStarted); + setError(result.error); + return; + } + + let newSettings = deploySettings; + + // Update netlify settings so future deployments will reuse the site. + if (deploySettings?.netlify?.createInfo && result.netlifySiteId) { + newSettings = { + ...deploySettings, + netlify: { authToken: deploySettings.netlify.authToken, siteId: result.netlifySiteId }, + }; + } + + // Update database with the deployment result. + newSettings = { + ...newSettings, + siteURL: result.siteURL, + repositoryId, + }; + + setDeploySettings(newSettings); + setStatus(DeployStatus.Succeeded); + + // Update the database with the new settings. + await database.updateChatDeploySettings(chatId, newSettings); + }; + + return ( + <> + + + + ); +} diff --git a/app/components/header/DeployChat/components/DeployChatModal.tsx b/app/components/header/DeployChat/components/DeployChatModal.tsx new file mode 100644 index 00000000..ec603934 --- /dev/null +++ b/app/components/header/DeployChat/components/DeployChatModal.tsx @@ -0,0 +1,259 @@ +import { DeployStatus } from "../DeployChatButton"; + +interface DeployChatModalProps { + isModalOpen: boolean; + setIsModalOpen: (isOpen: boolean) => void; + status: DeployStatus; + deploySettings: any; + setDeploySettings: (settings: any) => void; + error: string | null; + handleDeploy: () => void; +} + +const DeployChatModal = ({ + isModalOpen, + setIsModalOpen, + status, + deploySettings, + setDeploySettings, + error, + handleDeploy, +}: DeployChatModalProps) => { + + return ( + <> + {isModalOpen && ( +
+
+ {status === DeployStatus.Succeeded ? ( + <> +
Deployment Succeeded
+
+
+ + + + +
+
+ + ) : ( + <> +

Deploy

+
Deploy this chat's app to production.
+ + {deploySettings?.siteURL && ( + + )} + +
+ + { + 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 && ( +
+ +
+ )} + + {status === DeployStatus.NotStarted && ( + + )} + +
+ + {error &&
{error}
} + + )} +
+
+ )} + + ); +}; + +export default DeployChatModal; \ No newline at end of file diff --git a/app/components/header/DeployChatButton.tsx b/app/components/header/DeployChatButton.tsx deleted file mode 100644 index 9dd8b7db..00000000 --- a/app/components/header/DeployChatButton.tsx +++ /dev/null @@ -1,389 +0,0 @@ -import { toast } from 'react-toastify'; -import ReactModal from 'react-modal'; -import { useState } from 'react'; -import type { DeploySettingsDatabase } from '~/lib/replay/Deploy'; -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'; - -ReactModal.setAppElement('#root'); - -// Component for deploying a chat to production. - -enum DeployStatus { - NotStarted, - Started, - Succeeded, -} - -export function DeployChatButton() { - const [isModalOpen, setIsModalOpen] = useState(false); - const [deploySettings, setDeploySettings] = useState(null); - const [error, setError] = useState(null); - const [status, setStatus] = useState(DeployStatus.NotStarted); - - const handleOpenModal = async () => { - const chatId = chatStore.currentChat.get()?.id; - if (!chatId) { - toast.error('No chat open'); - return; - } - - const existingSettings = await database.getChatDeploySettings(chatId); - - setIsModalOpen(true); - setStatus(DeployStatus.NotStarted); - - if (existingSettings) { - setDeploySettings(existingSettings); - } else { - setDeploySettings({}); - } - }; - - const handleDeploy = async () => { - setError(null); - - const chatId = chatStore.currentChat.get()?.id; - if (!chatId) { - setError('No chat open'); - return; - } - - if (!deploySettings?.netlify?.authToken) { - setError('Netlify Auth Token is required'); - return; - } - - if (deploySettings?.netlify?.siteId) { - if (deploySettings.netlify.createInfo) { - setError('Cannot specify both a Netlify Site ID and a Netlify Account Slug'); - return; - } - } else if (!deploySettings?.netlify?.createInfo) { - setError('Either a Netlify Site ID or a Netlify Account Slug is required'); - return; - } else { - // Add a default site name if one isn't provided. - if (!deploySettings.netlify.createInfo?.siteName) { - deploySettings.netlify.createInfo.siteName = `nut-app-${generateRandomId()}`; - } - } - - if ( - deploySettings?.supabase?.databaseURL || - deploySettings?.supabase?.anonKey || - deploySettings?.supabase?.serviceRoleKey || - deploySettings?.supabase?.postgresURL - ) { - if (!deploySettings.supabase.databaseURL) { - setError('Supabase Database URL is required'); - return; - } - if (!deploySettings.supabase.anonKey) { - setError('Supabase Anonymous Key is required'); - return; - } - if (!deploySettings.supabase.serviceRoleKey) { - setError('Supabase Service Role Key is required'); - return; - } - if (!deploySettings.supabase.postgresURL) { - setError('Supabase Postgres URL is required'); - return; - } - } - - const repositoryId = workbenchStore.repositoryId.get(); - if (!repositoryId) { - setError('No repository ID found'); - return; - } - - setStatus(DeployStatus.Started); - - // Write out to the database before we start trying to deploy. - await database.updateChatDeploySettings(chatId, deploySettings); - - console.log('DeploymentStarting', repositoryId, deploySettings); - - const result = await deployRepository(repositoryId, deploySettings); - - console.log('DeploymentResult', repositoryId, deploySettings, result); - - if (result.error) { - setStatus(DeployStatus.NotStarted); - setError(result.error); - return; - } - - let newSettings = deploySettings; - - // Update netlify settings so future deployments will reuse the site. - if (deploySettings?.netlify?.createInfo && result.netlifySiteId) { - newSettings = { - ...deploySettings, - netlify: { authToken: deploySettings.netlify.authToken, siteId: result.netlifySiteId }, - }; - } - - // Update database with the deployment result. - newSettings = { - ...newSettings, - siteURL: result.siteURL, - repositoryId, - }; - - setDeploySettings(newSettings); - setStatus(DeployStatus.Succeeded); - - // Update the database with the new settings. - await database.updateChatDeploySettings(chatId, newSettings); - }; - - return ( - <> - - - {isModalOpen && ( -
-
- {status === DeployStatus.Succeeded ? ( - <> -
Deployment Succeeded
-
-
- - - - -
-
- - ) : ( - <> -

Deploy

-
Deploy this chat's app to production.
- - {deploySettings?.siteURL && ( - - )} - -
- - { - 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 && ( -
- -
- )} - - {status === DeployStatus.NotStarted && ( - - )} - -
- - {error &&
{error}
} - - )} -
-
- )} - - ); -} diff --git a/app/components/header/Feedback.tsx b/app/components/header/Feedback.tsx deleted file mode 100644 index 29e0c674..00000000 --- a/app/components/header/Feedback.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { toast } from 'react-toastify'; -import ReactModal from 'react-modal'; -import { useState } from 'react'; -import { supabaseSubmitFeedback } from '~/lib/supabase/feedback'; -import { getLastChatMessages } from '~/utils/chat/messageUtils'; - -ReactModal.setAppElement('#root'); - -// Component for leaving feedback. - -export function Feedback() { - const [isModalOpen, setIsModalOpen] = useState(false); - const [formData, setFormData] = useState({ - description: '', - email: '', - share: false, - }); - const [submitted, setSubmitted] = useState(false); - - const handleOpenModal = () => { - setIsModalOpen(true); - setFormData({ - description: '', - email: '', - share: false, - }); - setSubmitted(false); - }; - - const handleSubmitFeedback = async () => { - if (!formData.description) { - toast.error('Please fill in the feedback field'); - - return; - } - - toast.info('Submitting feedback...'); - - const feedbackData: any = { - description: formData.description, - share: formData.share, - source: 'feedback_modal', - }; - - if (feedbackData.share) { - feedbackData.chatMessages = getLastChatMessages(); - } - - try { - const success = await supabaseSubmitFeedback(feedbackData); - - if (success) { - setSubmitted(true); - toast.success('Feedback submitted successfully!'); - } else { - toast.error('Failed to submit feedback'); - } - } catch (error) { - console.error('Error submitting feedback:', error); - toast.error('An error occurred while submitting feedback'); - } - }; - - return ( - <> - - - {isModalOpen && ( -
-
- {submitted ? ( - <> -
Feedback Submitted
-
-

Thank you for your feedback! We appreciate your input.

-
- -
-
- - ) : ( - <> -

Share Your Feedback

-
- Let us know how Nut is doing or report any issues you've encountered. -
- -
- -