mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
Improve UI for viewing and changing problems (#15)
This commit is contained in:
parent
b7b602016e
commit
bb82c56958
@ -74,6 +74,12 @@ setInterval(async () => {
|
|||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
|
let gLastUserPrompt: string | undefined = "app goes blank getting directions";
|
||||||
|
|
||||||
|
export function getLastUserPrompt(): string | undefined {
|
||||||
|
return gLastUserPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
export function Chat() {
|
export function Chat() {
|
||||||
renderLogger.trace('Chat');
|
renderLogger.trace('Chat');
|
||||||
|
|
||||||
@ -345,6 +351,8 @@ export const ChatImpl = memo(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gLastUserPrompt = _input;
|
||||||
|
|
||||||
const anthropicApiKey = Cookies.get(anthropicApiKeyCookieName);
|
const anthropicApiKey = Cookies.get(anthropicApiKeyCookieName);
|
||||||
if (!anthropicApiKey) {
|
if (!anthropicApiKey) {
|
||||||
const numFreeUses = +(Cookies.get(anthropicNumFreeUsesCookieName) || 0);
|
const numFreeUses = +(Cookies.get(anthropicNumFreeUsesCookieName) || 0);
|
||||||
|
@ -4,7 +4,8 @@ import { toast } from 'react-toastify';
|
|||||||
import { createChatFromFolder, type FileArtifact } from '~/utils/folderImport';
|
import { createChatFromFolder, type FileArtifact } from '~/utils/folderImport';
|
||||||
import { logStore } from '~/lib/stores/logs'; // Assuming logStore is imported from this location
|
import { logStore } from '~/lib/stores/logs'; // Assuming logStore is imported from this location
|
||||||
import { assert, sendCommandDedicatedClient } from '~/lib/replay/ReplayProtocolClient';
|
import { assert, sendCommandDedicatedClient } from '~/lib/replay/ReplayProtocolClient';
|
||||||
import type { BoltProblem } from '~/components/sidebar/SaveProblem';
|
import type { BoltProblem } from '~/lib/replay/Problems';
|
||||||
|
import { getProblem } from '~/lib/replay/Problems';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
|
|
||||||
interface LoadProblemButtonProps {
|
interface LoadProblemButtonProps {
|
||||||
@ -12,32 +13,31 @@ interface LoadProblemButtonProps {
|
|||||||
importChat?: (description: string, messages: Message[]) => Promise<void>;
|
importChat?: (description: string, messages: Message[]) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadProblem(problemId: string, importChat: (description: string, messages: Message[]) => Promise<void>) {
|
export function setLastLoadedProblem(problem: BoltProblem) {
|
||||||
let problem: BoltProblem | null = null;
|
localStorage.setItem('loadedProblem', JSON.stringify(problem));
|
||||||
try {
|
}
|
||||||
const rv = await sendCommandDedicatedClient({
|
|
||||||
method: "Recording.globalExperimentalCommand",
|
export function getLastLoadedProblem(): BoltProblem | undefined {
|
||||||
params: {
|
const problemJSON = localStorage.getItem('loadedProblem');
|
||||||
name: "fetchBoltProblem",
|
if (!problemJSON) {
|
||||||
params: { problemId },
|
return undefined;
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log("FetchProblemRval", rv);
|
|
||||||
problem = (rv as { rval: BoltProblem }).rval;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching problem", error);
|
|
||||||
toast.error("Failed to fetch problem");
|
|
||||||
}
|
}
|
||||||
|
return JSON.parse(problemJSON);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadProblem(problemId: string, importChat: (description: string, messages: Message[]) => Promise<void>) {
|
||||||
|
const problem = await getProblem(problemId);
|
||||||
|
|
||||||
if (!problem) {
|
if (!problem) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const problemContents = problem.prompt.content;
|
setLastLoadedProblem(problem);
|
||||||
const problemTitle = problem.title;
|
|
||||||
|
const { repositoryContents, title: problemTitle } = problem;
|
||||||
|
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
await zip.loadAsync(problemContents, { base64: true });
|
await zip.loadAsync(repositoryContents, { base64: true });
|
||||||
|
|
||||||
const fileArtifacts: FileArtifact[] = [];
|
const fileArtifacts: FileArtifact[] = [];
|
||||||
for (const [key, object] of Object.entries(zip.files)) {
|
for (const [key, object] of Object.entries(zip.files)) {
|
||||||
|
@ -26,7 +26,7 @@ export const SettingsWindow = ({ open, onClose }: SettingsProps) => {
|
|||||||
|
|
||||||
const tabs: { id: TabType; label: string; icon: string; component?: ReactElement }[] = [
|
const tabs: { id: TabType; label: string; icon: string; component?: ReactElement }[] = [
|
||||||
{ id: 'data', label: 'Data', icon: 'i-ph:database', component: <DataTab /> },
|
{ id: 'data', label: 'Data', icon: 'i-ph:database', component: <DataTab /> },
|
||||||
{ id: 'apiKeys', label: 'API Keys', icon: 'i-ph:key', component: <APIKeysTab /> },
|
{ id: 'apiKeys', label: 'User Info', icon: 'i-ph:key', component: <APIKeysTab /> },
|
||||||
{ id: 'connection', label: 'Connection', icon: 'i-ph:link', component: <ConnectionsTab /> },
|
{ id: 'connection', label: 'Connection', icon: 'i-ph:link', component: <ConnectionsTab /> },
|
||||||
{ id: 'features', label: 'Features', icon: 'i-ph:star', component: <FeaturesTab /> },
|
{ id: 'features', label: 'Features', icon: 'i-ph:star', component: <FeaturesTab /> },
|
||||||
...(debug
|
...(debug
|
||||||
|
@ -2,9 +2,12 @@ import { useState } from 'react';
|
|||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import { anthropicNumFreeUsesCookieName, anthropicApiKeyCookieName, MaxFreeUses } from '~/utils/freeUses';
|
import { anthropicNumFreeUsesCookieName, anthropicApiKeyCookieName, MaxFreeUses } from '~/utils/freeUses';
|
||||||
|
import { setNutAdminKey, setProblemsUsername, getNutAdminKey, getProblemsUsername } from '~/lib/replay/Problems';
|
||||||
|
|
||||||
export default function ConnectionsTab() {
|
export default function ConnectionsTab() {
|
||||||
const [apiKey, setApiKey] = useState(Cookies.get(anthropicApiKeyCookieName) || '');
|
const [apiKey, setApiKey] = useState(Cookies.get(anthropicApiKeyCookieName) || '');
|
||||||
|
const [username, setUsername] = useState(getProblemsUsername() || '');
|
||||||
|
const [adminKey, setAdminKey] = useState(getNutAdminKey() || '');
|
||||||
const numFreeUses = +(Cookies.get(anthropicNumFreeUsesCookieName) || 0);
|
const numFreeUses = +(Cookies.get(anthropicNumFreeUsesCookieName) || 0);
|
||||||
|
|
||||||
const handleSaveAPIKey = async (key: string) => {
|
const handleSaveAPIKey = async (key: string) => {
|
||||||
@ -17,6 +20,16 @@ export default function ConnectionsTab() {
|
|||||||
setApiKey(key);
|
setApiKey(key);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSaveUsername = async (username: string) => {
|
||||||
|
setProblemsUsername(username);
|
||||||
|
setUsername(username);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveAdminKey = async (key: string) => {
|
||||||
|
setNutAdminKey(key);
|
||||||
|
setAdminKey(key);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 mb-4 border border-bolt-elements-borderColor rounded-lg bg-bolt-elements-background-depth-3">
|
<div className="p-4 mb-4 border border-bolt-elements-borderColor rounded-lg bg-bolt-elements-background-depth-3">
|
||||||
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Anthropic API Key</h3>
|
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Anthropic API Key</h3>
|
||||||
@ -37,6 +50,28 @@ export default function ConnectionsTab() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Problems User Name</h3>
|
||||||
|
<div className="flex mb-4">
|
||||||
|
<div className="flex-1 mr-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => handleSaveUsername(e.target.value)}
|
||||||
|
className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Nut Admin Key</h3>
|
||||||
|
<div className="flex mb-4">
|
||||||
|
<div className="flex-1 mr-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={adminKey}
|
||||||
|
onChange={(e) => handleSaveAdminKey(e.target.value)}
|
||||||
|
className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,8 @@ import { binDates } from './date-binning';
|
|||||||
import { useSearchFilter } from '~/lib/hooks/useSearchFilter';
|
import { useSearchFilter } from '~/lib/hooks/useSearchFilter';
|
||||||
import ReactModal from 'react-modal';
|
import ReactModal from 'react-modal';
|
||||||
import { SaveProblem } from './SaveProblem';
|
import { SaveProblem } from './SaveProblem';
|
||||||
|
import { SaveSolution } from './SaveSolution';
|
||||||
|
import { hasNutAdminKey } from '~/lib/replay/Problems';
|
||||||
|
|
||||||
const menuVariants = {
|
const menuVariants = {
|
||||||
closed: {
|
closed: {
|
||||||
@ -138,6 +140,7 @@ export const Menu = () => {
|
|||||||
Problems
|
Problems
|
||||||
</a>
|
</a>
|
||||||
<SaveProblem />
|
<SaveProblem />
|
||||||
|
{hasNutAdminKey() && <SaveSolution />}
|
||||||
<a
|
<a
|
||||||
href="/about"
|
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"
|
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,37 +1,18 @@
|
|||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import ReactModal from 'react-modal';
|
import ReactModal from 'react-modal';
|
||||||
import { assert, sendCommandDedicatedClient } from "~/lib/replay/ReplayProtocolClient";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { workbenchStore } from "~/lib/stores/workbench";
|
import { workbenchStore } from "~/lib/stores/workbench";
|
||||||
|
import { getProblemsUsername, submitProblem } from "~/lib/replay/Problems";
|
||||||
|
import type { BoltProblemInput } from "~/lib/replay/Problems";
|
||||||
|
|
||||||
ReactModal.setAppElement('#root');
|
ReactModal.setAppElement('#root');
|
||||||
|
|
||||||
// Combines information about the contents of a project along with a prompt
|
|
||||||
// from the user and any associated Replay data to accomplish a task. Together
|
|
||||||
// this information is enough that the model should be able to generate a
|
|
||||||
// suitable fix.
|
|
||||||
//
|
|
||||||
// Must be JSON serializable.
|
|
||||||
interface ProjectPrompt {
|
|
||||||
content: string; // base64 encoded zip file
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BoltProblem {
|
|
||||||
version: number;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
prompt: ProjectPrompt;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SaveProblem() {
|
export function SaveProblem() {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
title: '',
|
title: '',
|
||||||
description: '',
|
description: '',
|
||||||
name: '',
|
name: ''
|
||||||
email: ''
|
|
||||||
});
|
});
|
||||||
const [problemId, setProblemId] = useState<string | null>(null);
|
const [problemId, setProblemId] = useState<string | null>(null);
|
||||||
|
|
||||||
@ -40,8 +21,7 @@ export function SaveProblem() {
|
|||||||
setFormData({
|
setFormData({
|
||||||
title: '',
|
title: '',
|
||||||
description: '',
|
description: '',
|
||||||
name: '',
|
name: ''
|
||||||
email: '',
|
|
||||||
});
|
});
|
||||||
setProblemId(null);
|
setProblemId(null);
|
||||||
};
|
};
|
||||||
@ -61,36 +41,31 @@ export function SaveProblem() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const username = getProblemsUsername();
|
||||||
|
|
||||||
|
if (!username) {
|
||||||
|
toast.error('Please fill in username field');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
toast.info("Submitting problem...");
|
toast.info("Submitting problem...");
|
||||||
|
|
||||||
console.log("SubmitProblem", formData);
|
console.log("SubmitProblem", formData);
|
||||||
|
|
||||||
await workbenchStore.saveAllFiles();
|
await workbenchStore.saveAllFiles();
|
||||||
const { contentBase64 } = await workbenchStore.generateZipBase64();
|
const { contentBase64 } = await workbenchStore.generateZipBase64();
|
||||||
const prompt: ProjectPrompt = { content: contentBase64 };
|
|
||||||
|
|
||||||
const problem: BoltProblem = {
|
const problem: BoltProblemInput = {
|
||||||
version: 1,
|
version: 2,
|
||||||
title: formData.title,
|
title: formData.title,
|
||||||
description: formData.description,
|
description: formData.description,
|
||||||
name: formData.name,
|
username,
|
||||||
email: formData.email,
|
repositoryContents: contentBase64,
|
||||||
prompt,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
const problemId = await submitProblem(problem);
|
||||||
const rv = await sendCommandDedicatedClient({
|
if (problemId) {
|
||||||
method: "Recording.globalExperimentalCommand",
|
setProblemId(problemId);
|
||||||
params: {
|
|
||||||
name: "submitBoltProblem",
|
|
||||||
params: { problem },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log("SubmitProblemRval", rv);
|
|
||||||
setProblemId((rv as any).rval.problemId);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error submitting problem", error);
|
|
||||||
toast.error("Failed to submit problem");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,22 +115,6 @@ export function SaveProblem() {
|
|||||||
value={formData.description}
|
value={formData.description}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex items-center">Name (optional):</div>
|
|
||||||
<input type="text"
|
|
||||||
name="name"
|
|
||||||
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 w-full border border-gray-300"
|
|
||||||
value={formData.name}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex items-center">Email (optional):</div>
|
|
||||||
<input type="text"
|
|
||||||
name="email"
|
|
||||||
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 w-full border border-gray-300"
|
|
||||||
value={formData.email}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center gap-2 mt-4">
|
<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={handleSubmitProblem} className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">Submit</button>
|
||||||
|
133
app/components/sidebar/SaveSolution.tsx
Normal file
133
app/components/sidebar/SaveSolution.tsx
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { toast } from "react-toastify";
|
||||||
|
import ReactModal from 'react-modal';
|
||||||
|
import { useState } from "react";
|
||||||
|
import { workbenchStore } from "~/lib/stores/workbench";
|
||||||
|
import { BoltProblemStatus, getProblemsUsername, updateProblem } from "~/lib/replay/Problems";
|
||||||
|
import type { BoltProblemInput } from "~/lib/replay/Problems";
|
||||||
|
import { getLastLoadedProblem } from "../chat/LoadProblemButton";
|
||||||
|
import { getLastUserSimulationData } from "~/lib/replay/SimulationPrompt";
|
||||||
|
import { getLastUserPrompt } from "../chat/Chat.client";
|
||||||
|
|
||||||
|
ReactModal.setAppElement('#root');
|
||||||
|
|
||||||
|
export function SaveSolution() {
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
evaluator: ''
|
||||||
|
});
|
||||||
|
const [savedSolution, setSavedSolution] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleSaveSolution = () => {
|
||||||
|
setIsModalOpen(true);
|
||||||
|
setFormData({
|
||||||
|
evaluator: '',
|
||||||
|
});
|
||||||
|
setSavedSolution(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitSolution = async () => {
|
||||||
|
// Add validation here
|
||||||
|
if (!formData.evaluator) {
|
||||||
|
toast.error('Please fill in evaluator field');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedProblem = getLastLoadedProblem();
|
||||||
|
if (!savedProblem) {
|
||||||
|
toast.error('No problem loaded');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const simulationData = getLastUserSimulationData();
|
||||||
|
if (!simulationData) {
|
||||||
|
toast.error('No simulation data found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userPrompt = getLastUserPrompt();
|
||||||
|
if (!userPrompt) {
|
||||||
|
toast.error('No user prompt found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.info("Submitting solution...");
|
||||||
|
|
||||||
|
console.log("SubmitSolution", formData);
|
||||||
|
|
||||||
|
const problem: BoltProblemInput = {
|
||||||
|
version: 2,
|
||||||
|
title: savedProblem.title,
|
||||||
|
description: savedProblem.description,
|
||||||
|
username: savedProblem.username,
|
||||||
|
repositoryContents: savedProblem.repositoryContents,
|
||||||
|
status: BoltProblemStatus.Solved,
|
||||||
|
solution: {
|
||||||
|
simulationData,
|
||||||
|
userPrompt,
|
||||||
|
evaluator: formData.evaluator,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await updateProblem(savedProblem.problemId, problem);
|
||||||
|
|
||||||
|
setSavedSolution(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
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={handleSaveSolution}
|
||||||
|
>
|
||||||
|
Save Solution
|
||||||
|
</a>
|
||||||
|
<ReactModal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onRequestClose={() => setIsModalOpen(false)}
|
||||||
|
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white rounded-lg p-6 max-w-2xl w-full z-50"
|
||||||
|
overlayClassName="fixed inset-0 bg-black bg-opacity-50 z-40"
|
||||||
|
>
|
||||||
|
{savedSolution && (
|
||||||
|
<>
|
||||||
|
<div className="text-center mb-2">Solution Saved</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>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!savedSolution && (
|
||||||
|
<>
|
||||||
|
<div className="text-center">Save solution for loaded problem from last prompt and recording.</div>
|
||||||
|
<div className="text-center">Evaluator describes a condition the explanation must satisfy.</div>
|
||||||
|
<div style={{ marginTop: "10px" }}>
|
||||||
|
<div className="grid grid-cols-[auto_1fr] gap-4 max-w-md mx-auto">
|
||||||
|
<div className="flex items-center">Evaluator:</div>
|
||||||
|
<input type="text"
|
||||||
|
name="evaluator"
|
||||||
|
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 w-full border border-gray-300"
|
||||||
|
value={formData.evaluator}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-center gap-2 mt-4">
|
||||||
|
<button onClick={handleSubmitSolution} 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
147
app/lib/replay/Problems.ts
Normal file
147
app/lib/replay/Problems.ts
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
// Accessors for the API to access saved problems.
|
||||||
|
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { sendCommandDedicatedClient } from "./ReplayProtocolClient";
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
|
|
||||||
|
export interface BoltProblemComment {
|
||||||
|
username?: string;
|
||||||
|
content: string;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BoltProblemSolution {
|
||||||
|
simulationData: any;
|
||||||
|
userPrompt: string;
|
||||||
|
evaluator: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum BoltProblemStatus {
|
||||||
|
// Problem has been submitted but not yet reviewed.
|
||||||
|
Pending = "Pending",
|
||||||
|
|
||||||
|
// Problem has been reviewed and has not been solved yet.
|
||||||
|
Unsolved = "Unsolved",
|
||||||
|
|
||||||
|
// There are one or more known prompts describing the recording which solve the problem.
|
||||||
|
HasPrompt = "HasPrompt",
|
||||||
|
|
||||||
|
// Nut automatically produces a suitable explanation for solving the problem.
|
||||||
|
Solved = "Solved",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Information about each problem stored in the index file.
|
||||||
|
export interface BoltProblemDescription {
|
||||||
|
version: number;
|
||||||
|
problemId: string;
|
||||||
|
timestamp: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
status?: BoltProblemStatus;
|
||||||
|
keywords?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BoltProblem extends BoltProblemDescription {
|
||||||
|
username?: string;
|
||||||
|
repositoryContents: string;
|
||||||
|
comments?: BoltProblemComment[];
|
||||||
|
solution?: BoltProblemSolution;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BoltProblemInput = Omit<BoltProblem, "problemId" | "timestamp">;
|
||||||
|
|
||||||
|
export async function listAllProblems(): Promise<BoltProblemDescription[]> {
|
||||||
|
try {
|
||||||
|
const rv = await sendCommandDedicatedClient({
|
||||||
|
method: "Recording.globalExperimentalCommand",
|
||||||
|
params: {
|
||||||
|
name: "listBoltProblems",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log("ListProblemsRval", rv);
|
||||||
|
return (rv as any).rval.problems.reverse();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching problems", error);
|
||||||
|
toast.error("Failed to fetch problems");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProblem(problemId: string): Promise<BoltProblem | null> {
|
||||||
|
try {
|
||||||
|
const rv = await sendCommandDedicatedClient({
|
||||||
|
method: "Recording.globalExperimentalCommand",
|
||||||
|
params: {
|
||||||
|
name: "fetchBoltProblem",
|
||||||
|
params: { problemId },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log("FetchProblemRval", rv);
|
||||||
|
return (rv as { rval: { problem: BoltProblem } }).rval.problem;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching problem", error);
|
||||||
|
toast.error("Failed to fetch problem");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function submitProblem(problem: BoltProblemInput): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const rv = await sendCommandDedicatedClient({
|
||||||
|
method: "Recording.globalExperimentalCommand",
|
||||||
|
params: {
|
||||||
|
name: "submitBoltProblem",
|
||||||
|
params: { problem },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log("SubmitProblemRval", rv);
|
||||||
|
return (rv as any).rval.problemId;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error submitting problem", error);
|
||||||
|
toast.error("Failed to submit problem");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateProblem(problemId: string, problem: BoltProblemInput) {
|
||||||
|
try {
|
||||||
|
const adminKey = Cookies.get(nutAdminKeyCookieName);
|
||||||
|
if (!adminKey) {
|
||||||
|
toast.error("Admin key not specified");
|
||||||
|
}
|
||||||
|
await sendCommandDedicatedClient({
|
||||||
|
method: "Recording.globalExperimentalCommand",
|
||||||
|
params: {
|
||||||
|
name: "updateBoltProblem",
|
||||||
|
params: { problemId, problem, adminKey },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating problem", error);
|
||||||
|
toast.error("Failed to update problem");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nutAdminKeyCookieName = 'nutAdminKey';
|
||||||
|
|
||||||
|
export function getNutAdminKey(): string | undefined {
|
||||||
|
return Cookies.get(nutAdminKeyCookieName);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasNutAdminKey(): boolean {
|
||||||
|
return !!getNutAdminKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setNutAdminKey(key: string) {
|
||||||
|
Cookies.set(nutAdminKeyCookieName, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nutProblemsUsernameCookieName = 'nutProblemsUsername';
|
||||||
|
|
||||||
|
export function getProblemsUsername(): string | undefined {
|
||||||
|
return Cookies.get(nutProblemsUsernameCookieName);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setProblemsUsername(username: string) {
|
||||||
|
Cookies.set(nutProblemsUsernameCookieName, username);
|
||||||
|
}
|
@ -7,7 +7,7 @@ import { SimulationDataVersion } from './SimulationData';
|
|||||||
import { assert, ProtocolClient } from './ReplayProtocolClient';
|
import { assert, ProtocolClient } from './ReplayProtocolClient';
|
||||||
import type { MouseData } from './Recording';
|
import type { MouseData } from './Recording';
|
||||||
|
|
||||||
function createRepositoryContentsPacket(contents: string) {
|
function createRepositoryContentsPacket(contents: string): SimulationPacket {
|
||||||
return {
|
return {
|
||||||
kind: "repositoryContents",
|
kind: "repositoryContents",
|
||||||
contents,
|
contents,
|
||||||
@ -99,7 +99,7 @@ class ChatManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
finishSimulationData() {
|
finishSimulationData(): SimulationData {
|
||||||
assert(this.client, "Chat has been destroyed");
|
assert(this.client, "Chat has been destroyed");
|
||||||
assert(!this.simulationFinished, "Simulation has been finished");
|
assert(!this.simulationFinished, "Simulation has been finished");
|
||||||
assert(this.repositoryContents, "Expected repository contents");
|
assert(this.repositoryContents, "Expected repository contents");
|
||||||
@ -179,11 +179,21 @@ export async function simulationAddData(data: SimulationData) {
|
|||||||
gChatManager.addPageData(data);
|
gChatManager.addPageData(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let gLastUserSimulationData: SimulationData | undefined;
|
||||||
|
|
||||||
|
export function getLastUserSimulationData(): SimulationData | undefined {
|
||||||
|
return gLastUserSimulationData;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getSimulationRecording(): Promise<string> {
|
export async function getSimulationRecording(): Promise<string> {
|
||||||
assert(gChatManager, "Expected to have an active chat");
|
assert(gChatManager, "Expected to have an active chat");
|
||||||
|
|
||||||
const simulationData = gChatManager.finishSimulationData();
|
const simulationData = gChatManager.finishSimulationData();
|
||||||
|
|
||||||
|
// The repository contents are part of the problem and excluded from the simulation data
|
||||||
|
// reported for solutions.
|
||||||
|
gLastUserSimulationData = simulationData.filter(packet => packet.kind != "repositoryContents");
|
||||||
|
|
||||||
console.log("SimulationData", new Date().toISOString(), JSON.stringify(simulationData));
|
console.log("SimulationData", new Date().toISOString(), JSON.stringify(simulationData));
|
||||||
|
|
||||||
assert(gChatManager.recordingIdPromise, "Expected recording promise");
|
assert(gChatManager.recordingIdPromise, "Expected recording promise");
|
||||||
|
8
app/routes/load-problem.$id.tsx
Normal file
8
app/routes/load-problem.$id.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { json, type LoaderFunctionArgs } from '@remix-run/cloudflare';
|
||||||
|
import { default as IndexRoute } from './_index';
|
||||||
|
|
||||||
|
export async function loader(args: LoaderFunctionArgs) {
|
||||||
|
return json({ problemId: args.params.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IndexRoute;
|
@ -1,8 +1,184 @@
|
|||||||
import { json, type LoaderFunctionArgs } from '@remix-run/cloudflare';
|
import { ClientOnly } from 'remix-utils/client-only';
|
||||||
import { default as IndexRoute } from './_index';
|
import { Header } from '~/components/header/Header';
|
||||||
|
import { Menu } from '~/components/sidebar/Menu.client';
|
||||||
|
import BackgroundRays from '~/components/ui/BackgroundRays';
|
||||||
|
import { TooltipProvider } from '@radix-ui/react-tooltip';
|
||||||
|
import { ToastContainerWrapper } from './problems';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useParams } from '@remix-run/react';
|
||||||
|
import { getProblem, updateProblem as backendUpdateProblem, getProblemsUsername, BoltProblemStatus, hasNutAdminKey } from '~/lib/replay/Problems';
|
||||||
|
import type { BoltProblem, BoltProblemComment, BoltProblemInput } from '~/lib/replay/Problems';
|
||||||
|
|
||||||
export async function loader(args: LoaderFunctionArgs) {
|
function Status({ status }: { status: BoltProblemStatus }) {
|
||||||
return json({ problemId: args.params.id });
|
const statusColors: Record<BoltProblemStatus, string> = {
|
||||||
|
[BoltProblemStatus.Pending]: 'bg-yellow-400',
|
||||||
|
[BoltProblemStatus.Unsolved]: 'bg-orange-500',
|
||||||
|
[BoltProblemStatus.HasPrompt]: 'bg-blue-200',
|
||||||
|
[BoltProblemStatus.Solved]: 'bg-blue-500'
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 my-2">
|
||||||
|
<span className="font-semibold">Status:</span>
|
||||||
|
<div className={`inline-flex items-center px-3 py-1 rounded-full bg-opacity-10 ${statusColors[status]} text-${status}`}>
|
||||||
|
<span className={`w-2 h-2 rounded-full mr-2 ${statusColors[status]}`}></span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{status.charAt(0).toUpperCase() + status.slice(1)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default IndexRoute;
|
function Keywords({ keywords }: { keywords: string[] }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="keywords">
|
||||||
|
{keywords.map((keyword, index) => (
|
||||||
|
<span key={index} className="keyword">
|
||||||
|
{keyword}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Comments({ comments }: { comments: BoltProblemComment[] }) {
|
||||||
|
return (
|
||||||
|
<div className="comments">
|
||||||
|
{comments.map((comment, index) => (
|
||||||
|
<div key={index} className="comment">
|
||||||
|
<div className="comment-header">
|
||||||
|
<span className="comment-author">{comment.username ?? ""}</span>
|
||||||
|
<span className="comment-date">
|
||||||
|
{new Date(comment.timestamp).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="comment-text">{comment.content}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProblemViewer({ problem }: { problem: BoltProblem }) {
|
||||||
|
const { problemId, title, description, status = BoltProblemStatus.Pending, keywords = [], comments = [] } = problem;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="benchmark">
|
||||||
|
<h1 className="text-xl4 font-semibold mb-2">{title}</h1>
|
||||||
|
<p>{description}</p>
|
||||||
|
<a
|
||||||
|
href={`/load-problem/${problemId}`}
|
||||||
|
className="load-button inline-block px-4 py-2 mt-3 mb-3 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors duration-200 font-medium"
|
||||||
|
>
|
||||||
|
Load Problem
|
||||||
|
</a>
|
||||||
|
<Status status={status} />
|
||||||
|
<Keywords keywords={keywords} />
|
||||||
|
<Comments comments={comments} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DoUpdateCallback = (problem: BoltProblem) => BoltProblem;
|
||||||
|
type UpdateProblemCallback = (doUpdate: DoUpdateCallback) => void;
|
||||||
|
|
||||||
|
function CommentForm({ updateProblem }: { updateProblem: UpdateProblemCallback }) {
|
||||||
|
const [comment, setComment] = useState({
|
||||||
|
author: '',
|
||||||
|
text: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleAddComment = (content: string) => {
|
||||||
|
const newComment: BoltProblemComment = {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
username: getProblemsUsername(),
|
||||||
|
content,
|
||||||
|
}
|
||||||
|
updateProblem(problem => {
|
||||||
|
const comments = [...(problem.comments || []), newComment];
|
||||||
|
return {
|
||||||
|
...problem,
|
||||||
|
comments,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (comment.text.trim() && comment.author.trim()) {
|
||||||
|
handleAddComment(comment.text)
|
||||||
|
setComment({ author: '', text: '' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className="comment-form">
|
||||||
|
<textarea
|
||||||
|
value={comment.text}
|
||||||
|
onChange={(e) => setComment({ ...comment, text: e.target.value })}
|
||||||
|
placeholder="Add a comment..."
|
||||||
|
rows={3}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!comment.text.trim() || !comment.author.trim()}
|
||||||
|
>
|
||||||
|
Add Comment
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ViewProblemPage() {
|
||||||
|
const params = useParams();
|
||||||
|
const problemId = params.id;
|
||||||
|
if (typeof problemId !== 'string') {
|
||||||
|
throw new Error('Problem ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [problemData, setProblemData] = useState<BoltProblem | null>(null);
|
||||||
|
|
||||||
|
const updateProblem = async (callback: DoUpdateCallback) => {
|
||||||
|
if (!problemData) {
|
||||||
|
toast.error('Problem data missing');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newProblem = callback(problemData);
|
||||||
|
setProblemData(newProblem);
|
||||||
|
await backendUpdateProblem(problemId, newProblem);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getProblem(problemId).then(setProblemData);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<div className="flex flex-col h-full w-full bg-bolt-elements-background-depth-1">
|
||||||
|
<BackgroundRays />
|
||||||
|
<Header />
|
||||||
|
<ClientOnly>{() => <Menu />}</ClientOnly>
|
||||||
|
|
||||||
|
<div className="p-6">
|
||||||
|
{problemData === null
|
||||||
|
? (<div className="flex items-center justify-center">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white"></div>
|
||||||
|
</div>)
|
||||||
|
: <ProblemViewer problem={problemData} />}
|
||||||
|
</div>
|
||||||
|
{hasNutAdminKey() && problemData && (
|
||||||
|
<CommentForm updateProblem={updateProblem} />
|
||||||
|
)}
|
||||||
|
<ToastContainerWrapper />
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ViewProblemPage;
|
||||||
|
@ -3,24 +3,18 @@ import { Header } from '~/components/header/Header';
|
|||||||
import { Menu } from '~/components/sidebar/Menu.client';
|
import { Menu } from '~/components/sidebar/Menu.client';
|
||||||
import BackgroundRays from '~/components/ui/BackgroundRays';
|
import BackgroundRays from '~/components/ui/BackgroundRays';
|
||||||
import { TooltipProvider } from '@radix-ui/react-tooltip';
|
import { TooltipProvider } from '@radix-ui/react-tooltip';
|
||||||
import { sendCommandDedicatedClient } from '~/lib/replay/ReplayProtocolClient';
|
import { cssTransition, ToastContainer } from 'react-toastify';
|
||||||
import { cssTransition, toast, ToastContainer } from 'react-toastify';
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { listAllProblems } from '~/lib/replay/Problems';
|
||||||
interface BoltProblemDescription {
|
import type { BoltProblemDescription } from '~/lib/replay/Problems';
|
||||||
problemId: string;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
timestamp: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const toastAnimation = cssTransition({
|
const toastAnimation = cssTransition({
|
||||||
enter: 'animated fadeInRight',
|
enter: 'animated fadeInRight',
|
||||||
exit: 'animated fadeOutRight',
|
exit: 'animated fadeOutRight',
|
||||||
});
|
});
|
||||||
|
|
||||||
function ToastContainerWrapper() {
|
export function ToastContainerWrapper() {
|
||||||
return <ToastContainer
|
return <ToastContainer
|
||||||
closeButton={({ closeToast }) => {
|
closeButton={({ closeToast }) => {
|
||||||
return (
|
return (
|
||||||
@ -50,28 +44,11 @@ function ToastContainerWrapper() {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchProblems(): Promise<BoltProblemDescription[]> {
|
|
||||||
try {
|
|
||||||
const rv = await sendCommandDedicatedClient({
|
|
||||||
method: "Recording.globalExperimentalCommand",
|
|
||||||
params: {
|
|
||||||
name: "listBoltProblems",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log("ListProblemsRval", rv);
|
|
||||||
return (rv as any).rval.problems.reverse();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching problems", error);
|
|
||||||
toast.error("Failed to fetch problems");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ProblemsPage() {
|
function ProblemsPage() {
|
||||||
const [problems, setProblems] = useState<BoltProblemDescription[] | null>(null);
|
const [problems, setProblems] = useState<BoltProblemDescription[] | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchProblems().then(setProblems);
|
listAllProblems().then(setProblems);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
Loading…
Reference in New Issue
Block a user