Organize header components and update feedback modal

This commit is contained in:
Strider Wilson 2025-06-03 15:00:56 -04:00
parent 9003754ccb
commit 64d3bc7d9f
7 changed files with 628 additions and 551 deletions

View File

@ -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<DeploySettingsDatabase | null>(null);
const [error, setError] = useState<string | null>(null);
const [status, setStatus] = useState<DeployStatus>(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 (
<>
<button
className="flex gap-2 bg-bolt-elements-sidebar-buttonBackgroundDefault text-bolt-elements-sidebar-buttonText hover:bg-bolt-elements-sidebar-buttonBackgroundHover rounded-md p-2 transition-theme"
onClick={() => {
handleOpenModal();
}}
>
Deploy
</button>
<DeployChatModal
isModalOpen={isModalOpen}
setIsModalOpen={setIsModalOpen}
status={status}
deploySettings={deploySettings}
setDeploySettings={setDeploySettings}
error={error}
handleDeploy={handleDeploy}
/>
</>
);
}

View File

@ -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 && (
<div className="fixed inset-0 bg-black bg-opacity-50 z-40 flex items-center justify-center">
<div className="bg-white rounded-lg p-6 max-w-2xl w-full z-50">
{status === DeployStatus.Succeeded ? (
<>
<div className="text-center mb-2">Deployment Succeeded</div>
<div className="text-center">
<div className="flex justify-center gap-2 mt-4">
<a href={deploySettings?.siteURL} target="_blank" rel="noopener noreferrer">
<button className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition-colors">
{deploySettings?.siteURL}
</button>
</a>
<button
onClick={() => {
setIsModalOpen(false);
}}
className="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400"
>
Close
</button>
</div>
</div>
</>
) : (
<>
<h2 className="text-xl font-semibold text-center mb-4">Deploy</h2>
<div className="text-center mb-4">Deploy this chat's app to production.</div>
{deploySettings?.siteURL && (
<div className="text-center mb-4">
<span className="text-lg text-gray-700 pr-2">Existing site:</span>
<a href={deploySettings?.siteURL} target="_blank" rel="noopener noreferrer">
<button className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition-colors">
{deploySettings?.siteURL}
</button>
</a>
</div>
)}
<div className="grid grid-cols-2 gap-2 mb-4 items-center">
<label className="text-sm font-lg text-gray-700 text-right">Netlify Auth Token:</label>
<input
name="netlifyAuthToken"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 py-2 border border-gray-300"
value={deploySettings?.netlify?.authToken}
placeholder="nfp_..."
onChange={(e) => {
const netlify = {
authToken: e.target.value,
siteId: deploySettings?.netlify?.siteId || '',
createInfo: deploySettings?.netlify?.createInfo || undefined,
};
setDeploySettings({
...deploySettings,
netlify,
});
}}
/>
<label className="text-sm font-lg text-gray-700 text-right">Netlify Site ID (existing site):</label>
<input
name="netlifySiteId"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 py-2 border border-gray-300"
value={deploySettings?.netlify?.siteId}
placeholder="123e4567-..."
onChange={(e) => {
const netlify = {
authToken: deploySettings?.netlify?.authToken || '',
siteId: e.target.value,
createInfo: deploySettings?.netlify?.createInfo || undefined,
};
setDeploySettings({
...deploySettings,
netlify,
});
}}
/>
<label className="text-sm font-lg text-gray-700 text-right">Netlify Account Slug (new site):</label>
<input
name="netlifyAccountSlug"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 py-2 border border-gray-300"
value={deploySettings?.netlify?.createInfo?.accountSlug}
placeholder="abc..."
onChange={(e) => {
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,
});
}}
/>
<label className="text-sm font-lg text-gray-700 text-right">Netlify Site Name (new site):</label>
<input
name="netlifySiteName"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 py-2 border border-gray-300"
value={deploySettings?.netlify?.createInfo?.siteName}
placeholder="my-chat-app..."
onChange={(e) => {
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,
});
}}
/>
<label className="text-sm font-lg text-gray-700 text-right">Supabase Database URL:</label>
<input
name="supabaseDatabaseURL"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 py-2 border border-gray-300"
value={deploySettings?.supabase?.databaseURL}
placeholder="https://abc...def.supabase.co"
onChange={(e) => {
const supabase = {
databaseURL: e.target.value,
anonKey: deploySettings?.supabase?.anonKey || '',
serviceRoleKey: deploySettings?.supabase?.serviceRoleKey || '',
postgresURL: deploySettings?.supabase?.postgresURL || '',
};
setDeploySettings({
...deploySettings,
supabase,
});
}}
/>
<label className="text-sm font-lg text-gray-700 text-right">Supabase Anonymous Key:</label>
<input
name="supabaseAnonKey"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 py-2 border border-gray-300"
value={deploySettings?.supabase?.anonKey}
placeholder="ey..."
onChange={(e) => {
const supabase = {
databaseURL: deploySettings?.supabase?.databaseURL || '',
anonKey: e.target.value,
serviceRoleKey: deploySettings?.supabase?.serviceRoleKey || '',
postgresURL: deploySettings?.supabase?.postgresURL || '',
};
setDeploySettings({
...deploySettings,
supabase,
});
}}
/>
<label className="text-sm font-lg text-gray-700 text-right">Supabase Service Role Key:</label>
<input
name="supabaseServiceRoleKey"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 py-2 border border-gray-300"
value={deploySettings?.supabase?.serviceRoleKey}
placeholder="ey..."
onChange={(e) => {
const supabase = {
databaseURL: deploySettings?.supabase?.databaseURL || '',
anonKey: deploySettings?.supabase?.anonKey || '',
serviceRoleKey: e.target.value,
postgresURL: deploySettings?.supabase?.postgresURL || '',
};
setDeploySettings({
...deploySettings,
supabase,
});
}}
/>
<label className="text-sm font-lg text-gray-700 text-right">Supabase Postgres URL:</label>
<input
name="supabasePostgresURL"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 py-2 border border-gray-300"
value={deploySettings?.supabase?.postgresURL}
placeholder="postgresql://postgres:<password>@db.abc...def.supabase.co:5432/postgres"
onChange={(e) => {
const supabase = {
databaseURL: deploySettings?.supabase?.databaseURL || '',
anonKey: deploySettings?.supabase?.anonKey || '',
serviceRoleKey: deploySettings?.supabase?.serviceRoleKey || '',
postgresURL: e.target.value,
};
setDeploySettings({
...deploySettings,
supabase,
});
}}
/>
</div>
<div className="flex justify-center gap-2 mt-4">
{status === DeployStatus.Started && (
<div className="w-full text-bolt-elements-textSecondary flex items-center">
<span className="i-svg-spinners:3-dots-fade inline-block w-[1em] h-[1em] mr-2 text-4xl"></span>
</div>
)}
{status === DeployStatus.NotStarted && (
<button
onClick={handleDeploy}
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition-colors"
>
Deploy
</button>
)}
<button
onClick={() => {
setIsModalOpen(false);
}}
className="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400 transition-colors"
>
Cancel
</button>
</div>
{error && <div className="mt-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">{error}</div>}
</>
)}
</div>
</div>
)}
</>
);
};
export default DeployChatModal;

