Spaces:
Running
Running
File size: 4,565 Bytes
b9499ab | 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 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | import { useEffect, useRef, useState, useSyncExternalStore } from "react";
import { usePythonStatus } from "@elvis/pyodide";
import { python } from "./python";
import { Timeline } from "./Timeline";
import type { Graph, Mode, RunSettings, State } from "./types";
// The example from the course slides, with sin(x1 - x2) added
const DEFAULT_FUNCTION = "exp(x1) + x1*x2 + sin(x1-x2)";
// After typing stops, wait this long before rerunning
const TYPING_DELAY_MS = 500;
const ANIMATED_BY_DEFAULT = !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
function parseNumber(text: string): number | null {
const value = Number(text);
return text.trim() !== "" && Number.isFinite(value) ? value : null;
}
export default function useAutodiff() {
const status = usePythonStatus(python);
const [expression, setExpression] = useState(DEFAULT_FUNCTION);
const [functionError, setFunctionError] = useState<string | null>(null);
// The last function that parsed, kept while the box holds one that doesn't
const [graph, setGraph] = useState<Graph | null>(null);
// As typed, by name, so a value survives while its variable leaves the function
const [inputTexts, setInputTexts] = useState<Record<string, string>>({});
const [mode, setMode] = useState<Mode>("reverse");
const [seedChoice, setSeedChoice] = useState<string | null>(null);
const [runError, setRunError] = useState<string | null>(null);
// Delay before the next rerun: TYPING_DELAY_MS after a keystroke, else 0
const runDelay = useRef(0);
const [animated, setAnimatedState] = useState(ANIMATED_BY_DEFAULT);
const [timeline] = useState(() => new Timeline(ANIMATED_BY_DEFAULT));
const snapshot = useSyncExternalStore(timeline.subscribe, timeline.getSnapshot);
useEffect(() => timeline.pause, [timeline]);
useEffect(() => {
let current = true;
const timer = setTimeout(async () => {
const result = await python.call("handle_set_function", { expression });
if (!current) {
return;
}
setFunctionError(result.ok ? null : result.error.message);
if (result.ok) {
timeline.load(null, []);
setGraph(result.data);
}
}, TYPING_DELAY_MS);
return () => {
current = false;
clearTimeout(timer);
};
}, [expression, timeline]);
const inputs = (graph?.inputs ?? []).map((name, i) => {
const text = inputTexts[name] ?? String(i + 2);
return { name, text, value: parseNumber(text) };
});
const seed = seedChoice !== null && graph?.inputs.includes(seedChoice) ? seedChoice : (graph?.inputs[0] ?? "");
const settings: RunSettings | null =
graph && inputs.every((input) => input.value !== null)
? {
inputs: Object.fromEntries(inputs.map((input) => [input.name, input.value!])),
...(mode === "forward" ? { mode, seed } : { mode }),
}
: null;
const settingsKey = settings && JSON.stringify(settings);
useEffect(() => {
if (!settingsKey) {
return;
}
let current = true;
const timer = setTimeout(async () => {
const next: RunSettings = JSON.parse(settingsKey);
const result = await python.call("handle_set_run", next);
if (!current) {
return;
}
// Every state up front, as the playhead can land on any of them
const states: State[] = [];
let error = result.ok ? null : result.error.message;
for (let k = 0; result.ok && k <= result.data.steps && !error; k++) {
const state = await python.call("handle_state", { iteration: k });
if (!current) {
return;
}
if (state.ok && state.data) {
states.push(state.data);
} else {
error = state.ok ? "The run has no state to show." : state.error.message;
}
}
setRunError(error);
timeline.load(next, error ? [] : states);
}, runDelay.current);
runDelay.current = 0;
return () => {
current = false;
clearTimeout(timer);
};
}, [graph, settingsKey, timeline]);
function setInput(name: string, text: string) {
runDelay.current = TYPING_DELAY_MS;
setInputTexts((texts) => ({ ...texts, [name]: text }));
}
function setAnimated(on: boolean) {
timeline.setAnimated(on);
setAnimatedState(on);
}
return {
status,
expression,
setExpression,
functionError,
graph,
inputs,
setInput,
mode,
setMode,
seed,
setSeed: setSeedChoice,
run: snapshot.run,
runError,
animated,
setAnimated,
timeline,
snapshot,
};
}
|