mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
Use backend API for problem storage again (#89)
This commit is contained in:
parent
69da8cf043
commit
ccfee95851
@ -26,13 +26,13 @@ import { getIFrameSimulationData } from '~/lib/replay/Recording';
|
||||
import { getCurrentIFrame } from '~/components/workbench/Preview';
|
||||
import { getCurrentMouseData } from '~/components/workbench/PointSelector';
|
||||
import { anthropicNumFreeUsesCookieName, maxFreeUses } from '~/utils/freeUses';
|
||||
import { submitFeedback } from '~/lib/replay/Problems';
|
||||
import { ChatMessageTelemetry, pingTelemetry } from '~/lib/hooks/pingTelemetry';
|
||||
import type { RejectChangeData } from './ApproveChange';
|
||||
import { assert, generateRandomId } from '~/lib/replay/ReplayProtocolClient';
|
||||
import { getMessagesRepositoryId, getPreviousRepositoryId, type Message } from '~/lib/persistence/message';
|
||||
import { useAuthStatus } from '~/lib/stores/auth';
|
||||
import { debounce } from '~/utils/debounce';
|
||||
import { supabaseSubmitFeedback } from '~/lib/supabase/problems';
|
||||
|
||||
const toastAnimation = cssTransition({
|
||||
enter: 'animated fadeInRight',
|
||||
@ -461,7 +461,7 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory, importChat
|
||||
chatMessages: messages,
|
||||
};
|
||||
|
||||
shareProjectSuccess = await submitFeedback(feedbackData);
|
||||
shareProjectSuccess = await supabaseSubmitFeedback(feedbackData);
|
||||
}
|
||||
|
||||
pingTelemetry('RejectChange', {
|
||||
|
||||
@ -72,7 +72,11 @@ export function DeployChatButton() {
|
||||
}
|
||||
}
|
||||
|
||||
if (deploySettings?.supabase?.databaseURL || deploySettings?.supabase?.anonKey || deploySettings?.supabase?.postgresURL) {
|
||||
if (
|
||||
deploySettings?.supabase?.databaseURL ||
|
||||
deploySettings?.supabase?.anonKey ||
|
||||
deploySettings?.supabase?.postgresURL
|
||||
) {
|
||||
if (!deploySettings.supabase.databaseURL) {
|
||||
setError('Supabase Database URL is required');
|
||||
return;
|
||||
@ -172,9 +176,7 @@ export function DeployChatButton() {
|
||||
) : (
|
||||
<>
|
||||
<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>
|
||||
<div className="text-center mb-4">Deploy this chat's app to production.</div>
|
||||
|
||||
{deploySettings?.siteURL && (
|
||||
<div className="text-center mb-4">
|
||||
@ -349,11 +351,7 @@ export function DeployChatButton() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mt-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{error && <div className="mt-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">{error}</div>}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { toast } from 'react-toastify';
|
||||
import ReactModal from 'react-modal';
|
||||
import { useState } from 'react';
|
||||
import { submitFeedback } from '~/lib/replay/Problems';
|
||||
import { supabaseSubmitFeedback } from '~/lib/supabase/problems';
|
||||
import { getLastChatMessages } from '~/components/chat/Chat.client';
|
||||
|
||||
ReactModal.setAppElement('#root');
|
||||
@ -47,7 +47,7 @@ export function Feedback() {
|
||||
}
|
||||
|
||||
try {
|
||||
const success = await submitFeedback(feedbackData);
|
||||
const success = await supabaseSubmitFeedback(feedbackData);
|
||||
|
||||
if (success) {
|
||||
setSubmitted(true);
|
||||
|
||||
@ -12,8 +12,6 @@ import { logger } from '~/utils/logger';
|
||||
import { HistoryItem } from './HistoryItem';
|
||||
import { binDates } from './date-binning';
|
||||
import { useSearchFilter } from '~/lib/hooks/useSearchFilter';
|
||||
import { SaveProblem } from './SaveProblem';
|
||||
import { useAdminStatus } from '~/lib/stores/user';
|
||||
|
||||
const menuVariants = {
|
||||
closed: {
|
||||
@ -44,7 +42,6 @@ export const Menu = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [dialogContent, setDialogContent] = useState<DialogContent>(null);
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
const { isAdmin } = useAdminStatus();
|
||||
|
||||
const { filteredItems: filteredList, handleSearchChange } = useSearchFilter({
|
||||
items: list,
|
||||
@ -128,7 +125,6 @@ export const Menu = () => {
|
||||
>
|
||||
Problems
|
||||
</a>
|
||||
{isAdmin && <SaveProblem />}
|
||||
<a
|
||||
href="/about"
|
||||
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"
|
||||
|
||||
@ -1,254 +0,0 @@
|
||||
import { toast } from 'react-toastify';
|
||||
import ReactModal from 'react-modal';
|
||||
import { useState } from 'react';
|
||||
import { workbenchStore } from '~/lib/stores/workbench';
|
||||
import { submitProblem, NutProblemStatus } from '~/lib/replay/Problems';
|
||||
import type { NutProblemInput, NutProblemSolution } from '~/lib/replay/Problems';
|
||||
import { getCurrentUser } from '~/lib/supabase/client';
|
||||
import { authModalStore } from '~/lib/stores/authModal';
|
||||
import { authStatusStore } from '~/lib/stores/auth';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import {
|
||||
getLastUserSimulationData,
|
||||
getLastSimulationChatMessages,
|
||||
isSimulatingOrHasFinished,
|
||||
getLastSimulationChatReferences,
|
||||
} from '~/lib/replay/SimulationPrompt';
|
||||
|
||||
ReactModal.setAppElement('#root');
|
||||
|
||||
// External functions for problem storage
|
||||
|
||||
async function saveProblem(
|
||||
title: string,
|
||||
description: string,
|
||||
username: string,
|
||||
reproData: any,
|
||||
): Promise<string | null> {
|
||||
if (!title) {
|
||||
toast.error('Please fill in title field');
|
||||
return null;
|
||||
}
|
||||
|
||||
toast.info('Submitting problem...');
|
||||
|
||||
const repositoryId = workbenchStore.repositoryId.get();
|
||||
|
||||
if (!repositoryId) {
|
||||
toast.error('No repository ID found');
|
||||
return null;
|
||||
}
|
||||
|
||||
const solution: NutProblemSolution = {
|
||||
evaluator: undefined,
|
||||
...reproData,
|
||||
};
|
||||
|
||||
const problem: NutProblemInput = {
|
||||
version: 2,
|
||||
title,
|
||||
description,
|
||||
user_id: (await getCurrentUser())?.id || '',
|
||||
repositoryId,
|
||||
status: NutProblemStatus.Pending,
|
||||
solution,
|
||||
};
|
||||
|
||||
const problemId = await submitProblem(problem);
|
||||
|
||||
if (problemId) {
|
||||
localStorage.setItem('loadedProblemId', problemId);
|
||||
}
|
||||
|
||||
return problemId;
|
||||
}
|
||||
|
||||
function getReproductionData(): any | null {
|
||||
if (!isSimulatingOrHasFinished()) {
|
||||
toast.error('No simulation data found (neither in progress nor finished)');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const simulationData = getLastUserSimulationData();
|
||||
|
||||
if (!simulationData) {
|
||||
toast.error('No simulation data found');
|
||||
return null;
|
||||
}
|
||||
|
||||
const messages = getLastSimulationChatMessages();
|
||||
const references = getLastSimulationChatReferences();
|
||||
|
||||
if (!messages) {
|
||||
toast.error('No user prompt found');
|
||||
return null;
|
||||
}
|
||||
|
||||
return { simulationData, messages, references };
|
||||
} catch (error: any) {
|
||||
console.error('Error getting reproduction data', error?.stack || error);
|
||||
toast.error(`Error getting reproduction data: ${error?.message}`);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Component for saving the current chat as a new problem.
|
||||
|
||||
export function SaveProblem() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
title: '',
|
||||
description: '',
|
||||
username: '',
|
||||
});
|
||||
const [problemId, setProblemId] = useState<string | null>(null);
|
||||
const [reproData, setReproData] = useState<any>(null);
|
||||
const isLoggedIn = useStore(authStatusStore.isLoggedIn);
|
||||
|
||||
const handleSaveProblem = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const currentReproData = getReproductionData();
|
||||
|
||||
if (!currentReproData) {
|
||||
return;
|
||||
}
|
||||
|
||||
setReproData(currentReproData);
|
||||
setIsModalOpen(true);
|
||||
setProblemId(null);
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmitProblem = async () => {
|
||||
if (!reproData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newProblemId = await saveProblem(formData.title, formData.description, formData.username, reproData);
|
||||
|
||||
if (newProblemId) {
|
||||
setProblemId(newProblemId);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="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={handleSaveProblem}
|
||||
>
|
||||
Save Problem
|
||||
</button>
|
||||
<ReactModal
|
||||
isOpen={isModalOpen}
|
||||
onRequestClose={() => setIsModalOpen(false)}
|
||||
shouldCloseOnOverlayClick={true}
|
||||
shouldCloseOnEsc={true}
|
||||
style={{
|
||||
overlay: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
zIndex: 1000,
|
||||
},
|
||||
content: {
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
right: 'auto',
|
||||
bottom: 'auto',
|
||||
marginRight: '-50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
backgroundColor: 'white',
|
||||
padding: '20px',
|
||||
borderRadius: '8px',
|
||||
maxWidth: '500px',
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{!isLoggedIn && (
|
||||
<div className="text-center">
|
||||
<div className="mb-4">Please log in to save a problem</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsModalOpen(false);
|
||||
authModalStore.open();
|
||||
}}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
Log In
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{isLoggedIn && problemId && (
|
||||
<>
|
||||
<div className="text-center mb-2">Problem Submitted: {problemId}</div>
|
||||
<div className="text-center">
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
{isLoggedIn && !problemId && (
|
||||
<>
|
||||
<div className="text-center">
|
||||
Save prompts as new problems when AI results are unsatisfactory. Problems are publicly visible and are
|
||||
used to improve AI performance.
|
||||
</div>
|
||||
<div style={{ marginTop: '10px' }}>
|
||||
<div className="grid grid-cols-[auto_1fr] gap-4 max-w-md mx-auto">
|
||||
<div className="flex items-center">Title:</div>
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 w-full border border-gray-300"
|
||||
value={formData.title}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
|
||||
<div className="flex items-center">Description:</div>
|
||||
<input
|
||||
type="text"
|
||||
name="description"
|
||||
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 w-full border border-gray-300"
|
||||
value={formData.description}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-center gap-2 mt-4">
|
||||
<button
|
||||
onClick={handleSubmitProblem}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
className="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</ReactModal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -6,7 +6,7 @@ import { getSupabase, getCurrentUserId } from '~/lib/supabase/client';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { getMessagesRepositoryId, type Message } from './message';
|
||||
import { assert } from '~/lib/replay/ReplayProtocolClient';
|
||||
import type { DeploySettingsDatabase } from '../replay/Deploy';
|
||||
import type { DeploySettingsDatabase } from '~/lib/replay/Deploy';
|
||||
|
||||
export interface ChatContents {
|
||||
id: string;
|
||||
@ -200,7 +200,10 @@ export async function databaseGetChatDeploySettings(id: string): Promise<DeployS
|
||||
return data[0].deploy_settings;
|
||||
}
|
||||
|
||||
export async function databaseUpdateChatDeploySettings(id: string, deploySettings: DeploySettingsDatabase): Promise<void> {
|
||||
export async function databaseUpdateChatDeploySettings(
|
||||
id: string,
|
||||
deploySettings: DeploySettingsDatabase,
|
||||
): Promise<void> {
|
||||
const { error } = await getSupabase().from('chats').update({ deploy_settings: deploySettings }).eq('id', id);
|
||||
|
||||
if (error) {
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
|
||||
// State for deploying a chat to production.
|
||||
|
||||
import { sendCommandDedicatedClient } from "./ReplayProtocolClient";
|
||||
import { sendCommandDedicatedClient } from './ReplayProtocolClient';
|
||||
|
||||
// Deploy to a Netlify site.
|
||||
interface DeploySettingsNetlify {
|
||||
@ -53,13 +52,13 @@ export interface DeploySettingsDatabase extends DeploySettings {
|
||||
}
|
||||
|
||||
export async function deployRepository(repositoryId: string, settings: DeploySettings): Promise<DeployResult> {
|
||||
const { result } = await sendCommandDedicatedClient({
|
||||
const { result } = (await sendCommandDedicatedClient({
|
||||
method: 'Nut.deployRepository',
|
||||
params: {
|
||||
repositoryId,
|
||||
settings,
|
||||
},
|
||||
}) as { result: DeployResult };
|
||||
})) as { result: DeployResult };
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -1,15 +1,8 @@
|
||||
// Accessors for the API to access saved problems.
|
||||
|
||||
import { toast } from 'react-toastify';
|
||||
import { assert, sendCommandDedicatedClient } from './ReplayProtocolClient';
|
||||
import type { Message } from '~/lib/persistence/message';
|
||||
import {
|
||||
supabaseListAllProblems,
|
||||
supabaseGetProblem,
|
||||
supabaseSubmitProblem,
|
||||
supabaseUpdateProblem,
|
||||
supabaseSubmitFeedback,
|
||||
supabaseDeleteProblem,
|
||||
} from '~/lib/supabase/problems';
|
||||
import { getNutIsAdmin as getNutIsAdminFromSupabase } from '~/lib/supabase/client';
|
||||
|
||||
// Add global declaration for the problem property
|
||||
declare global {
|
||||
@ -64,11 +57,72 @@ export interface NutProblem extends NutProblemDescription {
|
||||
export type NutProblemInput = Omit<NutProblem, 'problemId' | 'timestamp'>;
|
||||
|
||||
export async function listAllProblems(): Promise<NutProblemDescription[]> {
|
||||
return supabaseListAllProblems();
|
||||
let problems: NutProblemDescription[] = [];
|
||||
|
||||
try {
|
||||
const rv = await sendCommandDedicatedClient({
|
||||
method: 'Recording.globalExperimentalCommand',
|
||||
params: {
|
||||
name: 'listBoltProblems',
|
||||
},
|
||||
});
|
||||
console.log('ListProblemsRval', rv);
|
||||
|
||||
problems = (rv as any).rval.problems.reverse();
|
||||
|
||||
const filteredProblems = problems.filter((problem) => {
|
||||
// if ?showAll=true is not in the url, filter out [test] problems
|
||||
if (window.location.search.includes('showAll=true')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !problem.title.includes('[test]');
|
||||
});
|
||||
|
||||
return filteredProblems;
|
||||
} catch (error) {
|
||||
console.error('Error fetching problems', error);
|
||||
toast.error('Failed to fetch problems');
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getProblem(problemId: string): Promise<NutProblem | null> {
|
||||
const problem = await supabaseGetProblem(problemId);
|
||||
let problem: NutProblem | null = null;
|
||||
|
||||
try {
|
||||
if (!problemId) {
|
||||
toast.error('Invalid problem ID');
|
||||
return null;
|
||||
}
|
||||
|
||||
const rv = await sendCommandDedicatedClient({
|
||||
method: 'Recording.globalExperimentalCommand',
|
||||
params: {
|
||||
name: 'fetchBoltProblem',
|
||||
params: { problemId },
|
||||
},
|
||||
});
|
||||
|
||||
problem = (rv as { rval: { problem: NutProblem } }).rval.problem;
|
||||
|
||||
if (!problem) {
|
||||
toast.error('Problem not found');
|
||||
return null;
|
||||
}
|
||||
|
||||
assert(problem.repositoryId, 'Problem probably has outdated data format. Must have a repositoryId.');
|
||||
} catch (error) {
|
||||
console.error('Error fetching problem', error);
|
||||
|
||||
// Check for specific protocol error
|
||||
if (error instanceof Error && error.message.includes('Unknown problem ID')) {
|
||||
toast.error('Problem not found');
|
||||
} else {
|
||||
toast.error('Failed to fetch problem');
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Only used for testing
|
||||
@ -79,27 +133,3 @@ export async function getProblem(problemId: string): Promise<NutProblem | null>
|
||||
|
||||
return problem;
|
||||
}
|
||||
|
||||
export async function submitProblem(problem: NutProblemInput): Promise<string | null> {
|
||||
return supabaseSubmitProblem(problem);
|
||||
}
|
||||
|
||||
export async function deleteProblem(problemId: string): Promise<void | undefined> {
|
||||
return supabaseDeleteProblem(problemId);
|
||||
}
|
||||
|
||||
export async function updateProblem(problemId: string, problem: NutProblemInput): Promise<NutProblem | null> {
|
||||
await supabaseUpdateProblem(problemId, problem);
|
||||
|
||||
const updatedProblem = await getProblem(problemId);
|
||||
|
||||
return updatedProblem;
|
||||
}
|
||||
|
||||
export async function getNutIsAdmin(): Promise<boolean> {
|
||||
return getNutIsAdminFromSupabase();
|
||||
}
|
||||
|
||||
export async function submitFeedback(feedback: any): Promise<boolean> {
|
||||
return supabaseSubmitFeedback(feedback);
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { atom } from 'nanostores';
|
||||
import { getNutIsAdmin } from '~/lib/replay/Problems';
|
||||
import { getNutIsAdmin } from '~/lib/supabase/client';
|
||||
import { userStore } from './auth';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
import { toast } from 'react-toastify';
|
||||
import { getSupabase, type Database } from './client';
|
||||
import type { NutProblem, NutProblemDescription, NutProblemInput, NutProblemStatus } from '~/lib/replay/Problems';
|
||||
import { getNutIsAdmin } from '~/lib/replay/Problems';
|
||||
import { getNutIsAdmin } from '~/lib/supabase/client';
|
||||
|
||||
async function downloadBlob(bucket: string, path: string) {
|
||||
const supabase = getSupabase();
|
||||
|
||||
@ -4,16 +4,9 @@ import { Menu } from '~/components/sidebar/Menu.client';
|
||||
import BackgroundRays from '~/components/ui/BackgroundRays';
|
||||
import { TooltipProvider } from '@radix-ui/react-tooltip';
|
||||
import { ToastContainerWrapper, Status, Keywords } from './problems';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Suspense, useCallback, useEffect, useState } from 'react';
|
||||
import { Suspense, useEffect, useState } from 'react';
|
||||
import { useParams } from '@remix-run/react';
|
||||
import {
|
||||
getProblem,
|
||||
updateProblem as backendUpdateProblem,
|
||||
deleteProblem as backendDeleteProblem,
|
||||
NutProblemStatus,
|
||||
} from '~/lib/replay/Problems';
|
||||
import { useAdminStatus } from '~/lib/stores/user';
|
||||
import { getProblem, NutProblemStatus } from '~/lib/replay/Problems';
|
||||
import type { NutProblem, NutProblemComment } from '~/lib/replay/Problems';
|
||||
|
||||
function Comments({ comments }: { comments: NutProblemComment[] }) {
|
||||
@ -61,181 +54,11 @@ function ProblemViewer({ problem }: { problem: NutProblem }) {
|
||||
);
|
||||
}
|
||||
|
||||
interface UpdateProblemFormProps {
|
||||
handleSubmit: (content: string) => void;
|
||||
updateText: string;
|
||||
placeholder: string;
|
||||
inputType?: 'textarea' | 'select';
|
||||
options?: { value: string; label: string }[];
|
||||
}
|
||||
|
||||
function UpdateProblemForm(props: UpdateProblemFormProps) {
|
||||
const { handleSubmit, updateText, placeholder, inputType = 'textarea', options = [] } = props;
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
const onSubmitClicked = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (value.trim()) {
|
||||
handleSubmit(value);
|
||||
setValue('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmitClicked} className="mb-6 p-4 bg-bolt-elements-background-depth-2 rounded-lg">
|
||||
{inputType === 'textarea' ? (
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
rows={4}
|
||||
className="w-full p-3 mb-3 bg-bolt-elements-background-depth-3 rounded-md border border-bolt-elements-background-depth-4 text-black placeholder-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-y min-h-[100px]"
|
||||
required
|
||||
/>
|
||||
) : (
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
className="w-full p-3 mb-3 bg-bolt-elements-background-depth-3 rounded-md border border-bolt-elements-background-depth-4 text-black focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
required
|
||||
>
|
||||
<option value="" disabled>
|
||||
{placeholder}
|
||||
</option>
|
||||
{options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!value.trim()}
|
||||
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
|
||||
>
|
||||
{updateText}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
type DoUpdateCallback = (problem: NutProblem) => NutProblem;
|
||||
type UpdateProblemCallback = (doUpdate: DoUpdateCallback) => void;
|
||||
type DeleteProblemCallback = () => void;
|
||||
|
||||
function UpdateProblemForms({
|
||||
updateProblem,
|
||||
deleteProblem,
|
||||
}: {
|
||||
updateProblem: UpdateProblemCallback;
|
||||
deleteProblem: DeleteProblemCallback;
|
||||
}) {
|
||||
const handleAddComment = (content: string) => {
|
||||
const newComment: NutProblemComment = {
|
||||
timestamp: Date.now(),
|
||||
username: 'Anonymous',
|
||||
content,
|
||||
};
|
||||
updateProblem((problem) => {
|
||||
const comments = [...(problem.comments || []), newComment];
|
||||
return {
|
||||
...problem,
|
||||
comments,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleSetTitle = (title: string) => {
|
||||
updateProblem((problem) => ({
|
||||
...problem,
|
||||
title,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSetDescription = (description: string) => {
|
||||
updateProblem((problem) => ({
|
||||
...problem,
|
||||
description,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSetStatus = (status: string) => {
|
||||
const statusEnum = NutProblemStatus[status as keyof typeof NutProblemStatus];
|
||||
|
||||
if (!statusEnum) {
|
||||
toast.error('Invalid status');
|
||||
return;
|
||||
}
|
||||
|
||||
updateProblem((problem) => ({
|
||||
...problem,
|
||||
status: statusEnum,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSetKeywords = (keywordString: string) => {
|
||||
const keywords = keywordString
|
||||
.split(' ')
|
||||
.map((keyword) => keyword.trim())
|
||||
.filter((keyword) => keyword.length > 0);
|
||||
updateProblem((problem) => ({
|
||||
...problem,
|
||||
keywords,
|
||||
}));
|
||||
};
|
||||
|
||||
// Convert NutProblemStatus enum to options array for select
|
||||
const statusOptions = Object.entries(NutProblemStatus).map(([key, _value]) => ({
|
||||
value: key,
|
||||
label: key,
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<UpdateProblemForm handleSubmit={handleAddComment} updateText="Add Comment" placeholder="Add a comment..." />
|
||||
<UpdateProblemForm
|
||||
handleSubmit={handleSetTitle}
|
||||
updateText="Set Title"
|
||||
placeholder="Set the title of the problem..."
|
||||
/>
|
||||
<UpdateProblemForm
|
||||
handleSubmit={handleSetDescription}
|
||||
updateText="Set Description"
|
||||
placeholder="Set the description of the problem..."
|
||||
/>
|
||||
<UpdateProblemForm
|
||||
handleSubmit={handleSetStatus}
|
||||
updateText="Set Status"
|
||||
placeholder="Select a status..."
|
||||
inputType="select"
|
||||
options={statusOptions}
|
||||
/>
|
||||
<UpdateProblemForm
|
||||
handleSubmit={handleSetKeywords}
|
||||
updateText="Set Keywords"
|
||||
placeholder="Set the keywords of the problem..."
|
||||
/>
|
||||
|
||||
<div className="mb-6 p-4 bg-bolt-elements-background-depth-2 rounded-lg">
|
||||
<button
|
||||
onClick={deleteProblem}
|
||||
className="px-6 py-2 bg-red-500 text-white rounded-md hover:bg-red-600 transition-colors duration-200 font-medium"
|
||||
>
|
||||
Delete Problem
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const Nothing = () => null;
|
||||
|
||||
function ViewProblemPage() {
|
||||
const params = useParams();
|
||||
const problemId = params.id;
|
||||
const { isAdmin } = useAdminStatus();
|
||||
|
||||
if (typeof problemId !== 'string') {
|
||||
throw new Error('Problem ID is required');
|
||||
@ -243,30 +66,6 @@ function ViewProblemPage() {
|
||||
|
||||
const [problemData, setProblemData] = useState<NutProblem | null>(null);
|
||||
|
||||
const updateProblem = useCallback(
|
||||
async (callback: DoUpdateCallback) => {
|
||||
if (!problemData) {
|
||||
toast.error('Problem data missing');
|
||||
return;
|
||||
}
|
||||
|
||||
const newProblem = callback(problemData);
|
||||
const updatedProblem = await backendUpdateProblem(problemId, newProblem);
|
||||
|
||||
// If we got an updated problem back from the backend, use it to update the UI
|
||||
if (updatedProblem) {
|
||||
setProblemData(updatedProblem);
|
||||
}
|
||||
},
|
||||
[problemData],
|
||||
);
|
||||
|
||||
const deleteProblem = useCallback(async () => {
|
||||
console.log('BackendDeleteProblem', problemId);
|
||||
await backendDeleteProblem(problemId);
|
||||
toast.success('Problem deleted');
|
||||
}, [problemData]);
|
||||
|
||||
useEffect(() => {
|
||||
getProblem(problemId).then(setProblemData);
|
||||
}, [problemId]);
|
||||
@ -288,7 +87,6 @@ function ViewProblemPage() {
|
||||
<ProblemViewer problem={problemData} />
|
||||
)}
|
||||
</div>
|
||||
{isAdmin && problemData && <UpdateProblemForms updateProblem={updateProblem} deleteProblem={deleteProblem} />}
|
||||
<ToastContainerWrapper />
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user