View File

@ -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<DeploySettingsDatabase | null>(null);
const [error, setError] = useState<string | null>(null);
const [status, setStatus] = useState<DeployStatus>(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 (
<>
<button
className="flex gap-2 bg-bolt-elements-sidebar-buttonBackgroundDefault text-bolt-elements-sidebar-buttonText hover:bg-bolt-elements-sidebar-buttonBackgroundHover rounded-md p-2 transition-theme"
onClick={() => {
handleOpenModal();
}}
>
Deploy
</button>
{isModalOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 z-40 flex items-center justify-center">
<div className="bg-white rounded-lg p-6 max-w-2xl w-full z-50">
{status === DeployStatus.Succeeded ? (
<>
<div className="text-center mb-2">Deployment Succeeded</div>
<div className="text-center">
<div className="flex justify-center gap-2 mt-4">
<a href={deploySettings?.siteURL} target="_blank" rel="noopener noreferrer">
<button className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition-colors">
{deploySettings?.siteURL}
</button>
</a>
<button
onClick={() => {
setIsModalOpen(false);
}}
className="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400"
>
Close
</button>
</div>
</div>
</>
) : (
<>
<h2 className="text-xl font-semibold text-center mb-4">Deploy</h2>
<div className="text-center mb-4">Deploy this chat's app to production.</div>
{deploySettings?.siteURL && (
<div className="text-center mb-4">
<span className="text-lg text-gray-700 pr-2">Existing site:</span>
<a href={deploySettings?.siteURL} target="_blank" rel="noopener noreferrer">
<button className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition-colors">
{deploySettings?.siteURL}
</button>
</a>
</div>
)}
<div className="grid grid-cols-2 gap-2 mb-4 items-center">
<label className="text-sm font-lg text-gray-700 text-right">Netlify Auth Token:</label>
<input
name="netlifyAuthToken"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 py-2 border border-gray-300"
value={deploySettings?.netlify?.authToken}
placeholder="nfp_..."
onChange={(e) => {
const netlify = {
authToken: e.target.value,
siteId: deploySettings?.netlify?.siteId || '',
createInfo: deploySettings?.netlify?.createInfo || undefined,
};
setDeploySettings({
...deploySettings,
netlify,
});
}}
/>
<label className="text-sm font-lg text-gray-700 text-right">Netlify Site ID (existing site):</label>
<input
name="netlifySiteId"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 py-2 border border-gray-300"
value={deploySettings?.netlify?.siteId}
placeholder="123e4567-..."
onChange={(e) => {
const netlify = {
authToken: deploySettings?.netlify?.authToken || '',
siteId: e.target.value,
createInfo: deploySettings?.netlify?.createInfo || undefined,
};
setDeploySettings({
...deploySettings,
netlify,
});
}}
/>
<label className="text-sm font-lg text-gray-700 text-right">Netlify Account Slug (new site):</label>
<input
name="netlifyAccountSlug"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 py-2 border border-gray-300"
value={deploySettings?.netlify?.createInfo?.accountSlug}
placeholder="abc..."
onChange={(e) => {
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,
});
}}
/>
<label className="text-sm font-lg text-gray-700 text-right">Netlify Site Name (new site):</label>
<input
name="netlifySiteName"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 py-2 border border-gray-300"
value={deploySettings?.netlify?.createInfo?.siteName}
placeholder="my-chat-app..."
onChange={(e) => {
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,
});
}}
/>
<label className="text-sm font-lg text-gray-700 text-right">Supabase Database URL:</label>
<input
name="supabaseDatabaseURL"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 py-2 border border-gray-300"
value={deploySettings?.supabase?.databaseURL}
placeholder="https://abc...def.supabase.co"
onChange={(e) => {
const supabase = {
databaseURL: e.target.value,
anonKey: deploySettings?.supabase?.anonKey || '',
serviceRoleKey: deploySettings?.supabase?.serviceRoleKey || '',
postgresURL: deploySettings?.supabase?.postgresURL || '',
};
setDeploySettings({
...deploySettings,
supabase,
});
}}
/>
<label className="text-sm font-lg text-gray-700 text-right">Supabase Anonymous Key:</label>
<input
name="supabaseAnonKey"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 py-2 border border-gray-300"
value={deploySettings?.supabase?.anonKey}
placeholder="ey..."
onChange={(e) => {
const supabase = {
databaseURL: deploySettings?.supabase?.databaseURL || '',
anonKey: e.target.value,
serviceRoleKey: deploySettings?.supabase?.serviceRoleKey || '',
postgresURL: deploySettings?.supabase?.postgresURL || '',
};
setDeploySettings({
...deploySettings,
supabase,
});
}}
/>
<label className="text-sm font-lg text-gray-700 text-right">Supabase Service Role Key:</label>
<input
name="supabaseServiceRoleKey"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 py-2 border border-gray-300"
value={deploySettings?.supabase?.serviceRoleKey}
placeholder="ey..."
onChange={(e) => {
const supabase = {
databaseURL: deploySettings?.supabase?.databaseURL || '',
anonKey: deploySettings?.supabase?.anonKey || '',
serviceRoleKey: e.target.value,
postgresURL: deploySettings?.supabase?.postgresURL || '',
};
setDeploySettings({
...deploySettings,
supabase,
});
}}
/>
<label className="text-sm font-lg text-gray-700 text-right">Supabase Postgres URL:</label>
<input
name="supabasePostgresURL"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 py-2 border border-gray-300"
value={deploySettings?.supabase?.postgresURL}
placeholder="postgresql://postgres:<password>@db.abc...def.supabase.co:5432/postgres"
onChange={(e) => {
const supabase = {
databaseURL: deploySettings?.supabase?.databaseURL || '',
anonKey: deploySettings?.supabase?.anonKey || '',
serviceRoleKey: deploySettings?.supabase?.serviceRoleKey || '',
postgresURL: e.target.value,
};
setDeploySettings({
...deploySettings,
supabase,
});
}}
/>
</div>
<div className="flex justify-center gap-2 mt-4">
{status === DeployStatus.Started && (
<div className="w-full text-bolt-elements-textSecondary flex items-center">
<span className="i-svg-spinners:3-dots-fade inline-block w-[1em] h-[1em] mr-2 text-4xl"></span>
</div>
)}
{status === DeployStatus.NotStarted && (
<button
onClick={handleDeploy}
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition-colors"
>
Deploy
</button>
)}
<button
onClick={() => {
setIsModalOpen(false);
}}
className="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400 transition-colors"
>
Cancel
</button>
</div>
{error && <div className="mt-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">{error}</div>}
</>
)}
</div>
</div>
)}
</>
);
}

