File size: 4,019 Bytes
c964813
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import { useId, useState } from "react";
import { Dices, Info, RotateCcw } from "lucide-react";
import { AllSettings, Dialog, Dropdown, IconButton, InputField, ParamControls, type ParamValue } from "@elvis/ui";

import { ALGORITHMS, algorithmById } from "./algorithms";
import type { RunEditor } from "./useOptimisation";

const helpClass = "text-sm text-ink-muted";

interface OptimiserControlsProps {
  editor: RunEditor;
  twoVariables: boolean;
  onResetStart: () => void;
  onRandomStart: () => void;
  // Extra help under the start point, e.g. that the plot can be clicked
  hint?: string;
}

// An optimiser's algorithm, settings and start point; used for the main run
// (Configure) and for setting up a comparison (Compare)
export default function OptimiserControls({ editor, twoVariables, onResetStart, onRandomStart, hint }: OptimiserControlsProps) {
  const active = algorithmById(editor.algorithm);
  const [infoOpen, setInfoOpen] = useState(false);
  const id = useId();
  const hasSettings = active.params.length > 0;
  const changed = Object.keys(editor.params).length > 0;

  function setOne(name: string, value: ParamValue) {
    editor.changeParams({ ...editor.params, [name]: value });
  }

  function resetOne(name: string) {
    editor.changeParams(Object.fromEntries(Object.entries(editor.params).filter(([key]) => key !== name)));
  }

  return (
    <>
      {/* The algorithm, with its settings' details and a reset beside it */}
      <div className="flex items-center gap-2">
        <div className="min-w-0 flex-1">
          <Dropdown
            label="Algorithm"
            hideLabel
            options={ALGORITHMS.map((a) => a.label)}
            activeOption={active.label}
            onChange={(label) => editor.changeAlgorithm(ALGORITHMS.find((a) => a.label === label)!.id)}
          />
        </div>
        <IconButton label={`About the ${active.label} settings`} onClick={() => setInfoOpen(true)} disabled={!hasSettings}>
          <Info size={16} />
        </IconButton>
        <IconButton label="Reset the settings to their defaults" onClick={() => editor.changeParams({})} disabled={!changed}>
          <RotateCcw size={16} />
        </IconButton>
      </div>
      {/* Up to 4 settings side by side, each shown by its symbol */}
      <ParamControls model={active} values={editor.params} onChange={editor.changeParams} inline />
      <Dialog title={`${active.label} settings`} open={infoOpen} onClose={() => setInfoOpen(false)} wide>
        <AllSettings model={active} values={editor.params} onChange={setOne} onResetOne={resetOne} onReset={() => editor.changeParams({})} />
      </Dialog>
      <div className="flex flex-col gap-2">
        {/* One label for the coordinates; with two variables each box says which */}
        <div role="group" aria-labelledby={`${id}-initial`} className="flex flex-col gap-1">
          <span id={`${id}-initial`} className="text-sm text-gray-700">Initial point</span>
          <div className="flex items-center gap-2">
            <div className="min-w-0 flex-1">
              <InputField label="Initial x" hideLabel prefix={twoVariables ? "x" : undefined} value={editor.startText[0]} onChange={(t) => editor.changeStart(0, t)} mono />
            </div>
            {twoVariables && (
              <div className="min-w-0 flex-1">
                <InputField label="Initial y" hideLabel prefix="y" value={editor.startText[1]} onChange={(t) => editor.changeStart(1, t)} mono />
              </div>
            )}
            <IconButton label="Random initial point" onClick={onRandomStart}>
              <Dices size={16} />
            </IconButton>
            <IconButton label="Reset the initial point" onClick={onResetStart}>
              <RotateCcw size={16} />
            </IconButton>
          </div>
        </div>
        {!editor.startValid && <p className="text-red-600 font-semibold">The initial point must be numbers.</p>}
        {hint && <p className={helpClass}>{hint}</p>}
      </div>
    </>
  );
}