mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
Merge pull request #133 from replayio/PRO-1445-Codebase-organization
Organize header components and update feedback modal
This commit is contained in:
commit
472cc7f8c8
166
app/components/header/DeployChat/DeployChatButton.tsx
Normal file
166
app/components/header/DeployChat/DeployChatButton.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,159 +1,26 @@
|
|||||||
import { toast } from 'react-toastify';
|
import { DeployStatus } from '~/components/header/DeployChat/DeployChatButton';
|
||||||
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');
|
interface DeployChatModalProps {
|
||||||
|
isModalOpen: boolean;
|
||||||
// Component for deploying a chat to production.
|
setIsModalOpen: (isOpen: boolean) => void;
|
||||||
|
status: DeployStatus;
|
||||||
enum DeployStatus {
|
deploySettings: any;
|
||||||
NotStarted,
|
setDeploySettings: (settings: any) => void;
|
||||||
Started,
|
error: string | null;
|
||||||
Succeeded,
|
handleDeploy: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DeployChatButton() {
|
const DeployChatModal = ({
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
isModalOpen,
|
||||||
const [deploySettings, setDeploySettings] = useState<DeploySettingsDatabase | null>(null);
|
setIsModalOpen,
|
||||||
const [error, setError] = useState<string | null>(null);
|
status,
|
||||||
const [status, setStatus] = useState<DeployStatus>(DeployStatus.NotStarted);
|
deploySettings,
|
||||||
|
setDeploySettings,
|
||||||
const handleOpenModal = async () => {
|
error,
|
||||||
const chatId = chatStore.currentChat.get()?.id;
|
handleDeploy,
|
||||||
if (!chatId) {
|
}: DeployChatModalProps) => {
|
||||||
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 (
|
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 && (
|
{isModalOpen && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-40 flex items-center justify-center">
|
<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">
|
<div className="bg-white rounded-lg p-6 max-w-2xl w-full z-50">
|
||||||
@ -386,4 +253,6 @@ export function DeployChatButton() {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default DeployChatModal;
|
@ -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>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
85
app/components/header/Feedback/FeedbackButton.tsx
Normal file
85
app/components/header/Feedback/FeedbackButton.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
124
app/components/header/Feedback/components/FeedbackModal.tsx
Normal file
124
app/components/header/Feedback/components/FeedbackModal.tsx
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
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;
|
@ -4,10 +4,10 @@ import { chatStore } from '~/lib/stores/chat';
|
|||||||
import { classNames } from '~/utils/classNames';
|
import { classNames } from '~/utils/classNames';
|
||||||
import { HeaderActionButtons } from './HeaderActionButtons.client';
|
import { HeaderActionButtons } from './HeaderActionButtons.client';
|
||||||
import { ChatDescription } from '~/lib/persistence/ChatDescription.client';
|
import { ChatDescription } from '~/lib/persistence/ChatDescription.client';
|
||||||
import { Feedback } from './Feedback';
|
import { Feedback } from './Feedback/FeedbackButton';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
import { ClientAuth } from '~/components/auth/ClientAuth/ClientAuth';
|
import { ClientAuth } from '~/components/auth/ClientAuth/ClientAuth';
|
||||||
import { DeployChatButton } from './DeployChatButton';
|
import { DeployChatButton } from './DeployChat/DeployChatButton';
|
||||||
import { DownloadButton } from './DownloadButton';
|
import { DownloadButton } from './DownloadButton';
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
|
Loading…
Reference in New Issue
Block a user