View File

@ -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<boolean>(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 (
<>
<button
className="flex gap-2 bg-bolt-elements-sidebar-buttonBackgroundDefault text-bolt-elements-sidebar-buttonText hover:bg-bolt-elements-sidebar-buttonBackgroundHover rounded-md p-2 transition-theme"
onClick={() => {
handleOpenModal();
}}
>
Feedback
</button>
{isModalOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 z-40 flex items-center justify-center">
<div className="bg-white rounded-lg p-6 max-w-2xl w-full z-50">
{submitted ? (
<>
<div className="text-center mb-2">Feedback Submitted</div>
<div className="text-center">
<p className="text-gray-600 mb-4">Thank you for your feedback! We appreciate your input.</p>
<div className="flex justify-center gap-2 mt-4">
<button
onClick={() => {
setIsModalOpen(false);
}}
className="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400"
>
Close
</button>
</div>
</div>
</>
) : (
<>
<h2 className="text-xl font-semibold text-center mb-4">Share Your Feedback</h2>
<div className="text-center mb-4">
Let us know how Nut is doing or report any issues you've encountered.
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">Your Feedback:</label>
<textarea
name="description"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 py-2 w-full border border-gray-300 min-h-[120px]"
value={formData.description}
placeholder="Tell us what you think or describe any issues..."
onChange={(e) => {
setFormData((prev) => ({
...prev,
description: e.target.value,
}));
}}
/>
</div>
<div className="flex items-center gap-2 mb-6">
<input
type="checkbox"
id="share-project"
name="share"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded border border-gray-300"
checked={formData.share}
onChange={(e) => {
setFormData((prev) => ({
...prev,
share: e.target.checked,
}));
}}
/>
<label htmlFor="share-project" className="text-sm text-gray-700">
Share project with the Nut team (helps us diagnose issues)
</label>
</div>
<div className="flex justify-center gap-2 mt-4">
<button
onClick={handleSubmitFeedback}
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition-colors"
>
Submit Feedback
</button>
<button
onClick={() => {
setIsModalOpen(false);
}}
className="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400 transition-colors"
>
Cancel
</button>
</div>
</>
)}
</div>
</div>
)}
</>
);
}

