Use backend API for problem storage again (#89)

This commit is contained in:
Brian Hackett 2025-04-01 11:21:59 -07:00 committed by GitHub
parent 69da8cf043
commit ccfee95851
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 88 additions and 518 deletions

View File

@ -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', {

View File

@ -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>

View File

@ -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);

View File

@ -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"

View File

@ -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>
</>
);
}

View File

@ -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) {

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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';

View File

@ -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();

View File

@ -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>