From ccfee95851ffb1925d805562455ae7a77d652b15 Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Tue, 1 Apr 2025 11:21:59 -0700 Subject: [PATCH] Use backend API for problem storage again (#89) --- app/components/chat/Chat.client.tsx | 4 +- app/components/header/DeployChatButton.tsx | 16 +- app/components/header/Feedback.tsx | 4 +- app/components/sidebar/Menu.client.tsx | 4 - app/components/sidebar/SaveProblem.tsx | 254 --------------------- app/lib/persistence/db.ts | 7 +- app/lib/replay/Deploy.ts | 7 +- app/lib/replay/Problems.ts | 100 +++++--- app/lib/stores/user.ts | 2 +- app/lib/supabase/problems.ts | 2 +- app/routes/problem.$id.tsx | 206 +---------------- 11 files changed, 88 insertions(+), 518 deletions(-) delete mode 100644 app/components/sidebar/SaveProblem.tsx diff --git a/app/components/chat/Chat.client.tsx b/app/components/chat/Chat.client.tsx index 5217afcc..3782753d 100644 --- a/app/components/chat/Chat.client.tsx +++ b/app/components/chat/Chat.client.tsx @@ -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', { diff --git a/app/components/header/DeployChatButton.tsx b/app/components/header/DeployChatButton.tsx index 5d51c25b..0667a0e0 100644 --- a/app/components/header/DeployChatButton.tsx +++ b/app/components/header/DeployChatButton.tsx @@ -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() { ) : ( <>

Deploy

-
- Deploy this chat's app to production. -
+
Deploy this chat's app to production.
{deploySettings?.siteURL && (
@@ -349,11 +351,7 @@ export function DeployChatButton() {
- {error && ( -
- {error} -
- )} + {error &&
{error}
} )} diff --git a/app/components/header/Feedback.tsx b/app/components/header/Feedback.tsx index fd989bef..3f8642df 100644 --- a/app/components/header/Feedback.tsx +++ b/app/components/header/Feedback.tsx @@ -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); diff --git a/app/components/sidebar/Menu.client.tsx b/app/components/sidebar/Menu.client.tsx index d91cd3a2..d7a0faf9 100644 --- a/app/components/sidebar/Menu.client.tsx +++ b/app/components/sidebar/Menu.client.tsx @@ -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(null); const [isSettingsOpen, setIsSettingsOpen] = useState(false); - const { isAdmin } = useAdminStatus(); const { filteredItems: filteredList, handleSearchChange } = useSearchFilter({ items: list, @@ -128,7 +125,6 @@ export const Menu = () => { > Problems - {isAdmin && } { - 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(null); - const [reproData, setReproData] = useState(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) => { - 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 ( - <> - - 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 && ( -
-
Please log in to save a problem
- -
- )} - {isLoggedIn && problemId && ( - <> -
Problem Submitted: {problemId}
-
-
- -
-
- - )} - {isLoggedIn && !problemId && ( - <> -
- Save prompts as new problems when AI results are unsatisfactory. Problems are publicly visible and are - used to improve AI performance. -
-
-
-
Title:
- - -
Description:
- -
-
- - -
-
- - )} -
- - ); -} diff --git a/app/lib/persistence/db.ts b/app/lib/persistence/db.ts index bdf888e8..0ea61716 100644 --- a/app/lib/persistence/db.ts +++ b/app/lib/persistence/db.ts @@ -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 { +export async function databaseUpdateChatDeploySettings( + id: string, + deploySettings: DeploySettingsDatabase, +): Promise { const { error } = await getSupabase().from('chats').update({ deploy_settings: deploySettings }).eq('id', id); if (error) { diff --git a/app/lib/replay/Deploy.ts b/app/lib/replay/Deploy.ts index 2c4b9e2e..baa98d07 100644 --- a/app/lib/replay/Deploy.ts +++ b/app/lib/replay/Deploy.ts @@ -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 { - const { result } = await sendCommandDedicatedClient({ + const { result } = (await sendCommandDedicatedClient({ method: 'Nut.deployRepository', params: { repositoryId, settings, }, - }) as { result: DeployResult }; + })) as { result: DeployResult }; return result; } diff --git a/app/lib/replay/Problems.ts b/app/lib/replay/Problems.ts index a7227d6b..93685982 100644 --- a/app/lib/replay/Problems.ts +++ b/app/lib/replay/Problems.ts @@ -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; export async function listAllProblems(): Promise { - 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 { - 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 return problem; } - -export async function submitProblem(problem: NutProblemInput): Promise { - return supabaseSubmitProblem(problem); -} - -export async function deleteProblem(problemId: string): Promise { - return supabaseDeleteProblem(problemId); -} - -export async function updateProblem(problemId: string, problem: NutProblemInput): Promise { - await supabaseUpdateProblem(problemId, problem); - - const updatedProblem = await getProblem(problemId); - - return updatedProblem; -} - -export async function getNutIsAdmin(): Promise { - return getNutIsAdminFromSupabase(); -} - -export async function submitFeedback(feedback: any): Promise { - return supabaseSubmitFeedback(feedback); -} diff --git a/app/lib/stores/user.ts b/app/lib/stores/user.ts index 22db1bfb..a89f310a 100644 --- a/app/lib/stores/user.ts +++ b/app/lib/stores/user.ts @@ -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'; diff --git a/app/lib/supabase/problems.ts b/app/lib/supabase/problems.ts index 1c67584b..7c363df6 100644 --- a/app/lib/supabase/problems.ts +++ b/app/lib/supabase/problems.ts @@ -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(); diff --git a/app/routes/problem.$id.tsx b/app/routes/problem.$id.tsx index d592cbb1..5fdabac2 100644 --- a/app/routes/problem.$id.tsx +++ b/app/routes/problem.$id.tsx @@ -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) => { - e.preventDefault(); - - if (value.trim()) { - handleSubmit(value); - setValue(''); - } - }; - - return ( -
- {inputType === 'textarea' ? ( -