View File

@ -0,0 +1,85 @@
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';
import FeedbackModal from './components/FeedbackModal';
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<boolean>(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 (
<>
<button
className="flex gap-2 bg-bolt-elements-sidebar-buttonBackgroundDefault text-bolt-elements-sidebar-buttonText hover:bg-bolt-elements-sidebar-buttonBackgroundHover rounded-md p-2 transition-theme"
onClick={() => {
handleOpenModal();
}}
>
Feedback
</button>
<FeedbackModal
isModalOpen={isModalOpen}
setIsModalOpen={setIsModalOpen}
submitted={submitted}
formData={formData}
setFormData={setFormData}
handleSubmitFeedback={handleSubmitFeedback}
/>
</>
);
}

View File

@ -0,0 +1,116 @@
interface FeedbackModalProps {
isModalOpen: boolean;
setIsModalOpen: (isOpen: boolean) => void;
submitted: boolean;
formData: any;
setFormData: (formData: any) => void;
handleSubmitFeedback: () => void;
};
const FeedbackModal = ({
isModalOpen,
setIsModalOpen,
submitted,
formData,
setFormData,
handleSubmitFeedback,
}: FeedbackModalProps) => {
const handleOverlayClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
setIsModalOpen(false);
}
};
return (
<>
{isModalOpen && (
<div
className="fixed inset-0 bg-black bg-opacity-50 z-40 flex items-center justify-center"
onClick={handleOverlayClick}
>
<div className="bg-bolt-elements-background-depth-1 rounded-lg p-8 max-w-2xl w-full z-50 border border-bolt-elements-borderColor">
{submitted ? (
<>
<h2 className="text-2xl font-bold mb-6 text-bolt-elements-textPrimary text-center">Feedback Submitted</h2>
<div className="text-center">
<p className="text-bolt-elements-textSecondary mb-6">Thank you for your feedback! We appreciate your input.</p>
<div className="flex justify-center gap-2">
<button
onClick={() => {
setIsModalOpen(false);
}}
className="px-4 py-3 bg-gray-300 rounded-lg hover:bg-gray-400 transition-colors font-medium"
>
Close
</button>
</div>
</div>
</>
) : (
<>
<h2 className="text-2xl font-bold mb-6 text-bolt-elements-textPrimary text-center">Share Your Feedback</h2>
<div className="text-center mb-6 text-bolt-elements-textSecondary">
Let us know how Nut is doing or report any issues you've encountered.
</div>
<div className="mb-6">
<label className="block mb-2 text-sm font-medium text-bolt-elements-textPrimary">Your Feedback:</label>
<textarea
name="description"
className="w-full p-3 border rounded-lg bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary border-bolt-elements-borderColor focus:ring-2 focus:ring-green-500 focus:border-transparent min-h-[120px]"
value={formData.description}
placeholder="Tell us what you think or describe any issues..."
onChange={(e) => {
setFormData((prev: any) => ({
...prev,
description: e.target.value,
}));
}}
/>
</div>
<div className="flex items-center gap-2 mb-6">
<input
type="checkbox"
id="share-project"
name="share"
className="bg-bolt-elements-background-depth-2 text-green-500 rounded border-bolt-elements-borderColor focus:ring-2 focus:ring-green-500"
checked={formData.share}
onChange={(e) => {
setFormData((prev: any) => ({
...prev,
share: e.target.checked,
}));
}}
/>
<label htmlFor="share-project" className="text-sm text-bolt-elements-textSecondary">
Share project with the Nut team (helps us diagnose issues)
</label>
</div>
<div className="flex justify-center gap-3">
<button
onClick={handleSubmitFeedback}
className="px-4 py-3 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors font-medium"
>
Submit Feedback
</button>
<button
onClick={() => {
setIsModalOpen(false);
}}
className="px-4 py-3 bg-gray-300 rounded-lg hover:bg-gray-400 transition-colors font-medium"
>
Cancel
</button>
</div>
</>
)}
</div>
</div>
)}
</>
);
};
export default FeedbackModal;

View File

@ -4,10 +4,10 @@ import { chatStore } from '~/lib/stores/chat';
import { classNames } from '~/utils/classNames';
import { HeaderActionButtons } from './HeaderActionButtons.client';
import { ChatDescription } from '~/lib/persistence/ChatDescription.client';
import { Feedback } from './Feedback';
import { Feedback } from './Feedback/FeedbackButton';
import { Suspense } from 'react';
import { ClientAuth } from '~/components/auth/ClientAuth/ClientAuth';
import { DeployChatButton } from './DeployChatButton';
import { DeployChatButton } from './DeployChat/DeployChatButton';
import { DownloadButton } from './DownloadButton';
export function Header() {