Improve UI for viewing and changing problems (#15)

This commit is contained in:
Brian Hackett 2025-02-10 20:18:54 -08:00 committed by GitHub
parent b7b602016e
commit bb82c56958
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 570 additions and 114 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View 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;

View File

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

View File

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