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);
|
||||
|
||||
let gLastUserPrompt: string | undefined = "app goes blank getting directions";
|
||||
|
||||
export function getLastUserPrompt(): string | undefined {
|
||||
return gLastUserPrompt;
|
||||
}
|
||||
|
||||
export function Chat() {
|
||||
renderLogger.trace('Chat');
|
||||
|
||||
@ -345,6 +351,8 @@ export const ChatImpl = memo(
|
||||
return;
|
||||
}
|
||||
|
||||
gLastUserPrompt = _input;
|
||||
|
||||
const anthropicApiKey = Cookies.get(anthropicApiKeyCookieName);
|
||||
if (!anthropicApiKey) {
|
||||
const numFreeUses = +(Cookies.get(anthropicNumFreeUsesCookieName) || 0);
|
||||
|
@ -4,7 +4,8 @@ import { toast } from 'react-toastify';
|
||||
import { createChatFromFolder, type FileArtifact } from '~/utils/folderImport';
|
||||
import { logStore } from '~/lib/stores/logs'; // Assuming logStore is imported from this location
|
||||
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';
|
||||
|
||||
interface LoadProblemButtonProps {
|
||||
@ -12,32 +13,31 @@ interface LoadProblemButtonProps {
|
||||
importChat?: (description: string, messages: Message[]) => Promise<void>;
|
||||
}
|
||||
|
||||
export async function loadProblem(problemId: string, importChat: (description: string, messages: Message[]) => Promise<void>) {
|
||||
let problem: BoltProblem | null = null;
|
||||
try {
|
||||
const rv = await sendCommandDedicatedClient({
|
||||
method: "Recording.globalExperimentalCommand",
|
||||
params: {
|
||||
name: "fetchBoltProblem",
|
||||
params: { problemId },
|
||||
},
|
||||
});
|
||||
console.log("FetchProblemRval", rv);
|
||||
problem = (rv as { rval: BoltProblem }).rval;
|
||||
} catch (error) {
|
||||
console.error("Error fetching problem", error);
|
||||
toast.error("Failed to fetch problem");
|
||||
export function setLastLoadedProblem(problem: BoltProblem) {
|
||||
localStorage.setItem('loadedProblem', JSON.stringify(problem));
|
||||
}
|
||||
|
||||
export function getLastLoadedProblem(): BoltProblem | undefined {
|
||||
const problemJSON = localStorage.getItem('loadedProblem');
|
||||
if (!problemJSON) {
|
||||
return undefined;
|
||||
}
|
||||
return JSON.parse(problemJSON);
|
||||
}
|
||||
|
||||
export async function loadProblem(problemId: string, importChat: (description: string, messages: Message[]) => Promise<void>) {
|
||||
const problem = await getProblem(problemId);
|
||||
|
||||
if (!problem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const problemContents = problem.prompt.content;
|
||||
const problemTitle = problem.title;
|
||||
setLastLoadedProblem(problem);
|
||||
|
||||
const { repositoryContents, title: problemTitle } = problem;
|
||||
|
||||
const zip = new JSZip();
|
||||
await zip.loadAsync(problemContents, { base64: true });
|
||||
await zip.loadAsync(repositoryContents, { base64: true });
|
||||
|
||||
const fileArtifacts: FileArtifact[] = [];
|
||||
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 }[] = [
|
||||
{ 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: 'features', label: 'Features', icon: 'i-ph:star', component: <FeaturesTab /> },
|
||||
...(debug
|
||||
|
@ -2,9 +2,12 @@ import { useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import Cookies from 'js-cookie';
|
||||
import { anthropicNumFreeUsesCookieName, anthropicApiKeyCookieName, MaxFreeUses } from '~/utils/freeUses';
|
||||
import { setNutAdminKey, setProblemsUsername, getNutAdminKey, getProblemsUsername } from '~/lib/replay/Problems';
|
||||
|
||||
export default function ConnectionsTab() {
|
||||
const [apiKey, setApiKey] = useState(Cookies.get(anthropicApiKeyCookieName) || '');
|
||||
const [username, setUsername] = useState(getProblemsUsername() || '');
|
||||
const [adminKey, setAdminKey] = useState(getNutAdminKey() || '');
|
||||
const numFreeUses = +(Cookies.get(anthropicNumFreeUsesCookieName) || 0);
|
||||
|
||||
const handleSaveAPIKey = async (key: string) => {
|
||||
@ -17,6 +20,16 @@ export default function ConnectionsTab() {
|
||||
setApiKey(key);
|
||||
};
|
||||
|
||||
const handleSaveUsername = async (username: string) => {
|
||||
setProblemsUsername(username);
|
||||
setUsername(username);
|
||||
};
|
||||
|
||||
const handleSaveAdminKey = async (key: string) => {
|
||||
setNutAdminKey(key);
|
||||
setAdminKey(key);
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
@ -37,6 +50,28 @@ export default function ConnectionsTab() {
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
@ -13,6 +13,8 @@ import { binDates } from './date-binning';
|
||||
import { useSearchFilter } from '~/lib/hooks/useSearchFilter';
|
||||
import ReactModal from 'react-modal';
|
||||
import { SaveProblem } from './SaveProblem';
|
||||
import { SaveSolution } from './SaveSolution';
|
||||
import { hasNutAdminKey } from '~/lib/replay/Problems';
|
||||
|
||||
const menuVariants = {
|
||||
closed: {
|
||||
@ -138,6 +140,7 @@ export const Menu = () => {
|
||||
Problems
|
||||
</a>
|
||||
<SaveProblem />
|
||||
{hasNutAdminKey() && <SaveSolution />}
|
||||
<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,37 +1,18 @@
|
||||
import { toast } from "react-toastify";
|
||||
import ReactModal from 'react-modal';
|
||||
import { assert, sendCommandDedicatedClient } from "~/lib/replay/ReplayProtocolClient";
|
||||
import { useState } from "react";
|
||||
import { workbenchStore } from "~/lib/stores/workbench";
|
||||
import { getProblemsUsername, submitProblem } from "~/lib/replay/Problems";
|
||||
import type { BoltProblemInput } from "~/lib/replay/Problems";
|
||||
|
||||
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() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
title: '',
|
||||
description: '',
|
||||
name: '',
|
||||
email: ''
|
||||
name: ''
|
||||
});
|
||||
const [problemId, setProblemId] = useState<string | null>(null);
|
||||
|
||||
@ -40,8 +21,7 @@ export function SaveProblem() {
|
||||
setFormData({
|
||||
title: '',
|
||||
description: '',
|
||||
name: '',
|
||||
email: '',
|
||||
name: ''
|
||||
});
|
||||
setProblemId(null);
|
||||
};
|
||||
@ -61,36 +41,31 @@ export function SaveProblem() {
|
||||
return;
|
||||
}
|
||||
|
||||
const username = getProblemsUsername();
|
||||
|
||||
if (!username) {
|
||||
toast.error('Please fill in username field');
|
||||
return;
|
||||
}
|
||||
|
||||
toast.info("Submitting problem...");
|
||||
|
||||
console.log("SubmitProblem", formData);
|
||||
|
||||
await workbenchStore.saveAllFiles();
|
||||
const { contentBase64 } = await workbenchStore.generateZipBase64();
|
||||
const prompt: ProjectPrompt = { content: contentBase64 };
|
||||
|
||||
const problem: BoltProblem = {
|
||||
version: 1,
|
||||
const problem: BoltProblemInput = {
|
||||
version: 2,
|
||||
title: formData.title,
|
||||
description: formData.description,
|
||||
name: formData.name,
|
||||
email: formData.email,
|
||||
prompt,
|
||||
username,
|
||||
repositoryContents: contentBase64,
|
||||
};
|
||||
|
||||
try {
|
||||
const rv = await sendCommandDedicatedClient({
|
||||
method: "Recording.globalExperimentalCommand",
|
||||
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");
|
||||
const problemId = await submitProblem(problem);
|
||||
if (problemId) {
|
||||
setProblemId(problemId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,22 +115,6 @@ export function SaveProblem() {
|
||||
value={formData.description}
|
||||
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 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>
|
||||
|
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 type { MouseData } from './Recording';
|
||||
|
||||
function createRepositoryContentsPacket(contents: string) {
|
||||
function createRepositoryContentsPacket(contents: string): SimulationPacket {
|
||||
return {
|
||||
kind: "repositoryContents",
|
||||
contents,
|
||||
@ -99,7 +99,7 @@ class ChatManager {
|
||||
});
|
||||
}
|
||||
|
||||
finishSimulationData() {
|
||||
finishSimulationData(): SimulationData {
|
||||
assert(this.client, "Chat has been destroyed");
|
||||
assert(!this.simulationFinished, "Simulation has been finished");
|
||||
assert(this.repositoryContents, "Expected repository contents");
|
||||
@ -179,11 +179,21 @@ export async function simulationAddData(data: SimulationData) {
|
||||
gChatManager.addPageData(data);
|
||||
}
|
||||
|
||||
let gLastUserSimulationData: SimulationData | undefined;
|
||||
|
||||
export function getLastUserSimulationData(): SimulationData | undefined {
|
||||
return gLastUserSimulationData;
|
||||
}
|
||||
|
||||
export async function getSimulationRecording(): Promise<string> {
|
||||
assert(gChatManager, "Expected to have an active chat");
|
||||
|
||||
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));
|
||||
|
||||
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 { default as IndexRoute } from './_index';
|
||||
import { ClientOnly } from 'remix-utils/client-only';
|
||||
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) {
|
||||
return json({ problemId: args.params.id });
|
||||
function Status({ status }: { status: BoltProblemStatus }) {
|
||||
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 BackgroundRays from '~/components/ui/BackgroundRays';
|
||||
import { TooltipProvider } from '@radix-ui/react-tooltip';
|
||||
import { sendCommandDedicatedClient } from '~/lib/replay/ReplayProtocolClient';
|
||||
import { cssTransition, toast, ToastContainer } from 'react-toastify';
|
||||
import { cssTransition, ToastContainer } from 'react-toastify';
|
||||
import { useEffect } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface BoltProblemDescription {
|
||||
problemId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
timestamp: number;
|
||||
}
|
||||
import { listAllProblems } from '~/lib/replay/Problems';
|
||||
import type { BoltProblemDescription } from '~/lib/replay/Problems';
|
||||
|
||||
const toastAnimation = cssTransition({
|
||||
enter: 'animated fadeInRight',
|
||||
exit: 'animated fadeOutRight',
|
||||
});
|
||||
|
||||
function ToastContainerWrapper() {
|
||||
export function ToastContainerWrapper() {
|
||||
return <ToastContainer
|
||||
closeButton={({ closeToast }) => {
|
||||
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() {
|
||||
const [problems, setProblems] = useState<BoltProblemDescription[] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProblems().then(setProblems);
|
||||
listAllProblems().then(setProblems);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
Loading…
Reference in New Issue
Block a user