optimization / frontends /react /src /Sidebar.tsx
nanye's picture
adjust tab label size; add function surface plot for bivariate functions; point tooltip for contour heatmap
0244a95
Raw History Blame Contribute Delete
16.6 kB
import type { ReactNode } from "react";
import AlgorithmControls from "./AlgorithmControls";
import { useCallback, useEffect, useRef, useState } from "react";
import ReactMarkdown from "react-markdown";
import { InputField, Tabs, Card } from "@elvis/ui";
import { type SettingsUi, type TrajectoryValues } from "./types.ts";
import usageMarkdown from "./usage.md?raw";
import IterationHistoryPlot from "./IterationHistoryPlot.tsx";
const UNIVARIATE_FUNCTION_OPTIONS = {
"--Custom--": "x^2",
"Quadratic": "x^2",
"Local Minima": "x^2 - 0.1cos(20x)",
"Plateau": "x^4",
}
const BIVARIATE_FUNCTION_OPTIONS = {
"--Custom--": "x^2 + 3y^2",
"Quadratic": "x^2 + 3y^2",
"Small": "(0.05x)^2 + 3(0.05y)^2",
"Ackley": "-20exp(-0.2 sqrt(0.5(x^2 + y^2))) - exp(0.5(cos(2 pi x) + cos(2 pi y))) + e + 20",
"Rasteringin": "20 + (x^2 - 10cos(2 pi x)) + (y^2 - 10cos(2 pi y))",
"Rosenbrock": "(1 - x)^2 + 100(y - x^2)^2",
}
interface SidebarProps {
comparison: ReactNode,
onTabChange: (tab: string) => void,
settings: SettingsUi,
setSettings: (settings: SettingsUi) => void,
onRandomInitialPoint: () => void,
trajectoryValues?: TrajectoryValues | null,
onReset?: () => void,
onNextStep?: () => void,
onPrevStep?: () => void,
onAnimate?: () => void,
onPause?: () => void,
isAnimating?: boolean,
currentIteration: number,
totalIterations: number,
showGradientField: boolean,
onShowGradientFieldChange: (show: boolean) => void,
}
export default function Sidebar({
comparison,
onTabChange,
settings,
setSettings,
onRandomInitialPoint,
trajectoryValues,
onReset,
onNextStep,
onPrevStep,
onAnimate,
onPause,
isAnimating = false,
currentIteration,
totalIterations,
showGradientField,
onShowGradientFieldChange,
}: SidebarProps) {
const tabs = ["Settings", "Optimize", "Compare", "Usage"] as const;
const [activeTab, setActiveTab] = useState<(typeof tabs)[number]>("Settings");
const [functionOption, setFunctionOption] = useState<string>("--Custom--");
function updateSettings(key: keyof SettingsUi, value: string) {
setSettings({ ...settings, [key]: value });
}
function handleProblemSettingChange(value: string) {
const [mode, option] = value.split("::") as [SettingsUi["mode"], string];
const selectedOption = option ?? "--Custom--";
const expr = mode === "Bivariate"
? BIVARIATE_FUNCTION_OPTIONS[selectedOption as keyof typeof BIVARIATE_FUNCTION_OPTIONS]
: UNIVARIATE_FUNCTION_OPTIONS[selectedOption as keyof typeof UNIVARIATE_FUNCTION_OPTIONS];
setFunctionOption(selectedOption);
setSettings({ ...settings, mode, functionExpr: expr });
}
function getCurrentValue<T>(values?: T[] | null): T | null {
return values?.[currentIteration] ?? null;
}
const currentX = getCurrentValue(trajectoryValues?.x);
const currentY = getCurrentValue(trajectoryValues?.y);
// univariate only
const currentDerivative = getCurrentValue(trajectoryValues?.derivative);
const currentSecondDerivative = getCurrentValue(trajectoryValues?.secondDerivative);
// bivariate only
const currentZ = getCurrentValue(trajectoryValues?.z);
const currentGradient = getCurrentValue(trajectoryValues?.gradient);
const currentHessian = getCurrentValue(trajectoryValues?.hessian);
const holdIntervalRef = useRef<number | null>(null);
const stopHolding = useCallback(() => {
if (holdIntervalRef.current !== null) {
window.clearInterval(holdIntervalRef.current);
holdIntervalRef.current = null;
}
}, []);
const startHolding = useCallback((action?: () => void) => {
if (!action || holdIntervalRef.current !== null) return;
action();
holdIntervalRef.current = window.setInterval(action, 200);
}, []);
useEffect(() => {
function handleKeyDown(event: KeyboardEvent) {
if (activeTab !== "Optimize") return;
const target = event.target as HTMLElement | null;
if (target?.matches("input, textarea, select, [contenteditable='true']")) return;
if (event.repeat) return;
if (event.key === "ArrowLeft") {
event.preventDefault();
startHolding(onPrevStep);
} else if (event.key === "ArrowRight") {
event.preventDefault();
startHolding(onNextStep);
}
}
function handleKeyUp(event: KeyboardEvent) {
if (event.key === "ArrowLeft" || event.key === "ArrowRight") stopHolding();
}
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
window.addEventListener("blur", stopHolding);
return () => {
stopHolding();
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
window.removeEventListener("blur", stopHolding);
};
}, [activeTab, onNextStep, onPrevStep, startHolding, stopHolding]);
return (
<Card className="flex flex-col h-full p-4 gap-2 min-w-0 min-h-0 overflow-auto">
<div className="w-full [&>div]:w-full [&>div>button]:w-1/4 [&>div>button]:px-1">
<Tabs tabs={tabs} activeTab={activeTab} onChange={(tab) => { setActiveTab(tab); onTabChange(tab); }} />
</div>
{/* Tab content */}
<div className="flex flex-1 flex-col gap-4 min-h-0 min-w-0 overflow-auto">
{activeTab === "Settings" && (
<>
<div className="grid grid-cols-[6rem_minmax(0,1fr)] items-center gap-0.5">
<label className="text-sm font-semibold whitespace-nowrap pr-0.25">Objective</label>
<div className="min-w-0">
<select
value={`${settings.mode}::${functionOption}`}
onChange={(event) => handleProblemSettingChange(event.target.value)}
className="h-10 w-full rounded border border-stone-300 bg-white px-2 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-stone-400"
>
<optgroup label="Univariate" className="font-semibold">
{Object.keys(UNIVARIATE_FUNCTION_OPTIONS).map((option) => (
<option key={`Univariate::${option}`} value={`Univariate::${option}`}>
{option}
</option>
))}
</optgroup>
<optgroup label="Bivariate" className="font-semibold">
{Object.keys(BIVARIATE_FUNCTION_OPTIONS).map((option) => (
<option key={`Bivariate::${option}`} value={`Bivariate::${option}`}>
{option}
</option>
))}
</optgroup>
</select>
</div>
</div>
<InputField
label=""
value={settings.functionExpr}
onChange={(value) => updateSettings("functionExpr", value)}
readonly={functionOption !== "--Custom--"}
rows={3}
/>
{settings.mode === "Bivariate" && (
<fieldset className="grid grid-cols-[6rem_minmax(0,1fr)] items-center gap-0.5">
<legend className="text-sm font-medium whitespace-nowrap pr-0.25">Gradient field</legend>
<div className="flex items-center gap-4 text-sm">
<label className="flex cursor-pointer items-center gap-1.5">
<input
type="radio"
name="gradient-field"
checked={showGradientField}
onChange={() => onShowGradientFieldChange(true)}
/>
Show
</label>
<label className="flex cursor-pointer items-center gap-1.5">
<input
type="radio"
name="gradient-field"
checked={!showGradientField}
onChange={() => onShowGradientFieldChange(false)}
/>
Hide
</label>
</div>
</fieldset>
)}
<AlgorithmControls settings={settings} setSettings={setSettings} onRandomInitialPoint={onRandomInitialPoint} />
</>
)}
{activeTab === "Optimize" && (
<>
<IterationHistoryPlot mode={settings.mode} trajectoryValues={trajectoryValues} currentIteration={currentIteration} />
<div className="hidden lg:flex flex-none flex-col gap-1 min-w-0">
<div className="flex items-center justify-between gap-2 text-sm font-semibold">
<span>Current values</span>
<span className="whitespace-nowrap font-normal text-stone-500">
Iteration {currentIteration} of {totalIterations}
</span>
</div>
{ settings.mode === "Univariate" && (
<>
<div className="grid grid-cols-[6rem_minmax(0,1fr)] items-center gap-2">
<label className="text-right text-sm font-medium whitespace-nowrap">X</label>
<input
type="text"
value={currentX !== null ? currentX.toFixed(4) : ""}
readOnly
className="w-full rounded border border-stone-300 bg-stone-50 px-2 py-1 text-sm"
/>
</div>
<div className="grid grid-cols-[6rem_minmax(0,1fr)] items-center gap-2">
<label className="text-right text-sm font-medium whitespace-nowrap">Y</label>
<input
type="text"
value={currentY !== null ? currentY.toFixed(4) : ""}
readOnly
className="w-full rounded border border-stone-300 bg-stone-50 px-2 py-1 text-sm"
/>
</div>
<div className="grid grid-cols-[6rem_minmax(0,1fr)] items-center gap-2">
<label className="text-right text-sm font-medium whitespace-nowrap">1st derivative</label>
<input
type="text"
value={currentDerivative !== null ? currentDerivative.toFixed(4) : ""}
readOnly
className="w-full rounded border border-stone-300 bg-stone-50 px-2 py-1 text-sm"
/>
</div>
<div className="grid grid-cols-[6rem_minmax(0,1fr)] items-center gap-2">
<label className="text-right text-sm font-medium whitespace-nowrap">2nd derivative</label>
<input
type="text"
value={currentSecondDerivative !== null ? currentSecondDerivative.toFixed(4) : ""}
readOnly
className="w-full rounded border border-stone-300 bg-stone-50 px-2 py-1 text-sm"
/>
</div>
</>
)}
{ settings.mode === "Bivariate" && (
<>
<div className="grid grid-cols-[6rem_minmax(0,1fr)] items-center gap-2">
<label className="text-right text-sm font-medium whitespace-nowrap">(X, Y)</label>
<input
type="text"
value={currentX !== null && currentY !== null ? `${currentX.toFixed(4)}, ${currentY.toFixed(4)}` : ""}
readOnly
className="w-full rounded border border-stone-300 bg-stone-50 px-2 py-1 text-sm"
/>
</div>
<div className="grid grid-cols-[6rem_minmax(0,1fr)] items-center gap-2">
<label className="text-right text-sm font-medium whitespace-nowrap">Z</label>
<input
type="text"
value={currentZ !== null ? currentZ.toFixed(4) : ""}
readOnly
className="w-full rounded border border-stone-300 bg-stone-50 px-2 py-1 text-sm"
/>
</div>
<div className="grid grid-cols-[6rem_minmax(0,1fr)] items-center gap-2">
<label className="text-right text-sm font-medium whitespace-nowrap">Gradient</label>
<input
type="text"
value={currentGradient !== null ? `[${currentGradient.map(v => v.toFixed(4)).join(", ")}]` : ""}
readOnly
className="w-full rounded border border-stone-300 bg-stone-50 px-2 py-1 text-sm"
/>
</div>
<div className="grid grid-cols-[6rem_minmax(0,1fr)] items-center gap-2">
<label className="text-right text-sm font-medium whitespace-nowrap">Hessian</label>
<input
type="text"
value={currentHessian !== null ? `[${currentHessian.map(row => `[${row.map(v => v.toFixed(4)).join(", ")}]`).join(", ")}]` : ""}
readOnly
className="w-full rounded border border-stone-300 bg-stone-50 px-2 py-1 text-sm"
/>
</div>
</>
)}
</div>
<div className="-mt-2 flex flex-col gap-1">
<div className="flex gap-2">
<button
type="button"
onPointerDown={() => startHolding(onNextStep)}
onPointerUp={stopHolding}
onPointerCancel={stopHolding}
onPointerLeave={stopHolding}
disabled={!onNextStep}
className="h-8 flex-1 rounded border border-gray-300 bg-orange-200 px-3 py-1 text-sm text-stone-700 hover:bg-orange-300 disabled:cursor-not-allowed disabled:opacity-50"
>
Next
</button>
<button
type="button"
onPointerDown={() => startHolding(onPrevStep)}
onPointerUp={stopHolding}
onPointerCancel={stopHolding}
onPointerLeave={stopHolding}
className="h-8 flex-1 rounded border border-gray-300 bg-orange-200 px-3 py-1 text-sm text-stone-700 hover:bg-orange-300"
>
Previous
</button>
<button
type="button"
onClick={onReset}
className="h-8 flex-1 rounded border border-gray-300 bg-orange-200 px-3 py-1 text-sm text-stone-700 hover:bg-orange-300"
>
Reset
</button>
</div>
<div className="grid grid-cols-3 gap-2">
<button
type="button"
onClick={onAnimate}
disabled={isAnimating || !onAnimate}
className="h-8 rounded border border-gray-300 bg-orange-200 px-3 py-1 text-sm text-stone-700 hover:bg-orange-300 disabled:cursor-not-allowed disabled:opacity-50"
>
Animate
</button>
<button
type="button"
onClick={onPause}
disabled={!isAnimating}
className="h-8 rounded border border-gray-300 bg-stone-100 px-3 py-1 text-sm text-stone-700 hover:bg-stone-200 disabled:cursor-not-allowed disabled:opacity-50"
>
Pause
</button>
</div>
</div>
</>
)}
{activeTab === "Compare" && comparison}
{activeTab === "Usage" && (
<ReactMarkdown
components={{
h1: ({ children }) => <h1 className="text-2xl font-bold mt-0 -mb-2">{children}</h1>,
h2: ({ children }) => <h2 className="text-xl font-semibold mt-0 -mb-2">{children}</h2>,
h3: ({ children }) => <h3 className="text-lg font-semibold mt-0 -mb-2">{children}</h3>,
p: ({ children }) => <p className="leading-6 mb-3 last:mb-0">{children}</p>,
ul: ({ children }) => <ul className="-mt-6 mb-0 list-disc pl-4 [&_p]:mb-0">{children}</ul>,
ol: ({ children }) => <ol className="-mt-5 mb-0 list-decimal pl-4 [&_p]:mb-0">{children}</ol>,
li: ({ children }) => <li className="mb-0">{children}</li>,
}}
>
{usageMarkdown}
</ReactMarkdown>
)}
</div>
</Card>
);
}