From 4466cc55b98404577bdc637a1caa4d5cb10a72b9 Mon Sep 17 00:00:00 2001 From: Jeremy Magland Date: Thu, 25 Jul 2024 12:12:15 -0400 Subject: [PATCH 01/16] Initial implementation of analysis.R --- .../AnalysisWindow/AnalysisRFileEditor.tsx | 242 ++++++++++++++++++ .../AnalysisWindow.tsx} | 54 +++- .../DataGenerationWindow.tsx | 2 +- .../DataGenerationWindow/DataRFileEditor.tsx | 2 +- .../SamplingWindow/SamplingWindow.tsx | 16 +- gui/src/app/pyodide/AnalysisPyFileEditor.tsx | 4 +- 6 files changed, 300 insertions(+), 20 deletions(-) create mode 100644 gui/src/app/pages/HomePage/AnalysisWindow/AnalysisRFileEditor.tsx rename gui/src/app/pages/HomePage/{AnalysisPyWindow/AnalysisPyWindow.tsx => AnalysisWindow/AnalysisWindow.tsx} (70%) diff --git a/gui/src/app/pages/HomePage/AnalysisWindow/AnalysisRFileEditor.tsx b/gui/src/app/pages/HomePage/AnalysisWindow/AnalysisRFileEditor.tsx new file mode 100644 index 00000000..aaace9f3 --- /dev/null +++ b/gui/src/app/pages/HomePage/AnalysisWindow/AnalysisRFileEditor.tsx @@ -0,0 +1,242 @@ +import TextEditor, { ToolbarItem } from "@SpComponents/TextEditor"; +import { GlobalDataForAnalysis } from "@SpPages/AnalysisWindow/AnalysisWindow"; +import { loadWebRInstance } from "@SpPages/DataGenerationWindow/DataRFileEditor"; +import { writeConsoleOutToDiv } from "@SpPyodide/AnalysisPyFileEditor"; +import { PlayArrow } from "@mui/icons-material"; +import { + FunctionComponent, + RefObject, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; +import { Shelter } from "webr"; + +type Props = { + fileName: string; + fileContent: string; + onSaveContent: () => void; + editedFileContent: string; + setEditedFileContent: (text: string) => void; + readOnly: boolean; + imagesRef: RefObject; + consoleRef: RefObject; + spData: GlobalDataForAnalysis | undefined; + scriptHeader?: string; +}; + +type RunStatus = "idle" | "loading" | "running" | "completed" | "failed"; + +const AnalysisRFileEditor: FunctionComponent = ({ + fileName, + fileContent, + onSaveContent, + editedFileContent, + setEditedFileContent, + readOnly, + imagesRef, + consoleRef, + spData, +}) => { + const [status, setStatus] = useState("idle"); + + const callbacks = useMemo( + () => ({ + onStdout: (x: string) => writeConsoleOutToDiv(consoleRef, x, "stdout"), + onStderr: (x: string) => writeConsoleOutToDiv(consoleRef, x, "stderr"), + }), + [consoleRef], + ); + + const hasData = useMemo(() => { + return spData !== undefined; + }, [spData]); + + useEffect(() => { + setStatus("idle"); + }, [spData, fileContent]); + + const handleRun = useCallback(async () => { + if (status === "running") { + return; + } + if (editedFileContent !== fileContent) { + throw new Error("Cannot run edited code"); + } + + if (consoleRef.current) { + consoleRef.current.innerHTML = ""; + } + if (imagesRef.current) { + imagesRef.current.innerHTML = ""; + } + try { + setStatus("loading"); + await sleep(100); // let the UI update + const webR = await loadWebRInstance(); + const shelter = await new webR.Shelter(); + setStatus("running"); + await sleep(100); // let the UI update + const rCode = fileContent; + await runR( + shelter, + rCode, + imagesRef.current, + callbacks.onStdout, + callbacks.onStderr, + ); + setStatus("completed"); + } catch (e) { + console.error(e); + setStatus("failed"); + } + }, [ + status, + editedFileContent, + fileContent, + consoleRef, + imagesRef, + setStatus, + callbacks, + ]); + const toolbarItems: ToolbarItem[] = useMemo(() => { + const ret: ToolbarItem[] = []; + const runnable = + fileContent === editedFileContent && imagesRef.current && hasData; + if (runnable) { + ret.push({ + type: "button", + tooltip: "Run script", + label: "Run", + icon: , + onClick: handleRun, + color: "black", + }); + } + if (!hasData) { + ret.push({ + type: "text", + label: "Run sampler first", + color: "red", + }); + } + if (!imagesRef.current) { + ret.push({ + type: "text", + label: "No output window", + color: "red", + }); + } + let label: string; + let color: string; + if (status === "loading") { + label = "Loading webR..."; + color = "blue"; + } else if (status === "running") { + label = "Running..."; + color = "blue"; + } else if (status === "completed") { + label = "Completed"; + color = "green"; + } else if (status === "failed") { + label = "Failed"; + color = "red"; + } else { + label = ""; + color = "black"; + } + + if (label) { + ret.push({ + type: "text", + label, + color, + }); + } + return ret; + }, [fileContent, editedFileContent, imagesRef, hasData, status, handleRun]); + + return ( + + ); +}; + +//////////////////////////////////////////////////////////////////////////////////////// +// Adapted from https://stackblitz.com/edit/vitejs-vite-6wuedv?file=src%2FApp.tsx +const runR = async ( + shelter: Shelter, + code: string, + imageOutputDiv: HTMLDivElement | null, + onStdout: (x: string) => void, + onStderr: (x: string) => void, +) => { + const captureOutputOptions: any = { + withAutoprint: true, + captureStreams: true, + captureConditions: false, + // env: webR.objs.emptyEnv, // maintain a global environment for webR v0.2.0 + captureGraphics: { + width: 340, + height: 340, + bg: "white", // default: transparent + pointsize: 12, + capture: true, + }, + }; + const result = await shelter.captureR(code, captureOutputOptions); + + try { + // Clear the output div + if (imageOutputDiv) { + imageOutputDiv.innerHTML = ""; + } + + // Display the console outputs. Note that they will all appear before + // the graphics regardless of the order in which they were generated. I + // don't know how to fix this. + for (const evt of result.output) { + if (evt.type === "stdout") { + console.log(evt.data); + onStdout(evt.data); + } else if (evt.type === "stderr") { + console.error(evt.data); + onStderr(evt.data); + } + } + + // Display the graphics + result.images.forEach((img) => { + const canvas = document.createElement("canvas"); + // Set canvas size to image + canvas.width = img.width; + canvas.height = img.height; + + // Draw image onto Canvas + const ctx = canvas.getContext("2d"); + ctx?.drawImage(img, 0, 0, img.width, img.height); + + // Append canvas to figure output area + if (imageOutputDiv) { + imageOutputDiv.appendChild(canvas); + } + }); + } finally { + // Clean up the remaining code + shelter.purge(); + } +}; +//////////////////////////////////////////////////////////////////////////////////////// + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export default AnalysisRFileEditor; diff --git a/gui/src/app/pages/HomePage/AnalysisPyWindow/AnalysisPyWindow.tsx b/gui/src/app/pages/HomePage/AnalysisWindow/AnalysisWindow.tsx similarity index 70% rename from gui/src/app/pages/HomePage/AnalysisPyWindow/AnalysisPyWindow.tsx rename to gui/src/app/pages/HomePage/AnalysisWindow/AnalysisWindow.tsx index bc91de48..5e34b9cf 100644 --- a/gui/src/app/pages/HomePage/AnalysisPyWindow/AnalysisPyWindow.tsx +++ b/gui/src/app/pages/HomePage/AnalysisWindow/AnalysisWindow.tsx @@ -11,13 +11,16 @@ import { SplitDirection, Splitter } from "@SpComponents/Splitter"; import { StanRun } from "@SpStanSampler/useStanSampler"; import { ProjectContext } from "@SpCore/ProjectContextProvider"; import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; +import AnalysisRFileEditor from "./AnalysisRFileEditor"; -type AnalysisPyWindowProps = { +type AnalysisWindowProps = { latestRun: StanRun; + language: "python" | "r"; }; -const AnalysisPyWindow: FunctionComponent = ({ +const AnalysisWindow: FunctionComponent = ({ latestRun, + language, }) => { const imagesRef = useRef(null); @@ -29,7 +32,11 @@ const AnalysisPyWindow: FunctionComponent = ({ return ( - + ); @@ -38,9 +45,10 @@ const AnalysisPyWindow: FunctionComponent = ({ type LeftPaneProps = { imagesRef: RefObject; latestRun: StanRun; + language: "python" | "r"; }; -export type GlobalDataForAnalysisPy = { +export type GlobalDataForAnalysis = { draws: number[][]; paramNames: string[]; numChains: number; @@ -49,6 +57,7 @@ export type GlobalDataForAnalysisPy = { const LeftPane: FunctionComponent = ({ imagesRef, latestRun, + language, }) => { const consoleRef = useRef(null); @@ -72,8 +81,9 @@ const LeftPane: FunctionComponent = ({ return undefined; } }, [status, draws, numChains, paramNames]); - return ( - + + const editor = + language === "python" ? ( = ({ readOnly={false} spData={spData} /> + ) : language === "r" ? ( + { + update({ + type: "editFile", + content, + filename: ProjectKnownFiles.ANALYSISPYFILE, + }); + }} + onSaveContent={() => { + update({ + type: "commitFile", + filename: ProjectKnownFiles.ANALYSISPYFILE, + }); + }} + consoleRef={consoleRef} + imagesRef={imagesRef} + readOnly={false} + spData={spData} + /> + ) : ( +
Unexpected language {language}
+ ); + + return ( + + {editor} ); @@ -121,4 +161,4 @@ const ImageOutputWindow: FunctionComponent = ({ return
; }; -export default AnalysisPyWindow; +export default AnalysisWindow; diff --git a/gui/src/app/pages/HomePage/DataGenerationWindow/DataGenerationWindow.tsx b/gui/src/app/pages/HomePage/DataGenerationWindow/DataGenerationWindow.tsx index b4f9dc06..150181bb 100644 --- a/gui/src/app/pages/HomePage/DataGenerationWindow/DataGenerationWindow.tsx +++ b/gui/src/app/pages/HomePage/DataGenerationWindow/DataGenerationWindow.tsx @@ -1,6 +1,5 @@ import { FunctionComponent, useCallback, useContext, useRef } from "react"; import DataPyFileEditor from "./DataPyFileEditor"; -import { ConsoleOutputWindow } from "../AnalysisPyWindow/AnalysisPyWindow"; import DataRFileEditor from "./DataRFileEditor"; import { writeConsoleOutToDiv } from "@SpPyodide/AnalysisPyFileEditor"; import { SplitDirection, Splitter } from "@SpComponents/Splitter"; @@ -8,6 +7,7 @@ import TabWidget from "@SpComponents/TabWidget"; import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; import { ProjectContext } from "@SpCore/ProjectContextProvider"; import { FileNames } from "@SpCore/FileMapping"; +import { ConsoleOutputWindow } from "@SpPages/AnalysisWindow/AnalysisWindow"; type DataGenerationWindowProps = { // empty diff --git a/gui/src/app/pages/HomePage/DataGenerationWindow/DataRFileEditor.tsx b/gui/src/app/pages/HomePage/DataGenerationWindow/DataRFileEditor.tsx index e0e2d412..8c4dbfa3 100644 --- a/gui/src/app/pages/HomePage/DataGenerationWindow/DataRFileEditor.tsx +++ b/gui/src/app/pages/HomePage/DataGenerationWindow/DataRFileEditor.tsx @@ -22,7 +22,7 @@ type Props = { }; let webR: WebR | null = null; -const loadWebRInstance = async () => { +export const loadWebRInstance = async () => { if (webR === null) { const w = new WebR(); await w.init(); diff --git a/gui/src/app/pages/HomePage/SamplingWindow/SamplingWindow.tsx b/gui/src/app/pages/HomePage/SamplingWindow/SamplingWindow.tsx index a896a20e..08cad336 100644 --- a/gui/src/app/pages/HomePage/SamplingWindow/SamplingWindow.tsx +++ b/gui/src/app/pages/HomePage/SamplingWindow/SamplingWindow.tsx @@ -1,19 +1,19 @@ import { FunctionComponent, useCallback, useContext, useMemo } from "react"; -import AnalysisPyWindow from "../AnalysisPyWindow/AnalysisPyWindow"; import Box from "@mui/material/Box"; -import Grid from "@mui/material/Grid"; import Divider from "@mui/material/Divider"; +import Grid from "@mui/material/Grid"; +import RunPanel from "@SpComponents/RunPanel"; +import SamplerOutputView from "@SpComponents/SamplerOutputView"; +import SamplingOptsPanel from "@SpComponents/SamplingOptsPanel"; import TabWidget from "@SpComponents/TabWidget"; import { ProjectContext } from "@SpCore/ProjectContextProvider"; import { modelHasUnsavedDataFileChanges, SamplingOpts, } from "@SpCore/ProjectDataModel"; +import AnalysisWindow from "@SpPages/AnalysisWindow/AnalysisWindow"; import useStanSampler, { StanRun } from "@SpStanSampler/useStanSampler"; -import RunPanel from "@SpComponents/RunPanel"; -import SamplingOptsPanel from "@SpComponents/SamplingOptsPanel"; -import SamplerOutputView from "@SpComponents/SamplerOutputView"; type SamplingWindowProps = { compiledMainJsUrl?: string; @@ -78,10 +78,8 @@ const SamplingResultsArea: FunctionComponent = ({ - -
-
R analysis not yet implemented
-
+ +
); }; diff --git a/gui/src/app/pyodide/AnalysisPyFileEditor.tsx b/gui/src/app/pyodide/AnalysisPyFileEditor.tsx index a572aae2..57bb947f 100644 --- a/gui/src/app/pyodide/AnalysisPyFileEditor.tsx +++ b/gui/src/app/pyodide/AnalysisPyFileEditor.tsx @@ -8,9 +8,9 @@ import { } from "react"; import { PlayArrow } from "@mui/icons-material"; import { PyodideWorkerStatus } from "./pyodideWorker/pyodideWorkerTypes"; -import { GlobalDataForAnalysisPy } from "../pages/HomePage/AnalysisPyWindow/AnalysisPyWindow"; import usePyodideWorker from "./pyodideWorker/usePyodideWorker"; import TextEditor, { ToolbarItem } from "@SpComponents/TextEditor"; +import { GlobalDataForAnalysis } from "@SpPages/AnalysisWindow/AnalysisWindow"; type Props = { fileName: string; @@ -21,7 +21,7 @@ type Props = { readOnly: boolean; imagesRef: RefObject; consoleRef: RefObject; - spData: GlobalDataForAnalysisPy | undefined; + spData: GlobalDataForAnalysis | undefined; scriptHeader?: string; }; From 6cd57df0442b54aaa0662ec9b32bd4669f35fc9c Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Thu, 25 Jul 2024 16:43:13 +0000 Subject: [PATCH 02/16] analysis.R in data model --- gui/src/app/Project/FileMapping.ts | 2 ++ gui/src/app/Project/ProjectDataModel.ts | 3 +++ .../app/pages/HomePage/AnalysisWindow/AnalysisWindow.tsx | 8 ++++---- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/gui/src/app/Project/FileMapping.ts b/gui/src/app/Project/FileMapping.ts index 7e2ae0cb..f6c818dd 100644 --- a/gui/src/app/Project/FileMapping.ts +++ b/gui/src/app/Project/FileMapping.ts @@ -25,6 +25,7 @@ export enum FileNames { STANFILE = "main.stan", DATAFILE = "data.json", ANALYSISPYFILE = "analysis.py", + ANALYSISRFILE = "analysis.R", DATAPYFILE = "data.py", DATARFILE = "data.R", } @@ -46,6 +47,7 @@ export const ProjectFileMap: FileMapType = { stanFileContent: FileNames.STANFILE, dataFileContent: FileNames.DATAFILE, analysisPyFileContent: FileNames.ANALYSISPYFILE, + analysisRFileContent: FileNames.ANALYSISRFILE, dataPyFileContent: FileNames.DATAPYFILE, dataRFileContent: FileNames.DATARFILE, }; diff --git a/gui/src/app/Project/ProjectDataModel.ts b/gui/src/app/Project/ProjectDataModel.ts index 8ee11f56..081eae4e 100644 --- a/gui/src/app/Project/ProjectDataModel.ts +++ b/gui/src/app/Project/ProjectDataModel.ts @@ -4,6 +4,7 @@ export enum ProjectKnownFiles { STANFILE = "stanFileContent", DATAFILE = "dataFileContent", ANALYSISPYFILE = "analysisPyFileContent", + ANALYSISRFILE = "analysisRFileContent", DATAPYFILE = "dataPyFileContent", DATARFILE = "dataRFileContent", } @@ -130,12 +131,14 @@ export const initialDataModel: ProjectDataModel = { stanFileContent: "", dataFileContent: "", analysisPyFileContent: "", + analysisRFileContent: "", dataPyFileContent: "", dataRFileContent: "", }, stanFileContent: "", dataFileContent: "", analysisPyFileContent: "", + analysisRFileContent: "", dataPyFileContent: "", dataRFileContent: "", samplingOpts: defaultSamplingOpts, diff --git a/gui/src/app/pages/HomePage/AnalysisWindow/AnalysisWindow.tsx b/gui/src/app/pages/HomePage/AnalysisWindow/AnalysisWindow.tsx index 5e34b9cf..c9d8d094 100644 --- a/gui/src/app/pages/HomePage/AnalysisWindow/AnalysisWindow.tsx +++ b/gui/src/app/pages/HomePage/AnalysisWindow/AnalysisWindow.tsx @@ -109,19 +109,19 @@ const LeftPane: FunctionComponent = ({ ) : language === "r" ? ( { update({ type: "editFile", content, - filename: ProjectKnownFiles.ANALYSISPYFILE, + filename: ProjectKnownFiles.ANALYSISRFILE, }); }} onSaveContent={() => { update({ type: "commitFile", - filename: ProjectKnownFiles.ANALYSISPYFILE, + filename: ProjectKnownFiles.ANALYSISRFILE, }); }} consoleRef={consoleRef} From 49abed8de11cbca7e743b3e992b83077e21d4f79 Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Thu, 25 Jul 2024 21:26:14 +0000 Subject: [PATCH 03/16] Re-organize various scripting editors --- .../AnalysisWindow/AnalysisPyWindow.tsx | 130 ++++++++++ .../AnalysisWindow/AnalysisRWindow.tsx | 72 ++++++ .../AnalysisWindow/useAnalysisState.ts | 43 ++++ .../DataGenerationWindow/DataPyWindow.tsx | 127 +++++++++ .../DataGenerationWindow/DataRWindow.tsx | 105 ++++++++ gui/src/app/Scripting/InterpreterTypes.ts | 18 ++ .../app/Scripting/PlottingScriptEditor.tsx | 26 ++ gui/src/app/Scripting/ScriptEditor.tsx | 203 +++++++++++++++ .../pyodideWorker/pyodideWorker.ts | 4 +- .../pyodideWorker/pyodideWorkerTypes.ts | 24 +- .../pyodideWorker/sp_load_draws.py | 0 .../pyodideWorker/sp_patch_matplotlib.py | 0 .../pyodideWorker/usePyodideWorker.ts | 6 +- gui/src/app/Scripting/runR.ts | 113 ++++++++ .../AnalysisWindow/AnalysisRFileEditor.tsx | 242 ------------------ .../AnalysisWindow/AnalysisWindow.tsx | 164 ------------ .../DataGenerationWindow.tsx | 112 -------- .../DataGenerationWindow/DataPyFileEditor.tsx | 124 --------- .../DataGenerationWindow/DataRFileEditor.tsx | 160 ------------ .../getDataGenerationToolbarItems.tsx | 59 ----- gui/src/app/pages/HomePage/HomePage.tsx | 8 +- .../SamplingWindow/SamplingWindow.tsx | 7 +- gui/src/app/pyodide/AnalysisPyFileEditor.tsx | 225 ---------------- 23 files changed, 855 insertions(+), 1117 deletions(-) create mode 100644 gui/src/app/Scripting/AnalysisWindow/AnalysisPyWindow.tsx create mode 100644 gui/src/app/Scripting/AnalysisWindow/AnalysisRWindow.tsx create mode 100644 gui/src/app/Scripting/AnalysisWindow/useAnalysisState.ts create mode 100644 gui/src/app/Scripting/DataGenerationWindow/DataPyWindow.tsx create mode 100644 gui/src/app/Scripting/DataGenerationWindow/DataRWindow.tsx create mode 100644 gui/src/app/Scripting/InterpreterTypes.ts create mode 100644 gui/src/app/Scripting/PlottingScriptEditor.tsx create mode 100644 gui/src/app/Scripting/ScriptEditor.tsx rename gui/src/app/{pyodide => Scripting}/pyodideWorker/pyodideWorker.ts (97%) rename gui/src/app/{pyodide => Scripting}/pyodideWorker/pyodideWorkerTypes.ts (75%) rename gui/src/app/{pyodide => Scripting}/pyodideWorker/sp_load_draws.py (100%) rename gui/src/app/{pyodide => Scripting}/pyodideWorker/sp_patch_matplotlib.py (100%) rename gui/src/app/{pyodide => Scripting}/pyodideWorker/usePyodideWorker.ts (95%) create mode 100644 gui/src/app/Scripting/runR.ts delete mode 100644 gui/src/app/pages/HomePage/AnalysisWindow/AnalysisRFileEditor.tsx delete mode 100644 gui/src/app/pages/HomePage/AnalysisWindow/AnalysisWindow.tsx delete mode 100644 gui/src/app/pages/HomePage/DataGenerationWindow/DataGenerationWindow.tsx delete mode 100644 gui/src/app/pages/HomePage/DataGenerationWindow/DataPyFileEditor.tsx delete mode 100644 gui/src/app/pages/HomePage/DataGenerationWindow/DataRFileEditor.tsx delete mode 100644 gui/src/app/pages/HomePage/DataGenerationWindow/getDataGenerationToolbarItems.tsx delete mode 100644 gui/src/app/pyodide/AnalysisPyFileEditor.tsx diff --git a/gui/src/app/Scripting/AnalysisWindow/AnalysisPyWindow.tsx b/gui/src/app/Scripting/AnalysisWindow/AnalysisPyWindow.tsx new file mode 100644 index 00000000..e40d3f5d --- /dev/null +++ b/gui/src/app/Scripting/AnalysisWindow/AnalysisPyWindow.tsx @@ -0,0 +1,130 @@ +import { FunctionComponent, useCallback, useContext, useMemo } from "react"; +import { StanRun } from "@SpStanSampler/useStanSampler"; +import { FileNames } from "@SpCore/FileMapping"; +import PlottingScriptEditor from "app/Scripting/PlottingScriptEditor"; +import { InterpreterStatus } from "app/Scripting/InterpreterTypes"; +import { writeConsoleOutToDiv } from "app/Scripting/ScriptEditor"; +import usePyodideWorker from "app/Scripting/pyodideWorker/usePyodideWorker"; +import { ProjectContext } from "@SpCore/ProjectContextProvider"; +import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; +import useAnalysisState from "./useAnalysisState"; + +export type GlobalDataForAnalysis = { + draws: number[][]; + paramNames: string[]; + numChains: number; +}; + +type AnalysisWindowProps = { + latestRun: StanRun; +}; + +const AnalysisPyWindow: FunctionComponent = ({ + latestRun, +}) => { + const { consoleRef, imagesRef, spData, status, setStatus } = + useAnalysisState(latestRun); + + const callbacks = useMemo( + () => ({ + onStdout: (x: string) => writeConsoleOutToDiv(consoleRef, x, "stdout"), + onStderr: (x: string) => writeConsoleOutToDiv(consoleRef, x, "stderr"), + onImage: (b64: string) => { + const imageUrl = `data:image/png;base64,${b64}`; + + const img = document.createElement("img"); + img.style.width = "100%"; + img.src = imageUrl; + + const divElement = document.createElement("div"); + divElement.appendChild(img); + imagesRef.current?.appendChild(divElement); + }, + onStatus: (status: InterpreterStatus) => { + setStatus(status); + }, + }), + [consoleRef, imagesRef, setStatus], + ); + + const { run } = usePyodideWorker(callbacks); + + const runnable = useMemo(() => { + return spData !== undefined && status !== "running"; + }, [spData, status]); + + const handleRun = useCallback( + (code: string) => { + if (status === "running") { + return; + } + + if (consoleRef.current) { + consoleRef.current.innerHTML = ""; + } + if (imagesRef.current) { + imagesRef.current.innerHTML = ""; + } + run(code, spData, { + loadsDraws: true, + showsPlots: true, + producesData: false, + }); + }, + [status, consoleRef, imagesRef, run, spData], + ); + + const { update } = useContext(ProjectContext); + const contentOnEmpty = useMemo(() => { + const spanElement = document.createElement("span"); + const t1 = document.createTextNode( + "Use the draws object to access the samples. ", + ); + const a1 = document.createElement("a"); + a1.onclick = () => { + update({ + type: "editFile", + filename: ProjectKnownFiles.ANALYSISPYFILE, + content: analysisPyTemplate, + }); + }; + a1.textContent = "Click here to generate an example"; + spanElement.appendChild(t1); + spanElement.appendChild(a1); + return spanElement; + }, [update]); + + return ( + {}} + imagesRef={imagesRef} + consoleRef={consoleRef} + contentOnEmpty={contentOnEmpty} + /> + ); +}; + +const analysisPyTemplate = `import matplotlib.pyplot as plt + +# Print the draws object +print(draws) + +# Print parameter names +print(draws.parameter_names) + +# plot the lp parameter +samples = draws.get("lp__") +print(samples.shape) +plt.hist(samples.ravel(), bins=30) +plt.title("lp__") +plt.show() +`; + +export default AnalysisPyWindow; diff --git a/gui/src/app/Scripting/AnalysisWindow/AnalysisRWindow.tsx b/gui/src/app/Scripting/AnalysisWindow/AnalysisRWindow.tsx new file mode 100644 index 00000000..ecbe7ca0 --- /dev/null +++ b/gui/src/app/Scripting/AnalysisWindow/AnalysisRWindow.tsx @@ -0,0 +1,72 @@ +import { FunctionComponent, useCallback, useContext, useMemo } from "react"; +import { StanRun } from "@SpStanSampler/useStanSampler"; +import { FileNames } from "@SpCore/FileMapping"; +import PlottingScriptEditor from "app/Scripting/PlottingScriptEditor"; +import { ProjectContext } from "@SpCore/ProjectContextProvider"; +import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; +import useAnalysisState from "./useAnalysisState"; +import runR from "../runR"; + +type AnalysisWindowProps = { + latestRun: StanRun; +}; + +const AnalysisRWindow: FunctionComponent = ({ + latestRun, +}) => { + const { consoleRef, imagesRef, spData, status, setStatus } = + useAnalysisState(latestRun); + + const handleRun = useCallback( + async (code: string) => { + await runR({ code, imagesRef, consoleRef, setStatus }); + }, + [consoleRef, imagesRef, setStatus], + ); + + const runnable = useMemo(() => { + return spData !== undefined && status !== "running"; + }, [spData, status]); + + const { update } = useContext(ProjectContext); + const contentOnEmpty = useMemo(() => { + const spanElement = document.createElement("span"); + const t1 = document.createTextNode( + "Use the draws object to access the samples. ", + ); + const a1 = document.createElement("a"); + a1.onclick = () => { + update({ + type: "editFile", + filename: ProjectKnownFiles.ANALYSISRFILE, + content: analysisRTemplate, + }); + }; + a1.textContent = "Click here to generate an example"; + spanElement.appendChild(t1); + spanElement.appendChild(a1); + return spanElement; + }, [update]); + + return ( + {}} + imagesRef={imagesRef} + consoleRef={consoleRef} + contentOnEmpty={contentOnEmpty} + /> + ); +}; + +const analysisRTemplate = ` +TODO +`; + +export default AnalysisRWindow; diff --git a/gui/src/app/Scripting/AnalysisWindow/useAnalysisState.ts b/gui/src/app/Scripting/AnalysisWindow/useAnalysisState.ts new file mode 100644 index 00000000..6c65a8c9 --- /dev/null +++ b/gui/src/app/Scripting/AnalysisWindow/useAnalysisState.ts @@ -0,0 +1,43 @@ +import { StanRun } from "@SpStanSampler/useStanSampler"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { InterpreterStatus } from "../InterpreterTypes"; + +export type GlobalDataForAnalysis = { + draws: number[][]; + paramNames: string[]; + numChains: number; +}; + +const useAnalysisState = (latestRun: StanRun) => { + const consoleRef = useRef(null); + const imagesRef = useRef(null); + + useEffect(() => { + if (imagesRef.current) { + imagesRef.current.innerHTML = ""; + } + if (consoleRef.current) { + consoleRef.current.innerHTML = ""; + } + }, [latestRun.draws]); + + const { draws, paramNames, samplingOpts, status: samplerStatus } = latestRun; + const numChains = samplingOpts?.num_chains; + const spData = useMemo(() => { + if (samplerStatus === "completed" && draws && numChains && paramNames) { + return { + draws, + paramNames, + numChains, + }; + } else { + return undefined; + } + }, [samplerStatus, draws, numChains, paramNames]); + + const [status, setStatus] = useState("idle"); + + return { consoleRef, imagesRef, spData, status, setStatus }; +}; + +export default useAnalysisState; diff --git a/gui/src/app/Scripting/DataGenerationWindow/DataPyWindow.tsx b/gui/src/app/Scripting/DataGenerationWindow/DataPyWindow.tsx new file mode 100644 index 00000000..7751a54e --- /dev/null +++ b/gui/src/app/Scripting/DataGenerationWindow/DataPyWindow.tsx @@ -0,0 +1,127 @@ +import { + FunctionComponent, + useCallback, + useContext, + useMemo, + useRef, + useState, +} from "react"; +import usePyodideWorker from "app/Scripting/pyodideWorker/usePyodideWorker"; +import ScriptEditor, { writeConsoleOutToDiv } from "app/Scripting/ScriptEditor"; +import { FileNames } from "@SpCore/FileMapping"; +import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; +import { ProjectContext } from "@SpCore/ProjectContextProvider"; +import { InterpreterStatus } from "../InterpreterTypes"; + +type Props = { + // empty +}; + +const DataPyWindow: FunctionComponent = () => { + const [status, setStatus] = useState("idle"); + const consoleRef = useRef(null); + const { data, update } = useContext(ProjectContext); + + const onData = useCallback( + (newData: unknown) => { + const dataJson = JSON.stringify(newData, null, 2); + + if (dataJson !== data.dataFileContent) { + update({ + type: "editFile", + content: dataJson, + filename: ProjectKnownFiles.DATAFILE, + }); + update({ type: "commitFile", filename: ProjectKnownFiles.DATAFILE }); + // Use "stan-playground" prefix to distinguish from console output of the running code + writeConsoleOutToDiv( + consoleRef, + "[stan-playground] Data updated", + "stdout", + ); + } else { + writeConsoleOutToDiv( + consoleRef, + "[stan-playground] Data unchanged", + "stdout", + ); + } + }, + [update, consoleRef, data.dataFileContent], + ); + + const callbacks = useMemo( + () => ({ + onStdout: (x: string) => writeConsoleOutToDiv(consoleRef, x, "stdout"), + onStderr: (x: string) => writeConsoleOutToDiv(consoleRef, x, "stderr"), + onStatus: (status: InterpreterStatus) => { + setStatus(status); + }, + onData, + }), + [onData], + ); + + const { run } = usePyodideWorker(callbacks); + + const handleRun = useCallback( + (code: string) => { + run( + code, + {}, + { + loadsDraws: false, + showsPlots: false, + producesData: true, + }, + ); + }, + [run], + ); + + const handleHelp = useCallback(() => { + alert( + 'Write a Python script to assign data to the "data" variable and then click "Run" to generate data.', + ); + }, []); + + const contentOnEmpty = useMemo(() => { + const spanElement = document.createElement("span"); + const t1 = document.createTextNode( + "Define a dictionary called data to update the data.json. ", + ); + const a1 = document.createElement("a"); + a1.onclick = () => { + update({ + type: "editFile", + filename: ProjectKnownFiles.DATAPYFILE, + content: dataPyTemplate, + }); + }; + a1.textContent = "Click here to generate an example"; + spanElement.appendChild(t1); + spanElement.appendChild(a1); + return spanElement; + }, [update]); + + return ( + + ); +}; + +const dataPyTemplate = `data = { + "a": [1, 2, 3] +}`; + +export default DataPyWindow; diff --git a/gui/src/app/Scripting/DataGenerationWindow/DataRWindow.tsx b/gui/src/app/Scripting/DataGenerationWindow/DataRWindow.tsx new file mode 100644 index 00000000..cca2b9c9 --- /dev/null +++ b/gui/src/app/Scripting/DataGenerationWindow/DataRWindow.tsx @@ -0,0 +1,105 @@ +import { + FunctionComponent, + useCallback, + useContext, + useMemo, + useRef, + useState, +} from "react"; +import ScriptEditor, { writeConsoleOutToDiv } from "app/Scripting/ScriptEditor"; +import { FileNames } from "@SpCore/FileMapping"; +import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; +import { ProjectContext } from "@SpCore/ProjectContextProvider"; +import { InterpreterStatus } from "../InterpreterTypes"; +import runR from "../runR"; + +type Props = { + // empty +}; + +const DataRWindow: FunctionComponent = () => { + const [status, setStatus] = useState("idle"); + const consoleRef = useRef(null); + const { data, update } = useContext(ProjectContext); + + const setData = useCallback( + (newData: unknown) => { + const dataJson = JSON.stringify(newData, null, 2); + + if (dataJson !== data.dataFileContent) { + update({ + type: "editFile", + content: dataJson, + filename: ProjectKnownFiles.DATAFILE, + }); + update({ type: "commitFile", filename: ProjectKnownFiles.DATAFILE }); + // Use "stan-playground" prefix to distinguish from console output of the running code + writeConsoleOutToDiv( + consoleRef, + "[stan-playground] Data updated", + "stdout", + ); + } else { + writeConsoleOutToDiv( + consoleRef, + "[stan-playground] Data unchanged", + "stdout", + ); + } + }, + [update, consoleRef, data.dataFileContent], + ); + + const handleRun = useCallback( + async (code: string) => { + await runR({ code, consoleRef, setStatus, setData }); + }, + [setData], + ); + + const handleHelp = useCallback(() => { + alert( + 'Write a Rthon script to assign data to the "data" variable and then click "Run" to generate data.', + ); + }, []); + + const contentOnEmpty = useMemo(() => { + const spanElement = document.createElement("span"); + const t1 = document.createTextNode( + "Define a dictionary called data to update the data.json. ", + ); + const a1 = document.createElement("a"); + a1.onclick = () => { + update({ + type: "editFile", + filename: ProjectKnownFiles.DATARFILE, + content: dataRTemplate, + }); + }; + a1.textContent = "Click here to generate an example"; + spanElement.appendChild(t1); + spanElement.appendChild(a1); + return spanElement; + }, [update]); + + return ( + + ); +}; + +const dataRTemplate = `data = { + "a": [1, 2, 3] +}`; + +export default DataRWindow; diff --git a/gui/src/app/Scripting/InterpreterTypes.ts b/gui/src/app/Scripting/InterpreterTypes.ts new file mode 100644 index 00000000..f20d3e42 --- /dev/null +++ b/gui/src/app/Scripting/InterpreterTypes.ts @@ -0,0 +1,18 @@ +export type InterpreterStatus = + | "idle" + | "loading" + | "installing" + | "running" + | "completed" + | "failed"; + +export const isInterpreterStatus = (x: any): x is InterpreterStatus => { + return [ + "idle", + "loading", + "installing", + "running", + "completed", + "failed", + ].includes(x); +}; diff --git a/gui/src/app/Scripting/PlottingScriptEditor.tsx b/gui/src/app/Scripting/PlottingScriptEditor.tsx new file mode 100644 index 00000000..0a6028bc --- /dev/null +++ b/gui/src/app/Scripting/PlottingScriptEditor.tsx @@ -0,0 +1,26 @@ +import { FunctionComponent, RefObject } from "react"; +import ScriptEditor, { ScriptEditorProps } from "./ScriptEditor"; +import { SplitDirection, Splitter } from "@SpComponents/Splitter"; + +const PlottingScriptEditor: FunctionComponent< + ScriptEditorProps & { imagesRef: RefObject } +> = (props) => { + return ( + + + + + ); +}; + +type ImageOutputWindowProps = { + imagesRef: RefObject; +}; + +const ImageOutputWindow: FunctionComponent = ({ + imagesRef, +}) => { + return
; +}; + +export default PlottingScriptEditor; diff --git a/gui/src/app/Scripting/ScriptEditor.tsx b/gui/src/app/Scripting/ScriptEditor.tsx new file mode 100644 index 00000000..415911ec --- /dev/null +++ b/gui/src/app/Scripting/ScriptEditor.tsx @@ -0,0 +1,203 @@ +import { Help, PlayArrow } from "@mui/icons-material"; +import { SplitDirection, Splitter } from "@SpComponents/Splitter"; +import TextEditor, { ToolbarItem } from "@SpComponents/TextEditor"; +import { FileNames } from "@SpCore/FileMapping"; +import { ProjectContext } from "@SpCore/ProjectContextProvider"; +import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; +import { + FunctionComponent, + RefObject, + useCallback, + useContext, + useMemo, +} from "react"; +import { InterpreterStatus } from "./InterpreterTypes"; + +const interpreterNames = { python: "pyodide", r: "webR" } as const; + +export type ScriptEditorProps = { + status: InterpreterStatus; + language: "python" | "r"; + filename: FileNames; + dataKey: ProjectKnownFiles; + onRun: (code: string) => void; + runnable: boolean; + notRunnableReason?: string; + onHelp: () => void; + contentOnEmpty?: string | HTMLSpanElement; + consoleRef: RefObject; +}; + +const ScriptEditor: FunctionComponent = ({ + status, + language, + filename, + dataKey, + onRun, + runnable, + notRunnableReason, + onHelp, + contentOnEmpty, + consoleRef, +}) => { + const { data, update } = useContext(ProjectContext); + + const content = data[dataKey]; + const editedContent = data.ephemera[dataKey]; + + const onSetEditedText = useCallback( + (content: string) => { + update({ + type: "editFile", + content, + filename: dataKey, + }); + }, + [dataKey, update], + ); + + const onSaveText = useCallback(() => { + update({ + type: "commitFile", + filename: dataKey, + }); + }, [dataKey, update]); + + const runCode = useCallback(() => { + onRun(content); + }, [content, onRun]); + + const unsavedChanges = useMemo(() => { + return content !== editedContent; + }, [content, editedContent]); + + const toolbarItems: ToolbarItem[] = useMemo(() => { + return makeToolbar({ + status, + name: interpreterNames[language], + runnable: runnable && !unsavedChanges, + notRunnableReason, + onRun: runCode, + onHelp, + }); + }, [ + language, + notRunnableReason, + onHelp, + runCode, + runnable, + status, + unsavedChanges, + ]); + + return ( + + + + + ); +}; + +const makeToolbar = (o: { + status: InterpreterStatus; + name: string; + runnable: boolean; + notRunnableReason?: string; + onRun: () => void; + onHelp: () => void; +}): ToolbarItem[] => { + const { status, onRun, runnable, onHelp, name } = o; + const ret: ToolbarItem[] = []; + ret.push({ + type: "button", + tooltip: "Help", + icon: , + onClick: onHelp, + }); + if (runnable) { + ret.push({ + type: "button", + tooltip: "Run code to generate data", + label: "Run", + icon: , + onClick: onRun, + color: "black", + }); + } else if (o.notRunnableReason) { + ret.push({ + type: "text", + label: o.notRunnableReason, + color: "red", + }); + } + + let label: string; + let color: string; + if (status === "loading") { + label = `Loading ${name}...`; + color = "blue"; + } else if (status === "installing") { + label = `Installing packages for ${name}...`; + color = "blue"; + } else if (status === "running") { + label = "Running..."; + color = "blue"; + } else if (status === "completed") { + label = "Completed"; + color = "green"; + } else if (status === "failed") { + label = "Failed"; + color = "red"; + } else { + label = ""; + color = "black"; + } + + if (label) { + ret.push({ + type: "text", + label, + color, + }); + } + return ret; +}; + +type ConsoleOutputWindowProps = { + consoleRef: RefObject; +}; + +export /* todo don't export */ const ConsoleOutputWindow: FunctionComponent< + ConsoleOutputWindowProps +> = ({ consoleRef }) => { + return
; +}; + +type ConsoleOutType = "stdout" | "stderr"; + +export const writeConsoleOutToDiv = ( + parentDiv: RefObject, + x: string, + type: ConsoleOutType, +) => { + if (x === "") return; + if (!parentDiv.current) return; + const styleClass = type === "stdout" ? "WorkerStdout" : "WorkerStderr"; + const preElement = document.createElement("pre"); + preElement.textContent = x; + const divElement = document.createElement("div"); + divElement.className = styleClass; + divElement.appendChild(preElement); + parentDiv.current.appendChild(divElement); +}; + +export default ScriptEditor; diff --git a/gui/src/app/pyodide/pyodideWorker/pyodideWorker.ts b/gui/src/app/Scripting/pyodideWorker/pyodideWorker.ts similarity index 97% rename from gui/src/app/pyodide/pyodideWorker/pyodideWorker.ts rename to gui/src/app/Scripting/pyodideWorker/pyodideWorker.ts index ed1ef590..afd436d4 100644 --- a/gui/src/app/pyodide/pyodideWorker/pyodideWorker.ts +++ b/gui/src/app/Scripting/pyodideWorker/pyodideWorker.ts @@ -1,11 +1,11 @@ import { PyodideInterface, loadPyodide } from "pyodide"; import { MessageFromPyodideWorker, - PyodideWorkerStatus, PyodideRunSettings, } from "./pyodideWorkerTypes"; import spDrawsScript from "./sp_load_draws.py?raw"; import spMPLScript from "./sp_patch_matplotlib.py?raw"; +import { InterpreterStatus } from "../InterpreterTypes"; let pyodide: PyodideInterface | null = null; const loadPyodideInstance = async () => { @@ -47,7 +47,7 @@ const sendStderr = (data: string) => { sendMessageToMain({ type: "stderr", data }); }; -const setStatus = (status: PyodideWorkerStatus) => { +const setStatus = (status: InterpreterStatus) => { sendMessageToMain({ type: "setStatus", status }); }; diff --git a/gui/src/app/pyodide/pyodideWorker/pyodideWorkerTypes.ts b/gui/src/app/Scripting/pyodideWorker/pyodideWorkerTypes.ts similarity index 75% rename from gui/src/app/pyodide/pyodideWorker/pyodideWorkerTypes.ts rename to gui/src/app/Scripting/pyodideWorker/pyodideWorkerTypes.ts index 44bf7b27..6f037acd 100644 --- a/gui/src/app/pyodide/pyodideWorker/pyodideWorkerTypes.ts +++ b/gui/src/app/Scripting/pyodideWorker/pyodideWorkerTypes.ts @@ -1,4 +1,5 @@ import baseObjectCheck from "@SpUtil/baseObjectCheck"; +import { InterpreterStatus, isInterpreterStatus } from "../InterpreterTypes"; export type PyodideRunSettings = Partial<{ loadsDraws: boolean; @@ -31,7 +32,7 @@ export type MessageFromPyodideWorker = } | { type: "setStatus"; - status: PyodideWorkerStatus; + status: InterpreterStatus; } | { type: "setData"; @@ -48,27 +49,8 @@ export const isMessageFromPyodideWorker = ( if (!baseObjectCheck(x)) return false; if (x.type === "stdout") return x.data !== undefined; if (x.type === "stderr") return x.data !== undefined; - if (x.type === "setStatus") return isPyodideWorkerStatus(x.status); + if (x.type === "setStatus") return isInterpreterStatus(x.status); if (x.type === "setData") return x.data !== undefined; if (x.type === "addImage") return x.image !== undefined; return false; }; - -export type PyodideWorkerStatus = - | "idle" - | "loading" - | "installing" - | "running" - | "completed" - | "failed"; - -export const isPyodideWorkerStatus = (x: any): x is PyodideWorkerStatus => { - return [ - "idle", - "loading", - "installing", - "running", - "completed", - "failed", - ].includes(x); -}; diff --git a/gui/src/app/pyodide/pyodideWorker/sp_load_draws.py b/gui/src/app/Scripting/pyodideWorker/sp_load_draws.py similarity index 100% rename from gui/src/app/pyodide/pyodideWorker/sp_load_draws.py rename to gui/src/app/Scripting/pyodideWorker/sp_load_draws.py diff --git a/gui/src/app/pyodide/pyodideWorker/sp_patch_matplotlib.py b/gui/src/app/Scripting/pyodideWorker/sp_patch_matplotlib.py similarity index 100% rename from gui/src/app/pyodide/pyodideWorker/sp_patch_matplotlib.py rename to gui/src/app/Scripting/pyodideWorker/sp_patch_matplotlib.py diff --git a/gui/src/app/pyodide/pyodideWorker/usePyodideWorker.ts b/gui/src/app/Scripting/pyodideWorker/usePyodideWorker.ts similarity index 95% rename from gui/src/app/pyodide/pyodideWorker/usePyodideWorker.ts rename to gui/src/app/Scripting/pyodideWorker/usePyodideWorker.ts index 9d74a2c6..83ca7380 100644 --- a/gui/src/app/pyodide/pyodideWorker/usePyodideWorker.ts +++ b/gui/src/app/Scripting/pyodideWorker/usePyodideWorker.ts @@ -4,15 +4,15 @@ import pyodideWorkerURL from "./pyodideWorker?worker&url"; import { useCallback, useEffect, useState } from "react"; import { MessageToPyodideWorker, - PyodideWorkerStatus, isMessageFromPyodideWorker, PyodideRunSettings, } from "./pyodideWorkerTypes"; +import { InterpreterStatus } from "../InterpreterTypes"; type PyodideWorkerCallbacks = { onStdout: (data: string) => void; onStderr: (data: string) => void; - onStatus: (status: PyodideWorkerStatus) => void; + onStatus: (status: InterpreterStatus) => void; onData?: (data: any) => void; onImage?: (image: string) => void; }; @@ -97,7 +97,7 @@ class PyodideWorkerInterface { const usePyodideWorker = (callbacks: { onStdout: (data: string) => void; onStderr: (data: string) => void; - onStatus: (status: PyodideWorkerStatus) => void; + onStatus: (status: InterpreterStatus) => void; onData?: (data: any) => void; onImage?: (image: string) => void; }) => { diff --git a/gui/src/app/Scripting/runR.ts b/gui/src/app/Scripting/runR.ts new file mode 100644 index 00000000..e9ad2052 --- /dev/null +++ b/gui/src/app/Scripting/runR.ts @@ -0,0 +1,113 @@ +import { RefObject } from "react"; +import { RString, WebR } from "webr"; +import { InterpreterStatus } from "./InterpreterTypes"; +import { writeConsoleOutToDiv } from "./ScriptEditor"; + +let webR: WebR | null = null; +export const loadWebRInstance = async () => { + if (webR === null) { + const w = new WebR(); + await w.init(); + w.installPackages(["jsonlite"]); + webR = w; + return webR; + } else { + return webR; + } +}; + +type RunRProps = { + code: string; + consoleRef: RefObject; + imagesRef?: RefObject; + setStatus: (status: InterpreterStatus) => void; + setData?: (data: any) => void; +}; + +const runR = async ({ + code, + imagesRef, + consoleRef, + setStatus, + setData, +}: RunRProps) => { + const captureOutputOptions: any = { + withAutoprint: true, + captureStreams: true, + captureConditions: false, + env: {}, + captureGraphics: { + width: 340, + height: 340, + bg: "white", // default: transparent + pointsize: 12, + capture: true, + }, + }; + + try { + setStatus("loading"); + await sleep(100); // let the UI update + const webR = await loadWebRInstance(); + + const shelter = await new webR.Shelter(); + + setStatus("running"); + await sleep(100); // let the UI update + let rCode = + ` +# redirect install.packages to webr's version +webr::shim_install() + +` + code; + + if (setData) { + rCode += ` +if (typeof(data) != "list") { +stop("[stan-playground] data must be a list") +} +.SP_DATA <- jsonlite::toJSON(data, pretty = TRUE, auto_unbox = TRUE) +.SP_DATA`; + } + try { + const ret = await shelter.captureR(rCode, captureOutputOptions); + ret.output.forEach(({ type, data }) => { + if (type === "stdout" || type === "stderr") { + writeConsoleOutToDiv(consoleRef, data, type); + } + }); + + ret.images.forEach((img) => { + if (imagesRef?.current) { + const canvas = document.createElement("canvas"); + // Set canvas size to image + canvas.width = img.width; + canvas.height = img.height; + + // Draw image onto Canvas + const ctx = canvas.getContext("2d"); + ctx?.drawImage(img, 0, 0, img.width, img.height); + + // Append canvas to figure output area + imagesRef.current.appendChild(canvas); + } + }); + + if (setData) { + const result = JSON.parse(await (ret.result as RString).toString()); + setData(result); + } + } finally { + shelter.purge(); + } + setStatus("completed"); + } catch (e: any) { + console.error(e); + writeConsoleOutToDiv(consoleRef, e.toString(), "stderr"); + setStatus("failed"); + } +}; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export default runR; diff --git a/gui/src/app/pages/HomePage/AnalysisWindow/AnalysisRFileEditor.tsx b/gui/src/app/pages/HomePage/AnalysisWindow/AnalysisRFileEditor.tsx deleted file mode 100644 index aaace9f3..00000000 --- a/gui/src/app/pages/HomePage/AnalysisWindow/AnalysisRFileEditor.tsx +++ /dev/null @@ -1,242 +0,0 @@ -import TextEditor, { ToolbarItem } from "@SpComponents/TextEditor"; -import { GlobalDataForAnalysis } from "@SpPages/AnalysisWindow/AnalysisWindow"; -import { loadWebRInstance } from "@SpPages/DataGenerationWindow/DataRFileEditor"; -import { writeConsoleOutToDiv } from "@SpPyodide/AnalysisPyFileEditor"; -import { PlayArrow } from "@mui/icons-material"; -import { - FunctionComponent, - RefObject, - useCallback, - useEffect, - useMemo, - useState, -} from "react"; -import { Shelter } from "webr"; - -type Props = { - fileName: string; - fileContent: string; - onSaveContent: () => void; - editedFileContent: string; - setEditedFileContent: (text: string) => void; - readOnly: boolean; - imagesRef: RefObject; - consoleRef: RefObject; - spData: GlobalDataForAnalysis | undefined; - scriptHeader?: string; -}; - -type RunStatus = "idle" | "loading" | "running" | "completed" | "failed"; - -const AnalysisRFileEditor: FunctionComponent = ({ - fileName, - fileContent, - onSaveContent, - editedFileContent, - setEditedFileContent, - readOnly, - imagesRef, - consoleRef, - spData, -}) => { - const [status, setStatus] = useState("idle"); - - const callbacks = useMemo( - () => ({ - onStdout: (x: string) => writeConsoleOutToDiv(consoleRef, x, "stdout"), - onStderr: (x: string) => writeConsoleOutToDiv(consoleRef, x, "stderr"), - }), - [consoleRef], - ); - - const hasData = useMemo(() => { - return spData !== undefined; - }, [spData]); - - useEffect(() => { - setStatus("idle"); - }, [spData, fileContent]); - - const handleRun = useCallback(async () => { - if (status === "running") { - return; - } - if (editedFileContent !== fileContent) { - throw new Error("Cannot run edited code"); - } - - if (consoleRef.current) { - consoleRef.current.innerHTML = ""; - } - if (imagesRef.current) { - imagesRef.current.innerHTML = ""; - } - try { - setStatus("loading"); - await sleep(100); // let the UI update - const webR = await loadWebRInstance(); - const shelter = await new webR.Shelter(); - setStatus("running"); - await sleep(100); // let the UI update - const rCode = fileContent; - await runR( - shelter, - rCode, - imagesRef.current, - callbacks.onStdout, - callbacks.onStderr, - ); - setStatus("completed"); - } catch (e) { - console.error(e); - setStatus("failed"); - } - }, [ - status, - editedFileContent, - fileContent, - consoleRef, - imagesRef, - setStatus, - callbacks, - ]); - const toolbarItems: ToolbarItem[] = useMemo(() => { - const ret: ToolbarItem[] = []; - const runnable = - fileContent === editedFileContent && imagesRef.current && hasData; - if (runnable) { - ret.push({ - type: "button", - tooltip: "Run script", - label: "Run", - icon: , - onClick: handleRun, - color: "black", - }); - } - if (!hasData) { - ret.push({ - type: "text", - label: "Run sampler first", - color: "red", - }); - } - if (!imagesRef.current) { - ret.push({ - type: "text", - label: "No output window", - color: "red", - }); - } - let label: string; - let color: string; - if (status === "loading") { - label = "Loading webR..."; - color = "blue"; - } else if (status === "running") { - label = "Running..."; - color = "blue"; - } else if (status === "completed") { - label = "Completed"; - color = "green"; - } else if (status === "failed") { - label = "Failed"; - color = "red"; - } else { - label = ""; - color = "black"; - } - - if (label) { - ret.push({ - type: "text", - label, - color, - }); - } - return ret; - }, [fileContent, editedFileContent, imagesRef, hasData, status, handleRun]); - - return ( - - ); -}; - -//////////////////////////////////////////////////////////////////////////////////////// -// Adapted from https://stackblitz.com/edit/vitejs-vite-6wuedv?file=src%2FApp.tsx -const runR = async ( - shelter: Shelter, - code: string, - imageOutputDiv: HTMLDivElement | null, - onStdout: (x: string) => void, - onStderr: (x: string) => void, -) => { - const captureOutputOptions: any = { - withAutoprint: true, - captureStreams: true, - captureConditions: false, - // env: webR.objs.emptyEnv, // maintain a global environment for webR v0.2.0 - captureGraphics: { - width: 340, - height: 340, - bg: "white", // default: transparent - pointsize: 12, - capture: true, - }, - }; - const result = await shelter.captureR(code, captureOutputOptions); - - try { - // Clear the output div - if (imageOutputDiv) { - imageOutputDiv.innerHTML = ""; - } - - // Display the console outputs. Note that they will all appear before - // the graphics regardless of the order in which they were generated. I - // don't know how to fix this. - for (const evt of result.output) { - if (evt.type === "stdout") { - console.log(evt.data); - onStdout(evt.data); - } else if (evt.type === "stderr") { - console.error(evt.data); - onStderr(evt.data); - } - } - - // Display the graphics - result.images.forEach((img) => { - const canvas = document.createElement("canvas"); - // Set canvas size to image - canvas.width = img.width; - canvas.height = img.height; - - // Draw image onto Canvas - const ctx = canvas.getContext("2d"); - ctx?.drawImage(img, 0, 0, img.width, img.height); - - // Append canvas to figure output area - if (imageOutputDiv) { - imageOutputDiv.appendChild(canvas); - } - }); - } finally { - // Clean up the remaining code - shelter.purge(); - } -}; -//////////////////////////////////////////////////////////////////////////////////////// - -const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - -export default AnalysisRFileEditor; diff --git a/gui/src/app/pages/HomePage/AnalysisWindow/AnalysisWindow.tsx b/gui/src/app/pages/HomePage/AnalysisWindow/AnalysisWindow.tsx deleted file mode 100644 index c9d8d094..00000000 --- a/gui/src/app/pages/HomePage/AnalysisWindow/AnalysisWindow.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { - FunctionComponent, - RefObject, - useContext, - useEffect, - useMemo, - useRef, -} from "react"; -import AnalysisPyFileEditor from "../../../pyodide/AnalysisPyFileEditor"; -import { SplitDirection, Splitter } from "@SpComponents/Splitter"; -import { StanRun } from "@SpStanSampler/useStanSampler"; -import { ProjectContext } from "@SpCore/ProjectContextProvider"; -import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; -import AnalysisRFileEditor from "./AnalysisRFileEditor"; - -type AnalysisWindowProps = { - latestRun: StanRun; - language: "python" | "r"; -}; - -const AnalysisWindow: FunctionComponent = ({ - latestRun, - language, -}) => { - const imagesRef = useRef(null); - - useEffect(() => { - if (imagesRef.current) { - imagesRef.current.innerHTML = ""; - } - }, [latestRun.draws]); - - return ( - - - - - ); -}; - -type LeftPaneProps = { - imagesRef: RefObject; - latestRun: StanRun; - language: "python" | "r"; -}; - -export type GlobalDataForAnalysis = { - draws: number[][]; - paramNames: string[]; - numChains: number; -}; - -const LeftPane: FunctionComponent = ({ - imagesRef, - latestRun, - language, -}) => { - const consoleRef = useRef(null); - - useEffect(() => { - if (consoleRef.current) { - consoleRef.current.innerHTML = ""; - } - }, [latestRun.draws]); - - const { data, update } = useContext(ProjectContext); - const { draws, paramNames, samplingOpts, status } = latestRun; - const numChains = samplingOpts?.num_chains; - const spData = useMemo(() => { - if (status === "completed" && draws && numChains && paramNames) { - return { - draws, - paramNames, - numChains, - }; - } else { - return undefined; - } - }, [status, draws, numChains, paramNames]); - - const editor = - language === "python" ? ( - { - update({ - type: "editFile", - content, - filename: ProjectKnownFiles.ANALYSISPYFILE, - }); - }} - onSaveContent={() => { - update({ - type: "commitFile", - filename: ProjectKnownFiles.ANALYSISPYFILE, - }); - }} - consoleRef={consoleRef} - imagesRef={imagesRef} - readOnly={false} - spData={spData} - /> - ) : language === "r" ? ( - { - update({ - type: "editFile", - content, - filename: ProjectKnownFiles.ANALYSISRFILE, - }); - }} - onSaveContent={() => { - update({ - type: "commitFile", - filename: ProjectKnownFiles.ANALYSISRFILE, - }); - }} - consoleRef={consoleRef} - imagesRef={imagesRef} - readOnly={false} - spData={spData} - /> - ) : ( -
Unexpected language {language}
- ); - - return ( - - {editor} - - - ); -}; - -type ConsoleOutputWindowProps = { - consoleRef: RefObject; -}; - -export const ConsoleOutputWindow: FunctionComponent< - ConsoleOutputWindowProps -> = ({ consoleRef }) => { - return
; -}; - -type ImageOutputWindowProps = { - imagesRef: RefObject; -}; - -const ImageOutputWindow: FunctionComponent = ({ - imagesRef, -}) => { - return
; -}; - -export default AnalysisWindow; diff --git a/gui/src/app/pages/HomePage/DataGenerationWindow/DataGenerationWindow.tsx b/gui/src/app/pages/HomePage/DataGenerationWindow/DataGenerationWindow.tsx deleted file mode 100644 index 150181bb..00000000 --- a/gui/src/app/pages/HomePage/DataGenerationWindow/DataGenerationWindow.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { FunctionComponent, useCallback, useContext, useRef } from "react"; -import DataPyFileEditor from "./DataPyFileEditor"; -import DataRFileEditor from "./DataRFileEditor"; -import { writeConsoleOutToDiv } from "@SpPyodide/AnalysisPyFileEditor"; -import { SplitDirection, Splitter } from "@SpComponents/Splitter"; -import TabWidget from "@SpComponents/TabWidget"; -import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; -import { ProjectContext } from "@SpCore/ProjectContextProvider"; -import { FileNames } from "@SpCore/FileMapping"; -import { ConsoleOutputWindow } from "@SpPages/AnalysisWindow/AnalysisWindow"; - -type DataGenerationWindowProps = { - // empty -}; - -type Language = "python" | "r"; - -const DataGenerationWindow: FunctionComponent< - DataGenerationWindowProps -> = () => { - return ( - - - - - ); -}; - -type DataGenerationChildWindowProps = { - language: Language; -}; - -const DataGenerationChildWindow: FunctionComponent< - DataGenerationChildWindowProps -> = ({ language }) => { - const { data, update } = useContext(ProjectContext); - - const consoleRef = useRef(null); - - const handleSetData = useCallback( - (newData: unknown) => { - const dataJson = JSON.stringify(newData, null, 2); - - if (dataJson !== data.dataFileContent) { - update({ - type: "editFile", - content: dataJson, - filename: ProjectKnownFiles.DATAFILE, - }); - update({ type: "commitFile", filename: ProjectKnownFiles.DATAFILE }); - // Use "stan-playground" prefix to distinguish from console output of the running code - writeConsoleOutToDiv( - consoleRef, - "[stan-playground] Data updated", - "stdout", - ); - } else { - writeConsoleOutToDiv( - consoleRef, - "[stan-playground] Data unchanged", - "stdout", - ); - } - }, - [update, consoleRef, data.dataFileContent], - ); - - const EditorComponent = - language === "python" ? DataPyFileEditor : DataRFileEditor; - return ( - - { - update({ - type: "commitFile", - filename: - language === "python" - ? ProjectKnownFiles.DATAPYFILE - : ProjectKnownFiles.DATARFILE, - }); - }} - editedFileContent={ - language === "python" - ? data.ephemera.dataPyFileContent - : data.ephemera.dataRFileContent - } - setEditedFileContent={(content) => { - update({ - type: "editFile", - content, - filename: - language === "python" - ? ProjectKnownFiles.DATAPYFILE - : ProjectKnownFiles.DATARFILE, - }); - }} - readOnly={false} - setData={handleSetData} - outputDiv={consoleRef} - /> - - - ); -}; - -export default DataGenerationWindow; diff --git a/gui/src/app/pages/HomePage/DataGenerationWindow/DataPyFileEditor.tsx b/gui/src/app/pages/HomePage/DataGenerationWindow/DataPyFileEditor.tsx deleted file mode 100644 index eed5aa0f..00000000 --- a/gui/src/app/pages/HomePage/DataGenerationWindow/DataPyFileEditor.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import TextEditor, { ToolbarItem } from "@SpComponents/TextEditor"; -import { - FunctionComponent, - RefObject, - useCallback, - useMemo, - useState, -} from "react"; -import getDataGenerationToolbarItems from "./getDataGenerationToolbarItems"; -import { PyodideWorkerStatus } from "@SpPyodide/pyodideWorker/pyodideWorkerTypes"; -import { writeConsoleOutToDiv } from "@SpPyodide/AnalysisPyFileEditor"; -import usePyodideWorker from "@SpPyodide/pyodideWorker/usePyodideWorker"; - -type Props = { - fileName: string; - fileContent: string; - onSaveContent: () => void; - editedFileContent: string; - setEditedFileContent: (text: string) => void; - readOnly: boolean; - setData?: (data: any) => void; - outputDiv: RefObject; -}; - -const DataPyFileEditor: FunctionComponent = ({ - fileName, - fileContent, - onSaveContent, - editedFileContent, - setEditedFileContent, - setData, - readOnly, - outputDiv, -}) => { - const [status, setStatus] = useState("idle"); - - const callbacks = useMemo( - () => ({ - onStdout: (x: string) => writeConsoleOutToDiv(outputDiv, x, "stdout"), - onStderr: (x: string) => writeConsoleOutToDiv(outputDiv, x, "stderr"), - onStatus: (status: PyodideWorkerStatus) => { - setStatus(status); - }, - onData: setData, - }), - [outputDiv, setData], - ); - - const { run } = usePyodideWorker(callbacks); - - const handleRun = useCallback(async () => { - if (status === "running") { - return; - } - if (editedFileContent !== fileContent) { - throw new Error("Cannot run edited code"); - } - if (outputDiv.current) { - outputDiv.current.innerHTML = ""; - } - run( - fileContent, - {}, - { - loadsDraws: false, - showsPlots: false, - producesData: true, - }, - ); - }, [editedFileContent, fileContent, status, run, outputDiv]); - - const handleHelp = useCallback(() => { - alert( - 'Write a Python script to assign data to the "data" variable and then click "Run" to generate data.', - ); - }, []); - - const toolbarItems: ToolbarItem[] = useMemo( - () => - getDataGenerationToolbarItems({ - name: "pyodide", - status, - runnable: fileContent === editedFileContent, - onRun: handleRun, - onHelp: handleHelp, - }), - [fileContent, editedFileContent, handleRun, status, handleHelp], - ); - - const contentOnEmpty = useMemo(() => { - const spanElement = document.createElement("span"); - const t1 = document.createTextNode( - "Define a dictionary called data to update the data.json. ", - ); - const a1 = document.createElement("a"); - a1.onclick = () => { - setEditedFileContent(dataPyTemplate); - }; - a1.textContent = "Click here to generate an example"; - spanElement.appendChild(t1); - spanElement.appendChild(a1); - return spanElement; - }, [setEditedFileContent]); - - return ( - - ); -}; - -const dataPyTemplate = `data = { - "a": [1, 2, 3] -}`; - -export default DataPyFileEditor; diff --git a/gui/src/app/pages/HomePage/DataGenerationWindow/DataRFileEditor.tsx b/gui/src/app/pages/HomePage/DataGenerationWindow/DataRFileEditor.tsx deleted file mode 100644 index 8c4dbfa3..00000000 --- a/gui/src/app/pages/HomePage/DataGenerationWindow/DataRFileEditor.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { - FunctionComponent, - RefObject, - useCallback, - useMemo, - useState, -} from "react"; -import { RString, WebR } from "webr"; -import getDataGenerationToolbarItems from "./getDataGenerationToolbarItems"; -import TextEditor, { ToolbarItem } from "@SpComponents/TextEditor"; -import { writeConsoleOutToDiv } from "@SpPyodide/AnalysisPyFileEditor"; - -type Props = { - fileName: string; - fileContent: string; - onSaveContent: () => void; - editedFileContent: string; - setEditedFileContent: (text: string) => void; - readOnly: boolean; - setData?: (data: any) => void; - outputDiv: RefObject; -}; - -let webR: WebR | null = null; -export const loadWebRInstance = async () => { - if (webR === null) { - const w = new WebR(); - await w.init(); - w.installPackages(["jsonlite"]); - webR = w; - return webR; - } else { - return webR; - } -}; - -const DataRFileEditor: FunctionComponent = ({ - fileName, - fileContent, - onSaveContent, - editedFileContent, - setEditedFileContent, - setData, - readOnly, - outputDiv, -}) => { - const [status, setStatus] = useState< - "idle" | "loading" | "running" | "completed" | "failed" - >("idle"); - const handleRun = useCallback(async () => { - if (status === "running") { - return; - } - if (editedFileContent !== fileContent) { - throw new Error("Cannot run edited code"); - } - if (outputDiv.current) { - outputDiv.current.innerHTML = ""; - } - setStatus("loading"); - try { - const webR = await loadWebRInstance(); - - const shelter = await new webR.Shelter(); - - setStatus("running"); - const rCode = - ` -# redirect install.packages to webr's version -webr::shim_install() -\n\n -` + - fileContent + - "\n\n" + - ` -if (typeof(data) != "list") { - stop("[stan-playground] data must be a list") -} -.SP_DATA <- jsonlite::toJSON(data, pretty = TRUE, auto_unbox = TRUE) -.SP_DATA - `; - - try { - const ret = await shelter.captureR(rCode, { - env: {}, - }); - ret.output.forEach(({ type, data }) => { - if (type === "stdout" || type === "stderr") { - writeConsoleOutToDiv(outputDiv, data, type); - } - }); - - const result = JSON.parse(await (ret.result as RString).toString()); - if (setData) { - setData(result); - } - } finally { - shelter.purge(); - } - setStatus("completed"); - } catch (e: any) { - console.error(e); - writeConsoleOutToDiv(outputDiv, e.toString(), "stderr"); - setStatus("failed"); - } - }, [editedFileContent, fileContent, status, setData, outputDiv]); - - const handleHelp = useCallback(() => { - alert( - 'Write an R script to assign data to the "data" variable and then click "Run" to generate data.', - ); - }, []); - - const toolbarItems: ToolbarItem[] = useMemo( - () => - getDataGenerationToolbarItems({ - name: "WebR", - status, - runnable: fileContent === editedFileContent, - onRun: handleRun, - onHelp: handleHelp, - }), - [fileContent, editedFileContent, handleRun, status, handleHelp], - ); - - const contentOnEmpty = useMemo(() => { - const spanElement = document.createElement("span"); - const t1 = document.createTextNode( - "Define a list called data to update the data.json. ", - ); - const a1 = document.createElement("a"); - a1.onclick = () => { - setEditedFileContent(dataRTemplate); - }; - a1.textContent = "Click here to generate an example"; - spanElement.appendChild(t1); - spanElement.appendChild(a1); - return spanElement; - }, [setEditedFileContent]); - - return ( - - ); -}; - -const dataRTemplate = `data <- list( - a = c(1, 2, 3) -)`; - -export default DataRFileEditor; diff --git a/gui/src/app/pages/HomePage/DataGenerationWindow/getDataGenerationToolbarItems.tsx b/gui/src/app/pages/HomePage/DataGenerationWindow/getDataGenerationToolbarItems.tsx deleted file mode 100644 index 2e0e6c6e..00000000 --- a/gui/src/app/pages/HomePage/DataGenerationWindow/getDataGenerationToolbarItems.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { ToolbarItem } from "@SpComponents/TextEditor"; -import { PyodideWorkerStatus } from "@SpPyodide/pyodideWorker/pyodideWorkerTypes"; -import { Help, PlayArrow } from "@mui/icons-material"; - -const getDataGenerationToolbarItems = (o: { - status: PyodideWorkerStatus; - name: string; - runnable: boolean; - onRun: () => void; - onHelp: () => void; -}): ToolbarItem[] => { - const { status, onRun, runnable, onHelp, name } = o; - const ret: ToolbarItem[] = []; - ret.push({ - type: "button", - tooltip: "Help", - icon: , - onClick: onHelp, - }); - if (runnable) { - ret.push({ - type: "button", - tooltip: "Run code to generate data", - label: "Run", - icon: , - onClick: onRun, - color: "black", - }); - } - let label: string; - let color: string; - if (status === "loading") { - label = `Loading ${name}...`; - color = "blue"; - } else if (status === "running") { - label = "Running..."; - color = "blue"; - } else if (status === "completed") { - label = "Completed"; - color = "green"; - } else if (status === "failed") { - label = "Failed"; - color = "red"; - } else { - label = ""; - color = "black"; - } - - if (label) { - ret.push({ - type: "text", - label, - color, - }); - } - return ret; -}; - -export default getDataGenerationToolbarItems; diff --git a/gui/src/app/pages/HomePage/HomePage.tsx b/gui/src/app/pages/HomePage/HomePage.tsx index 61b0c204..17186818 100644 --- a/gui/src/app/pages/HomePage/HomePage.tsx +++ b/gui/src/app/pages/HomePage/HomePage.tsx @@ -20,8 +20,9 @@ import { } from "react"; import TabWidget from "@SpComponents/TabWidget"; import SamplingWindow from "./SamplingWindow/SamplingWindow"; -import DataGenerationWindow from "./DataGenerationWindow/DataGenerationWindow"; import { FileNames } from "@SpCore/FileMapping"; +import DataPyWindow from "app/Scripting/DataGenerationWindow/DataPyWindow"; +import DataRWindow from "app/Scripting/DataGenerationWindow/DataRWindow"; type Props = { // @@ -85,7 +86,10 @@ const RightView: FunctionComponent = ({ return ( - + + + + ); }; diff --git a/gui/src/app/pages/HomePage/SamplingWindow/SamplingWindow.tsx b/gui/src/app/pages/HomePage/SamplingWindow/SamplingWindow.tsx index 08cad336..1467f7b0 100644 --- a/gui/src/app/pages/HomePage/SamplingWindow/SamplingWindow.tsx +++ b/gui/src/app/pages/HomePage/SamplingWindow/SamplingWindow.tsx @@ -12,8 +12,9 @@ import { modelHasUnsavedDataFileChanges, SamplingOpts, } from "@SpCore/ProjectDataModel"; -import AnalysisWindow from "@SpPages/AnalysisWindow/AnalysisWindow"; +import AnalysisPyWindow from "app/Scripting/AnalysisWindow/AnalysisPyWindow"; import useStanSampler, { StanRun } from "@SpStanSampler/useStanSampler"; +import AnalysisRWindow from "app/Scripting/AnalysisWindow/AnalysisRWindow"; type SamplingWindowProps = { compiledMainJsUrl?: string; @@ -78,8 +79,8 @@ const SamplingResultsArea: FunctionComponent = ({ - - + + ); }; diff --git a/gui/src/app/pyodide/AnalysisPyFileEditor.tsx b/gui/src/app/pyodide/AnalysisPyFileEditor.tsx deleted file mode 100644 index 57bb947f..00000000 --- a/gui/src/app/pyodide/AnalysisPyFileEditor.tsx +++ /dev/null @@ -1,225 +0,0 @@ -import { - FunctionComponent, - RefObject, - useCallback, - useEffect, - useMemo, - useState, -} from "react"; -import { PlayArrow } from "@mui/icons-material"; -import { PyodideWorkerStatus } from "./pyodideWorker/pyodideWorkerTypes"; -import usePyodideWorker from "./pyodideWorker/usePyodideWorker"; -import TextEditor, { ToolbarItem } from "@SpComponents/TextEditor"; -import { GlobalDataForAnalysis } from "@SpPages/AnalysisWindow/AnalysisWindow"; - -type Props = { - fileName: string; - fileContent: string; - onSaveContent: () => void; - editedFileContent: string; - setEditedFileContent: (text: string) => void; - readOnly: boolean; - imagesRef: RefObject; - consoleRef: RefObject; - spData: GlobalDataForAnalysis | undefined; - scriptHeader?: string; -}; - -const AnalysisPyFileEditor: FunctionComponent = ({ - fileName, - fileContent, - onSaveContent, - editedFileContent, - setEditedFileContent, - readOnly, - imagesRef, - consoleRef, - spData, -}) => { - const [status, setStatus] = useState("idle"); - - const callbacks = useMemo( - () => ({ - onStdout: (x: string) => writeConsoleOutToDiv(consoleRef, x, "stdout"), - onStderr: (x: string) => writeConsoleOutToDiv(consoleRef, x, "stderr"), - onImage: (b64: string) => { - const imageUrl = `data:image/png;base64,${b64}`; - - const img = document.createElement("img"); - img.style.width = "100%"; - img.src = imageUrl; - - const divElement = document.createElement("div"); - divElement.appendChild(img); - imagesRef.current?.appendChild(divElement); - }, - onStatus: (status: PyodideWorkerStatus) => { - setStatus(status); - }, - }), - [consoleRef, imagesRef], - ); - - const { run } = usePyodideWorker(callbacks); - - const hasData = useMemo(() => { - return spData !== undefined; - }, [spData]); - - useEffect(() => { - setStatus("idle"); - }, [spData, fileContent]); - - const handleRun = useCallback(() => { - if (status === "running") { - return; - } - if (editedFileContent !== fileContent) { - throw new Error("Cannot run edited code"); - } - - if (consoleRef.current) { - consoleRef.current.innerHTML = ""; - } - if (imagesRef.current) { - imagesRef.current.innerHTML = ""; - } - run(fileContent, spData, { - loadsDraws: true, - showsPlots: true, - producesData: false, - }); - }, [ - status, - editedFileContent, - fileContent, - consoleRef, - imagesRef, - run, - spData, - ]); - const toolbarItems: ToolbarItem[] = useMemo(() => { - const ret: ToolbarItem[] = []; - const runnable = - fileContent === editedFileContent && imagesRef.current && hasData; - if (runnable) { - ret.push({ - type: "button", - tooltip: "Run script", - label: "Run", - icon: , - onClick: handleRun, - color: "black", - }); - } - if (!hasData) { - ret.push({ - type: "text", - label: "Run sampler first", - color: "red", - }); - } - if (!imagesRef.current) { - ret.push({ - type: "text", - label: "No output window", - color: "red", - }); - } - let label: string; - let color: string; - if (status === "loading") { - label = "Loading pyodide..."; - color = "blue"; - } else if (status === "running") { - label = "Running..."; - color = "blue"; - } else if (status === "installing") { - label = "Installing packages..."; - color = "blue"; - } else if (status === "completed") { - label = "Completed"; - color = "green"; - } else if (status === "failed") { - label = "Failed"; - color = "red"; - } else { - label = ""; - color = "black"; - } - - if (label) { - ret.push({ - type: "text", - label, - color, - }); - } - return ret; - }, [fileContent, editedFileContent, imagesRef, hasData, status, handleRun]); - - const contentOnEmpty = useMemo(() => { - const spanElement = document.createElement("span"); - const t1 = document.createTextNode( - "Use the draws object to access the samples. ", - ); - const a1 = document.createElement("a"); - a1.onclick = () => { - setEditedFileContent(analysisPyTemplate); - }; - a1.textContent = "Click here to generate an example"; - spanElement.appendChild(t1); - spanElement.appendChild(a1); - return spanElement; - }, [setEditedFileContent]); - - return ( - - ); -}; - -const analysisPyTemplate = `import matplotlib.pyplot as plt - -# Print the draws object -print(draws) - -# Print parameter names -print(draws.parameter_names) - -# plot the lp parameter -samples = draws.get("lp__") -print(samples.shape) -plt.hist(samples.ravel(), bins=30) -plt.title("lp__") -plt.show() -`; - -type ConsoleOutType = "stdout" | "stderr"; - -export const writeConsoleOutToDiv = ( - parentDiv: RefObject, - x: string, - type: ConsoleOutType, -) => { - if (x === "") return; - if (!parentDiv.current) return; - const styleClass = type === "stdout" ? "WorkerStdout" : "WorkerStderr"; - const preElement = document.createElement("pre"); - preElement.textContent = x; - const divElement = document.createElement("div"); - divElement.className = styleClass; - divElement.appendChild(preElement); - parentDiv.current.appendChild(divElement); -}; - -export default AnalysisPyFileEditor; From 4b1a981d2b353d3f3b1ea958c8d93f55331185c5 Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Fri, 26 Jul 2024 13:46:58 +0000 Subject: [PATCH 04/16] More reorganization --- .../AnalysisWindow/AnalysisPyWindow.tsx | 39 +++------- .../AnalysisWindow/AnalysisRWindow.tsx | 36 +++------ .../AnalysisWindow/useAnalysisState.ts | 2 +- .../DataGenerationWindow/DataPyWindow.tsx | 76 +++---------------- .../DataGenerationWindow/DataRWindow.tsx | 76 ++++--------------- .../DataGenerationWindow/useDataGenState.ts | 44 +++++++++++ .../pyodideWorker.ts | 0 .../pyodideWorkerTypes.ts | 0 .../sp_load_draws.py | 0 .../sp_patch_matplotlib.py | 0 .../usePyodideWorker.ts | 0 .../app/Scripting/useTemplatedFillerText.ts | 32 ++++++++ gui/src/app/Scripting/{ => webR}/runR.ts | 26 +++---- 13 files changed, 137 insertions(+), 194 deletions(-) create mode 100644 gui/src/app/Scripting/DataGenerationWindow/useDataGenState.ts rename gui/src/app/Scripting/{pyodideWorker => pyodide}/pyodideWorker.ts (100%) rename gui/src/app/Scripting/{pyodideWorker => pyodide}/pyodideWorkerTypes.ts (100%) rename gui/src/app/Scripting/{pyodideWorker => pyodide}/sp_load_draws.py (100%) rename gui/src/app/Scripting/{pyodideWorker => pyodide}/sp_patch_matplotlib.py (100%) rename gui/src/app/Scripting/{pyodideWorker => pyodide}/usePyodideWorker.ts (100%) create mode 100644 gui/src/app/Scripting/useTemplatedFillerText.ts rename gui/src/app/Scripting/{ => webR}/runR.ts (86%) diff --git a/gui/src/app/Scripting/AnalysisWindow/AnalysisPyWindow.tsx b/gui/src/app/Scripting/AnalysisWindow/AnalysisPyWindow.tsx index e40d3f5d..b21f63c3 100644 --- a/gui/src/app/Scripting/AnalysisWindow/AnalysisPyWindow.tsx +++ b/gui/src/app/Scripting/AnalysisWindow/AnalysisPyWindow.tsx @@ -1,13 +1,12 @@ -import { FunctionComponent, useCallback, useContext, useMemo } from "react"; +import { FunctionComponent, useCallback, useMemo } from "react"; import { StanRun } from "@SpStanSampler/useStanSampler"; import { FileNames } from "@SpCore/FileMapping"; import PlottingScriptEditor from "app/Scripting/PlottingScriptEditor"; -import { InterpreterStatus } from "app/Scripting/InterpreterTypes"; import { writeConsoleOutToDiv } from "app/Scripting/ScriptEditor"; -import usePyodideWorker from "app/Scripting/pyodideWorker/usePyodideWorker"; -import { ProjectContext } from "@SpCore/ProjectContextProvider"; +import usePyodideWorker from "app/Scripting/pyodide/usePyodideWorker"; import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; import useAnalysisState from "./useAnalysisState"; +import useTemplatedFillerText from "../useTemplatedFillerText"; export type GlobalDataForAnalysis = { draws: number[][]; @@ -22,7 +21,7 @@ type AnalysisWindowProps = { const AnalysisPyWindow: FunctionComponent = ({ latestRun, }) => { - const { consoleRef, imagesRef, spData, status, setStatus } = + const { consoleRef, imagesRef, spData, status, onStatus } = useAnalysisState(latestRun); const callbacks = useMemo( @@ -40,11 +39,9 @@ const AnalysisPyWindow: FunctionComponent = ({ divElement.appendChild(img); imagesRef.current?.appendChild(divElement); }, - onStatus: (status: InterpreterStatus) => { - setStatus(status); - }, + onStatus, }), - [consoleRef, imagesRef, setStatus], + [consoleRef, imagesRef, onStatus], ); const { run } = usePyodideWorker(callbacks); @@ -74,25 +71,11 @@ const AnalysisPyWindow: FunctionComponent = ({ [status, consoleRef, imagesRef, run, spData], ); - const { update } = useContext(ProjectContext); - const contentOnEmpty = useMemo(() => { - const spanElement = document.createElement("span"); - const t1 = document.createTextNode( - "Use the draws object to access the samples. ", - ); - const a1 = document.createElement("a"); - a1.onclick = () => { - update({ - type: "editFile", - filename: ProjectKnownFiles.ANALYSISPYFILE, - content: analysisPyTemplate, - }); - }; - a1.textContent = "Click here to generate an example"; - spanElement.appendChild(t1); - spanElement.appendChild(a1); - return spanElement; - }, [update]); + const contentOnEmpty = useTemplatedFillerText( + "Use the draws object to access the samples. ", + analysisPyTemplate, + ProjectKnownFiles.ANALYSISPYFILE, + ); return ( = ({ latestRun, }) => { - const { consoleRef, imagesRef, spData, status, setStatus } = + const { consoleRef, imagesRef, spData, status, onStatus } = useAnalysisState(latestRun); const handleRun = useCallback( async (code: string) => { - await runR({ code, imagesRef, consoleRef, setStatus }); + await runR({ code, imagesRef, consoleRef, onStatus }); }, - [consoleRef, imagesRef, setStatus], + [consoleRef, imagesRef, onStatus], ); const runnable = useMemo(() => { return spData !== undefined && status !== "running"; }, [spData, status]); - const { update } = useContext(ProjectContext); - const contentOnEmpty = useMemo(() => { - const spanElement = document.createElement("span"); - const t1 = document.createTextNode( - "Use the draws object to access the samples. ", - ); - const a1 = document.createElement("a"); - a1.onclick = () => { - update({ - type: "editFile", - filename: ProjectKnownFiles.ANALYSISRFILE, - content: analysisRTemplate, - }); - }; - a1.textContent = "Click here to generate an example"; - spanElement.appendChild(t1); - spanElement.appendChild(a1); - return spanElement; - }, [update]); + const contentOnEmpty = useTemplatedFillerText( + "Use the draws object to access the samples. ", + analysisRTemplate, + ProjectKnownFiles.ANALYSISRFILE, + ); return ( { const [status, setStatus] = useState("idle"); - return { consoleRef, imagesRef, spData, status, setStatus }; + return { consoleRef, imagesRef, spData, status, onStatus: setStatus }; }; export default useAnalysisState; diff --git a/gui/src/app/Scripting/DataGenerationWindow/DataPyWindow.tsx b/gui/src/app/Scripting/DataGenerationWindow/DataPyWindow.tsx index 7751a54e..2a902c65 100644 --- a/gui/src/app/Scripting/DataGenerationWindow/DataPyWindow.tsx +++ b/gui/src/app/Scripting/DataGenerationWindow/DataPyWindow.tsx @@ -1,65 +1,26 @@ -import { - FunctionComponent, - useCallback, - useContext, - useMemo, - useRef, - useState, -} from "react"; -import usePyodideWorker from "app/Scripting/pyodideWorker/usePyodideWorker"; +import { FunctionComponent, useCallback, useMemo } from "react"; +import usePyodideWorker from "app/Scripting/pyodide/usePyodideWorker"; import ScriptEditor, { writeConsoleOutToDiv } from "app/Scripting/ScriptEditor"; import { FileNames } from "@SpCore/FileMapping"; import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; -import { ProjectContext } from "@SpCore/ProjectContextProvider"; -import { InterpreterStatus } from "../InterpreterTypes"; +import useDataGenState from "./useDataGenState"; +import useTemplatedFillerText from "../useTemplatedFillerText"; type Props = { // empty }; const DataPyWindow: FunctionComponent = () => { - const [status, setStatus] = useState("idle"); - const consoleRef = useRef(null); - const { data, update } = useContext(ProjectContext); - - const onData = useCallback( - (newData: unknown) => { - const dataJson = JSON.stringify(newData, null, 2); - - if (dataJson !== data.dataFileContent) { - update({ - type: "editFile", - content: dataJson, - filename: ProjectKnownFiles.DATAFILE, - }); - update({ type: "commitFile", filename: ProjectKnownFiles.DATAFILE }); - // Use "stan-playground" prefix to distinguish from console output of the running code - writeConsoleOutToDiv( - consoleRef, - "[stan-playground] Data updated", - "stdout", - ); - } else { - writeConsoleOutToDiv( - consoleRef, - "[stan-playground] Data unchanged", - "stdout", - ); - } - }, - [update, consoleRef, data.dataFileContent], - ); + const { consoleRef, status, onStatus, onData } = useDataGenState(); const callbacks = useMemo( () => ({ onStdout: (x: string) => writeConsoleOutToDiv(consoleRef, x, "stdout"), onStderr: (x: string) => writeConsoleOutToDiv(consoleRef, x, "stderr"), - onStatus: (status: InterpreterStatus) => { - setStatus(status); - }, + onStatus, onData, }), - [onData], + [consoleRef, onData, onStatus], ); const { run } = usePyodideWorker(callbacks); @@ -85,24 +46,11 @@ const DataPyWindow: FunctionComponent = () => { ); }, []); - const contentOnEmpty = useMemo(() => { - const spanElement = document.createElement("span"); - const t1 = document.createTextNode( - "Define a dictionary called data to update the data.json. ", - ); - const a1 = document.createElement("a"); - a1.onclick = () => { - update({ - type: "editFile", - filename: ProjectKnownFiles.DATAPYFILE, - content: dataPyTemplate, - }); - }; - a1.textContent = "Click here to generate an example"; - spanElement.appendChild(t1); - spanElement.appendChild(a1); - return spanElement; - }, [update]); + const contentOnEmpty = useTemplatedFillerText( + "Define a dictionary called data to update the data.json. ", + dataPyTemplate, + ProjectKnownFiles.DATAPYFILE, + ); return ( = () => { - const [status, setStatus] = useState("idle"); - const consoleRef = useRef(null); - const { data, update } = useContext(ProjectContext); - - const setData = useCallback( - (newData: unknown) => { - const dataJson = JSON.stringify(newData, null, 2); - - if (dataJson !== data.dataFileContent) { - update({ - type: "editFile", - content: dataJson, - filename: ProjectKnownFiles.DATAFILE, - }); - update({ type: "commitFile", filename: ProjectKnownFiles.DATAFILE }); - // Use "stan-playground" prefix to distinguish from console output of the running code - writeConsoleOutToDiv( - consoleRef, - "[stan-playground] Data updated", - "stdout", - ); - } else { - writeConsoleOutToDiv( - consoleRef, - "[stan-playground] Data unchanged", - "stdout", - ); - } - }, - [update, consoleRef, data.dataFileContent], - ); + const { consoleRef, status, onStatus, onData } = useDataGenState(); const handleRun = useCallback( async (code: string) => { - await runR({ code, consoleRef, setStatus, setData }); + await runR({ code, consoleRef, onStatus, onData }); }, - [setData], + [consoleRef, onData, onStatus], ); const handleHelp = useCallback(() => { @@ -63,24 +26,11 @@ const DataRWindow: FunctionComponent = () => { ); }, []); - const contentOnEmpty = useMemo(() => { - const spanElement = document.createElement("span"); - const t1 = document.createTextNode( - "Define a dictionary called data to update the data.json. ", - ); - const a1 = document.createElement("a"); - a1.onclick = () => { - update({ - type: "editFile", - filename: ProjectKnownFiles.DATARFILE, - content: dataRTemplate, - }); - }; - a1.textContent = "Click here to generate an example"; - spanElement.appendChild(t1); - spanElement.appendChild(a1); - return spanElement; - }, [update]); + const contentOnEmpty = useTemplatedFillerText( + "Define a dictionary called data to update the data.json. ", + dataRTemplate, + ProjectKnownFiles.DATARFILE, + ); return ( { + const [status, setStatus] = useState("idle"); + const consoleRef = useRef(null); + + const { data, update } = useContext(ProjectContext); + + const onData = useCallback( + (newData: unknown) => { + const dataJson = JSON.stringify(newData, null, 2); + + if (dataJson !== data.dataFileContent) { + update({ + type: "editFile", + content: dataJson, + filename: ProjectKnownFiles.DATAFILE, + }); + update({ type: "commitFile", filename: ProjectKnownFiles.DATAFILE }); + // Use "stan-playground" prefix to distinguish from console output of the running code + writeConsoleOutToDiv( + consoleRef, + "[stan-playground] Data updated", + "stdout", + ); + } else { + writeConsoleOutToDiv( + consoleRef, + "[stan-playground] Data unchanged", + "stdout", + ); + } + }, + [update, consoleRef, data.dataFileContent], + ); + + return { consoleRef, status, onStatus: setStatus, onData }; +}; + +export default useDataGenState; diff --git a/gui/src/app/Scripting/pyodideWorker/pyodideWorker.ts b/gui/src/app/Scripting/pyodide/pyodideWorker.ts similarity index 100% rename from gui/src/app/Scripting/pyodideWorker/pyodideWorker.ts rename to gui/src/app/Scripting/pyodide/pyodideWorker.ts diff --git a/gui/src/app/Scripting/pyodideWorker/pyodideWorkerTypes.ts b/gui/src/app/Scripting/pyodide/pyodideWorkerTypes.ts similarity index 100% rename from gui/src/app/Scripting/pyodideWorker/pyodideWorkerTypes.ts rename to gui/src/app/Scripting/pyodide/pyodideWorkerTypes.ts diff --git a/gui/src/app/Scripting/pyodideWorker/sp_load_draws.py b/gui/src/app/Scripting/pyodide/sp_load_draws.py similarity index 100% rename from gui/src/app/Scripting/pyodideWorker/sp_load_draws.py rename to gui/src/app/Scripting/pyodide/sp_load_draws.py diff --git a/gui/src/app/Scripting/pyodideWorker/sp_patch_matplotlib.py b/gui/src/app/Scripting/pyodide/sp_patch_matplotlib.py similarity index 100% rename from gui/src/app/Scripting/pyodideWorker/sp_patch_matplotlib.py rename to gui/src/app/Scripting/pyodide/sp_patch_matplotlib.py diff --git a/gui/src/app/Scripting/pyodideWorker/usePyodideWorker.ts b/gui/src/app/Scripting/pyodide/usePyodideWorker.ts similarity index 100% rename from gui/src/app/Scripting/pyodideWorker/usePyodideWorker.ts rename to gui/src/app/Scripting/pyodide/usePyodideWorker.ts diff --git a/gui/src/app/Scripting/useTemplatedFillerText.ts b/gui/src/app/Scripting/useTemplatedFillerText.ts new file mode 100644 index 00000000..1d7e92b4 --- /dev/null +++ b/gui/src/app/Scripting/useTemplatedFillerText.ts @@ -0,0 +1,32 @@ +import { ProjectContext } from "@SpCore/ProjectContextProvider"; +import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; +import { useContext, useMemo } from "react"; + +const useTemplatedFillerText = ( + text: string, + template: string, + file: ProjectKnownFiles, +) => { + const { update } = useContext(ProjectContext); + + const contentOnEmpty = useMemo(() => { + const spanElement = document.createElement("span"); + const t1 = document.createTextNode(text); + const a1 = document.createElement("a"); + a1.onclick = () => { + update({ + type: "editFile", + filename: file, + content: template, + }); + }; + a1.textContent = "Click here to generate an example"; + spanElement.appendChild(t1); + spanElement.appendChild(a1); + return spanElement; + }, [file, template, text, update]); + + return contentOnEmpty; +}; + +export default useTemplatedFillerText; diff --git a/gui/src/app/Scripting/runR.ts b/gui/src/app/Scripting/webR/runR.ts similarity index 86% rename from gui/src/app/Scripting/runR.ts rename to gui/src/app/Scripting/webR/runR.ts index e9ad2052..364639d2 100644 --- a/gui/src/app/Scripting/runR.ts +++ b/gui/src/app/Scripting/webR/runR.ts @@ -1,7 +1,7 @@ import { RefObject } from "react"; import { RString, WebR } from "webr"; -import { InterpreterStatus } from "./InterpreterTypes"; -import { writeConsoleOutToDiv } from "./ScriptEditor"; +import { InterpreterStatus } from "../InterpreterTypes"; +import { writeConsoleOutToDiv } from "../ScriptEditor"; let webR: WebR | null = null; export const loadWebRInstance = async () => { @@ -20,16 +20,16 @@ type RunRProps = { code: string; consoleRef: RefObject; imagesRef?: RefObject; - setStatus: (status: InterpreterStatus) => void; - setData?: (data: any) => void; + onStatus: (status: InterpreterStatus) => void; + onData?: (data: any) => void; }; const runR = async ({ code, imagesRef, consoleRef, - setStatus, - setData, + onStatus, + onData, }: RunRProps) => { const captureOutputOptions: any = { withAutoprint: true, @@ -46,13 +46,13 @@ const runR = async ({ }; try { - setStatus("loading"); + onStatus("loading"); await sleep(100); // let the UI update const webR = await loadWebRInstance(); const shelter = await new webR.Shelter(); - setStatus("running"); + onStatus("running"); await sleep(100); // let the UI update let rCode = ` @@ -61,7 +61,7 @@ webr::shim_install() ` + code; - if (setData) { + if (onData) { rCode += ` if (typeof(data) != "list") { stop("[stan-playground] data must be a list") @@ -93,18 +93,18 @@ stop("[stan-playground] data must be a list") } }); - if (setData) { + if (onData) { const result = JSON.parse(await (ret.result as RString).toString()); - setData(result); + onData(result); } } finally { shelter.purge(); } - setStatus("completed"); + onStatus("completed"); } catch (e: any) { console.error(e); writeConsoleOutToDiv(consoleRef, e.toString(), "stderr"); - setStatus("failed"); + onStatus("failed"); } }; From 4a4c4e5b2946543fd94a2f4917acc3a7d395168c Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Fri, 26 Jul 2024 14:02:55 +0000 Subject: [PATCH 05/16] Restore "Run sampler first" message --- .../AnalysisWindow/AnalysisPyWindow.tsx | 17 ++++++----- .../AnalysisWindow/AnalysisRWindow.tsx | 25 +++++++++++----- .../AnalysisWindow/useAnalysisState.ts | 30 +++++++++++++++++-- gui/src/app/Scripting/InterpreterTypes.ts | 4 +++ 4 files changed, 59 insertions(+), 17 deletions(-) diff --git a/gui/src/app/Scripting/AnalysisWindow/AnalysisPyWindow.tsx b/gui/src/app/Scripting/AnalysisWindow/AnalysisPyWindow.tsx index b21f63c3..99fefc81 100644 --- a/gui/src/app/Scripting/AnalysisWindow/AnalysisPyWindow.tsx +++ b/gui/src/app/Scripting/AnalysisWindow/AnalysisPyWindow.tsx @@ -21,8 +21,15 @@ type AnalysisWindowProps = { const AnalysisPyWindow: FunctionComponent = ({ latestRun, }) => { - const { consoleRef, imagesRef, spData, status, onStatus } = - useAnalysisState(latestRun); + const { + consoleRef, + imagesRef, + spData, + status, + onStatus, + runnable, + notRunnableReason, + } = useAnalysisState(latestRun); const callbacks = useMemo( () => ({ @@ -46,10 +53,6 @@ const AnalysisPyWindow: FunctionComponent = ({ const { run } = usePyodideWorker(callbacks); - const runnable = useMemo(() => { - return spData !== undefined && status !== "running"; - }, [spData, status]); - const handleRun = useCallback( (code: string) => { if (status === "running") { @@ -85,7 +88,7 @@ const AnalysisPyWindow: FunctionComponent = ({ status={status} onRun={handleRun} runnable={runnable} - notRunnableReason="" + notRunnableReason={notRunnableReason} onHelp={() => {}} imagesRef={imagesRef} consoleRef={consoleRef} diff --git a/gui/src/app/Scripting/AnalysisWindow/AnalysisRWindow.tsx b/gui/src/app/Scripting/AnalysisWindow/AnalysisRWindow.tsx index fcb5bb63..19758146 100644 --- a/gui/src/app/Scripting/AnalysisWindow/AnalysisRWindow.tsx +++ b/gui/src/app/Scripting/AnalysisWindow/AnalysisRWindow.tsx @@ -1,4 +1,10 @@ -import { FunctionComponent, useCallback, useMemo } from "react"; +import { + FunctionComponent, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; import { StanRun } from "@SpStanSampler/useStanSampler"; import { FileNames } from "@SpCore/FileMapping"; import PlottingScriptEditor from "app/Scripting/PlottingScriptEditor"; @@ -14,8 +20,15 @@ type AnalysisWindowProps = { const AnalysisRWindow: FunctionComponent = ({ latestRun, }) => { - const { consoleRef, imagesRef, spData, status, onStatus } = - useAnalysisState(latestRun); + const { + consoleRef, + imagesRef, + spData, + status, + onStatus, + runnable, + notRunnableReason, + } = useAnalysisState(latestRun); const handleRun = useCallback( async (code: string) => { @@ -24,10 +37,6 @@ const AnalysisRWindow: FunctionComponent = ({ [consoleRef, imagesRef, onStatus], ); - const runnable = useMemo(() => { - return spData !== undefined && status !== "running"; - }, [spData, status]); - const contentOnEmpty = useTemplatedFillerText( "Use the draws object to access the samples. ", analysisRTemplate, @@ -42,7 +51,7 @@ const AnalysisRWindow: FunctionComponent = ({ status={status} onRun={handleRun} runnable={runnable} - notRunnableReason="" + notRunnableReason={notRunnableReason} onHelp={() => {}} imagesRef={imagesRef} consoleRef={consoleRef} diff --git a/gui/src/app/Scripting/AnalysisWindow/useAnalysisState.ts b/gui/src/app/Scripting/AnalysisWindow/useAnalysisState.ts index c0c2c04f..1b5ec654 100644 --- a/gui/src/app/Scripting/AnalysisWindow/useAnalysisState.ts +++ b/gui/src/app/Scripting/AnalysisWindow/useAnalysisState.ts @@ -1,6 +1,6 @@ import { StanRun } from "@SpStanSampler/useStanSampler"; import { useEffect, useMemo, useRef, useState } from "react"; -import { InterpreterStatus } from "../InterpreterTypes"; +import { InterpreterStatus, isInterpreterBusy } from "../InterpreterTypes"; export type GlobalDataForAnalysis = { draws: number[][]; @@ -37,7 +37,33 @@ const useAnalysisState = (latestRun: StanRun) => { const [status, setStatus] = useState("idle"); - return { consoleRef, imagesRef, spData, status, onStatus: setStatus }; + const [runnable, setRunnable] = useState(true); + const [notRunnableReason, setNotRunnableReason] = useState(""); + + const isDataDefined = useMemo(() => spData !== undefined, [spData]); + + useEffect(() => { + if (!isDataDefined) { + setRunnable(false); + setNotRunnableReason("Run sampler first."); + } else if (isInterpreterBusy(status)) { + setRunnable(false); + setNotRunnableReason(""); + } else { + setRunnable(true); + setNotRunnableReason(""); + } + }, [isDataDefined, status]); + + return { + consoleRef, + imagesRef, + spData, + status, + onStatus: setStatus, + runnable, + notRunnableReason, + }; }; export default useAnalysisState; diff --git a/gui/src/app/Scripting/InterpreterTypes.ts b/gui/src/app/Scripting/InterpreterTypes.ts index f20d3e42..bf3a60ca 100644 --- a/gui/src/app/Scripting/InterpreterTypes.ts +++ b/gui/src/app/Scripting/InterpreterTypes.ts @@ -16,3 +16,7 @@ export const isInterpreterStatus = (x: any): x is InterpreterStatus => { "failed", ].includes(x); }; + +export const isInterpreterBusy = (status: InterpreterStatus) => { + return ["loading", "installing", "running"].includes(status); +}; From 000c0bc67de92b9c97b4ff14cef409313fd03472 Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Fri, 26 Jul 2024 14:07:38 +0000 Subject: [PATCH 06/16] Move examples to proper files --- .../AnalysisWindow/AnalysisPyWindow.tsx | 18 ++---------------- .../AnalysisWindow/analysis_template.py | 14 ++++++++++++++ .../DataGenerationWindow/DataPyWindow.tsx | 6 ++---- .../DataGenerationWindow/DataRWindow.tsx | 10 ++++------ .../DataGenerationWindow/data_template.R | 1 + .../DataGenerationWindow/data_template.py | 1 + 6 files changed, 24 insertions(+), 26 deletions(-) create mode 100644 gui/src/app/Scripting/AnalysisWindow/analysis_template.py create mode 100644 gui/src/app/Scripting/DataGenerationWindow/data_template.R create mode 100644 gui/src/app/Scripting/DataGenerationWindow/data_template.py diff --git a/gui/src/app/Scripting/AnalysisWindow/AnalysisPyWindow.tsx b/gui/src/app/Scripting/AnalysisWindow/AnalysisPyWindow.tsx index 99fefc81..bbb5f2a7 100644 --- a/gui/src/app/Scripting/AnalysisWindow/AnalysisPyWindow.tsx +++ b/gui/src/app/Scripting/AnalysisWindow/AnalysisPyWindow.tsx @@ -8,6 +8,8 @@ import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; import useAnalysisState from "./useAnalysisState"; import useTemplatedFillerText from "../useTemplatedFillerText"; +import analysisPyTemplate from "./analysis_template.py?raw"; + export type GlobalDataForAnalysis = { draws: number[][]; paramNames: string[]; @@ -97,20 +99,4 @@ const AnalysisPyWindow: FunctionComponent = ({ ); }; -const analysisPyTemplate = `import matplotlib.pyplot as plt - -# Print the draws object -print(draws) - -# Print parameter names -print(draws.parameter_names) - -# plot the lp parameter -samples = draws.get("lp__") -print(samples.shape) -plt.hist(samples.ravel(), bins=30) -plt.title("lp__") -plt.show() -`; - export default AnalysisPyWindow; diff --git a/gui/src/app/Scripting/AnalysisWindow/analysis_template.py b/gui/src/app/Scripting/AnalysisWindow/analysis_template.py new file mode 100644 index 00000000..ec932233 --- /dev/null +++ b/gui/src/app/Scripting/AnalysisWindow/analysis_template.py @@ -0,0 +1,14 @@ +import matplotlib.pyplot as plt + +# Print the draws object +print(draws) + +# Print parameter names +print(draws.parameter_names) + +# plot the lp parameter +samples = draws.get("lp__") +print(samples.shape) +plt.hist(samples.ravel(), bins=30) +plt.title("lp__") +plt.show() diff --git a/gui/src/app/Scripting/DataGenerationWindow/DataPyWindow.tsx b/gui/src/app/Scripting/DataGenerationWindow/DataPyWindow.tsx index 2a902c65..3e3c124e 100644 --- a/gui/src/app/Scripting/DataGenerationWindow/DataPyWindow.tsx +++ b/gui/src/app/Scripting/DataGenerationWindow/DataPyWindow.tsx @@ -6,6 +6,8 @@ import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; import useDataGenState from "./useDataGenState"; import useTemplatedFillerText from "../useTemplatedFillerText"; +import dataPyTemplate from "./data_template.py?raw"; + type Props = { // empty }; @@ -68,8 +70,4 @@ const DataPyWindow: FunctionComponent = () => { ); }; -const dataPyTemplate = `data = { - "a": [1, 2, 3] -}`; - export default DataPyWindow; diff --git a/gui/src/app/Scripting/DataGenerationWindow/DataRWindow.tsx b/gui/src/app/Scripting/DataGenerationWindow/DataRWindow.tsx index 597c09e5..c4163f4f 100644 --- a/gui/src/app/Scripting/DataGenerationWindow/DataRWindow.tsx +++ b/gui/src/app/Scripting/DataGenerationWindow/DataRWindow.tsx @@ -6,6 +6,8 @@ import runR from "../webR/runR"; import useDataGenState from "./useDataGenState"; import useTemplatedFillerText from "../useTemplatedFillerText"; +import dataRTemplate from "./data_template.R?raw"; + type Props = { // empty }; @@ -22,12 +24,12 @@ const DataRWindow: FunctionComponent = () => { const handleHelp = useCallback(() => { alert( - 'Write a Rthon script to assign data to the "data" variable and then click "Run" to generate data.', + 'Write a R script to assign data to the "data" variable and then click "Run" to generate data.', ); }, []); const contentOnEmpty = useTemplatedFillerText( - "Define a dictionary called data to update the data.json. ", + "Define a list called data to update the data.json. ", dataRTemplate, ProjectKnownFiles.DATARFILE, ); @@ -48,8 +50,4 @@ const DataRWindow: FunctionComponent = () => { ); }; -const dataRTemplate = `data = { - "a": [1, 2, 3] -}`; - export default DataRWindow; diff --git a/gui/src/app/Scripting/DataGenerationWindow/data_template.R b/gui/src/app/Scripting/DataGenerationWindow/data_template.R new file mode 100644 index 00000000..9ca55987 --- /dev/null +++ b/gui/src/app/Scripting/DataGenerationWindow/data_template.R @@ -0,0 +1 @@ +data <- list(a=c(1, 2, 3)) diff --git a/gui/src/app/Scripting/DataGenerationWindow/data_template.py b/gui/src/app/Scripting/DataGenerationWindow/data_template.py new file mode 100644 index 00000000..8ac2e196 --- /dev/null +++ b/gui/src/app/Scripting/DataGenerationWindow/data_template.py @@ -0,0 +1 @@ +data = {"a": [1, 2, 3]} From 6a5cbd31a43909e0f99d6dd499d93addae09c2c4 Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Fri, 26 Jul 2024 15:21:01 +0000 Subject: [PATCH 07/16] Add draws functionality to analysis.R --- .../AnalysisWindow/AnalysisPyWindow.tsx | 6 +- .../AnalysisWindow/AnalysisRWindow.tsx | 34 +++++----- .../AnalysisWindow/analysis_template.R | 7 +++ .../DataGenerationWindow/DataPyWindow.tsx | 11 ++-- .../DataGenerationWindow/DataRWindow.tsx | 11 ++-- gui/src/app/Scripting/webR/runR.ts | 62 ++++++++++++------- gui/src/app/Scripting/webR/sp_load_draws.R | 29 +++++++++ 7 files changed, 108 insertions(+), 52 deletions(-) create mode 100644 gui/src/app/Scripting/AnalysisWindow/analysis_template.R create mode 100644 gui/src/app/Scripting/webR/sp_load_draws.R diff --git a/gui/src/app/Scripting/AnalysisWindow/AnalysisPyWindow.tsx b/gui/src/app/Scripting/AnalysisWindow/AnalysisPyWindow.tsx index bbb5f2a7..d399e525 100644 --- a/gui/src/app/Scripting/AnalysisWindow/AnalysisPyWindow.tsx +++ b/gui/src/app/Scripting/AnalysisWindow/AnalysisPyWindow.tsx @@ -57,10 +57,6 @@ const AnalysisPyWindow: FunctionComponent = ({ const handleRun = useCallback( (code: string) => { - if (status === "running") { - return; - } - if (consoleRef.current) { consoleRef.current.innerHTML = ""; } @@ -73,7 +69,7 @@ const AnalysisPyWindow: FunctionComponent = ({ producesData: false, }); }, - [status, consoleRef, imagesRef, run, spData], + [consoleRef, imagesRef, run, spData], ); const contentOnEmpty = useTemplatedFillerText( diff --git a/gui/src/app/Scripting/AnalysisWindow/AnalysisRWindow.tsx b/gui/src/app/Scripting/AnalysisWindow/AnalysisRWindow.tsx index 19758146..906359f3 100644 --- a/gui/src/app/Scripting/AnalysisWindow/AnalysisRWindow.tsx +++ b/gui/src/app/Scripting/AnalysisWindow/AnalysisRWindow.tsx @@ -1,10 +1,4 @@ -import { - FunctionComponent, - useCallback, - useEffect, - useMemo, - useState, -} from "react"; +import { FunctionComponent, useCallback } from "react"; import { StanRun } from "@SpStanSampler/useStanSampler"; import { FileNames } from "@SpCore/FileMapping"; import PlottingScriptEditor from "app/Scripting/PlottingScriptEditor"; @@ -13,6 +7,9 @@ import useAnalysisState from "./useAnalysisState"; import runR from "../webR/runR"; import useTemplatedFillerText from "../useTemplatedFillerText"; +import loadDrawsCode from "../webR/sp_load_draws.R?raw"; +import analysisRTemplate from "./analysis_template.R?raw"; + type AnalysisWindowProps = { latestRun: StanRun; }; @@ -31,10 +28,23 @@ const AnalysisRWindow: FunctionComponent = ({ } = useAnalysisState(latestRun); const handleRun = useCallback( - async (code: string) => { - await runR({ code, imagesRef, consoleRef, onStatus }); + async (userCode: string) => { + if (consoleRef.current) { + consoleRef.current.innerHTML = ""; + } + if (imagesRef.current) { + imagesRef.current.innerHTML = ""; + } + const code = loadDrawsCode + userCode; + await runR({ + code, + imagesRef, + consoleRef, + onStatus, + spData, + }); }, - [consoleRef, imagesRef, onStatus], + [consoleRef, imagesRef, onStatus, spData], ); const contentOnEmpty = useTemplatedFillerText( @@ -60,8 +70,4 @@ const AnalysisRWindow: FunctionComponent = ({ ); }; -const analysisRTemplate = ` -TODO -`; - export default AnalysisRWindow; diff --git a/gui/src/app/Scripting/AnalysisWindow/analysis_template.R b/gui/src/app/Scripting/AnalysisWindow/analysis_template.R new file mode 100644 index 00000000..e18fc1eb --- /dev/null +++ b/gui/src/app/Scripting/AnalysisWindow/analysis_template.R @@ -0,0 +1,7 @@ +library(posterior) +print(summary(draws)) + +install.packages("bayesplot") +library(bayesplot) + +mcmc_hist(draws, pars=c("lp__")) diff --git a/gui/src/app/Scripting/DataGenerationWindow/DataPyWindow.tsx b/gui/src/app/Scripting/DataGenerationWindow/DataPyWindow.tsx index 3e3c124e..3132d93d 100644 --- a/gui/src/app/Scripting/DataGenerationWindow/DataPyWindow.tsx +++ b/gui/src/app/Scripting/DataGenerationWindow/DataPyWindow.tsx @@ -12,6 +12,11 @@ type Props = { // empty }; +const handleHelp = () => + alert( + 'Write a Python script to assign data to the "data" variable and then click "Run" to generate data.', + ); + const DataPyWindow: FunctionComponent = () => { const { consoleRef, status, onStatus, onData } = useDataGenState(); @@ -42,12 +47,6 @@ const DataPyWindow: FunctionComponent = () => { [run], ); - const handleHelp = useCallback(() => { - alert( - 'Write a Python script to assign data to the "data" variable and then click "Run" to generate data.', - ); - }, []); - const contentOnEmpty = useTemplatedFillerText( "Define a dictionary called data to update the data.json. ", dataPyTemplate, diff --git a/gui/src/app/Scripting/DataGenerationWindow/DataRWindow.tsx b/gui/src/app/Scripting/DataGenerationWindow/DataRWindow.tsx index c4163f4f..3788f4a8 100644 --- a/gui/src/app/Scripting/DataGenerationWindow/DataRWindow.tsx +++ b/gui/src/app/Scripting/DataGenerationWindow/DataRWindow.tsx @@ -12,6 +12,11 @@ type Props = { // empty }; +const handleHelp = () => + alert( + 'Write a R script to assign data to the "data" variable and then click "Run" to generate data.', + ); + const DataRWindow: FunctionComponent = () => { const { consoleRef, status, onStatus, onData } = useDataGenState(); @@ -22,12 +27,6 @@ const DataRWindow: FunctionComponent = () => { [consoleRef, onData, onStatus], ); - const handleHelp = useCallback(() => { - alert( - 'Write a R script to assign data to the "data" variable and then click "Run" to generate data.', - ); - }, []); - const contentOnEmpty = useTemplatedFillerText( "Define a list called data to update the data.json. ", dataRTemplate, diff --git a/gui/src/app/Scripting/webR/runR.ts b/gui/src/app/Scripting/webR/runR.ts index 364639d2..4f88ab02 100644 --- a/gui/src/app/Scripting/webR/runR.ts +++ b/gui/src/app/Scripting/webR/runR.ts @@ -4,11 +4,20 @@ import { InterpreterStatus } from "../InterpreterTypes"; import { writeConsoleOutToDiv } from "../ScriptEditor"; let webR: WebR | null = null; -export const loadWebRInstance = async () => { +export const loadWebRInstance = async ( + onStatus: (s: InterpreterStatus) => void, +) => { if (webR === null) { + onStatus("loading"); + await sleep(100); // let the UI update + const w = new WebR(); await w.init(); - w.installPackages(["jsonlite"]); + + onStatus("installing"); + await sleep(100); // let the UI update + await w.installPackages(["jsonlite", "posterior"]); + webR = w; return webR; } else { @@ -16,40 +25,39 @@ export const loadWebRInstance = async () => { } }; +const captureOutputOptions = { + withAutoprint: true, + captureStreams: true, + captureConditions: false, + captureGraphics: { + width: 340, + height: 340, + bg: "white", // default: transparent + pointsize: 12, + capture: true, + }, +} as const; + type RunRProps = { code: string; consoleRef: RefObject; imagesRef?: RefObject; onStatus: (status: InterpreterStatus) => void; onData?: (data: any) => void; + spData?: Record; }; +// todo: consider using something like Console class from webr const runR = async ({ code, imagesRef, consoleRef, onStatus, onData, + spData, }: RunRProps) => { - const captureOutputOptions: any = { - withAutoprint: true, - captureStreams: true, - captureConditions: false, - env: {}, - captureGraphics: { - width: 340, - height: 340, - bg: "white", // default: transparent - pointsize: 12, - capture: true, - }, - }; - try { - onStatus("loading"); - await sleep(100); // let the UI update - const webR = await loadWebRInstance(); - + const webR = await loadWebRInstance(onStatus); const shelter = await new webR.Shelter(); onStatus("running"); @@ -70,7 +78,19 @@ stop("[stan-playground] data must be a list") .SP_DATA`; } try { - const ret = await shelter.captureR(rCode, captureOutputOptions); + const globals: { [key: string]: any } = { + ".stan_playground": true, + }; + if (spData) { + globals[".SP_DATA_IN"] = await new shelter.RList(spData); + } + + const env = await new shelter.REnvironment(globals); + + const options = { ...captureOutputOptions, env }; + + const ret = await shelter.captureR(rCode, options); + ret.output.forEach(({ type, data }) => { if (type === "stdout" || type === "stderr") { writeConsoleOutToDiv(consoleRef, data, type); diff --git a/gui/src/app/Scripting/webR/sp_load_draws.R b/gui/src/app/Scripting/webR/sp_load_draws.R new file mode 100644 index 00000000..2fdc297a --- /dev/null +++ b/gui/src/app/Scripting/webR/sp_load_draws.R @@ -0,0 +1,29 @@ +# copied from cmdstanr, definitely doesn't handle tuples, but then neither does +# posterior +.repair_variable_names <- function(names) { + names <- sub("\\.", "[", names) + names <- gsub("\\.", ",", names) + names[grep("\\[", names)] <- paste0(names[grep("\\[", names)], "]") + names +} + + +.to_posterior_draws_array <- function(SP_DATA_IN) { + draws <- SP_DATA_IN$draws + num_chains <- SP_DATA_IN$numChains + + names <- .repair_variable_names(SP_DATA_IN$paramNames) + num_params <- length(names) + + num_draws <- length(draws) %/% num_chains + + dims <- c(num_params, num_draws, num_chains ) + draws <- array(unlist(draws), dim = dims, dimnames = list( names, NULL, NULL)) + # posterior likes draws x chains x params + draws <- aperm(draws, c(2,3,1)) + + posterior::as_draws_array(draws) +} + +draws <- .to_posterior_draws_array(.SP_DATA_IN) +rm(.SP_DATA_IN) From b26d09839b1b0978664e360e142ba3ec0f4aa3c0 Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Fri, 26 Jul 2024 15:39:24 +0000 Subject: [PATCH 08/16] Path changes --- .../{AnalysisWindow => Analysis}/AnalysisPyWindow.tsx | 8 ++++---- .../{AnalysisWindow => Analysis}/AnalysisRWindow.tsx | 8 ++++---- .../{AnalysisWindow => Analysis}/analysis_template.R | 0 .../{AnalysisWindow => Analysis}/analysis_template.py | 0 .../{AnalysisWindow => Analysis}/useAnalysisState.ts | 5 ++++- .../DataPyWindow.tsx | 6 +++--- .../DataRWindow.tsx | 6 +++--- .../data_template.R | 0 .../data_template.py | 0 .../useDataGenState.ts | 6 +++--- gui/src/app/Scripting/pyodide/pyodideWorker.ts | 2 +- gui/src/app/Scripting/pyodide/pyodideWorkerTypes.ts | 5 ++++- gui/src/app/Scripting/pyodide/usePyodideWorker.ts | 5 +++-- gui/src/app/Scripting/webR/runR.ts | 4 ++-- gui/src/app/pages/HomePage/HomePage.tsx | 4 ++-- .../app/pages/HomePage/SamplingWindow/SamplingWindow.tsx | 4 ++-- gui/tsconfig.json | 2 +- 17 files changed, 36 insertions(+), 29 deletions(-) rename gui/src/app/Scripting/{AnalysisWindow => Analysis}/AnalysisPyWindow.tsx (89%) rename gui/src/app/Scripting/{AnalysisWindow => Analysis}/AnalysisRWindow.tsx (86%) rename gui/src/app/Scripting/{AnalysisWindow => Analysis}/analysis_template.R (100%) rename gui/src/app/Scripting/{AnalysisWindow => Analysis}/analysis_template.py (100%) rename gui/src/app/Scripting/{AnalysisWindow => Analysis}/useAnalysisState.ts (95%) rename gui/src/app/Scripting/{DataGenerationWindow => DataGeneration}/DataPyWindow.tsx (88%) rename gui/src/app/Scripting/{DataGenerationWindow => DataGeneration}/DataRWindow.tsx (88%) rename gui/src/app/Scripting/{DataGenerationWindow => DataGeneration}/data_template.R (100%) rename gui/src/app/Scripting/{DataGenerationWindow => DataGeneration}/data_template.py (100%) rename gui/src/app/Scripting/{DataGenerationWindow => DataGeneration}/useDataGenState.ts (90%) diff --git a/gui/src/app/Scripting/AnalysisWindow/AnalysisPyWindow.tsx b/gui/src/app/Scripting/Analysis/AnalysisPyWindow.tsx similarity index 89% rename from gui/src/app/Scripting/AnalysisWindow/AnalysisPyWindow.tsx rename to gui/src/app/Scripting/Analysis/AnalysisPyWindow.tsx index d399e525..8201046e 100644 --- a/gui/src/app/Scripting/AnalysisWindow/AnalysisPyWindow.tsx +++ b/gui/src/app/Scripting/Analysis/AnalysisPyWindow.tsx @@ -1,12 +1,12 @@ import { FunctionComponent, useCallback, useMemo } from "react"; import { StanRun } from "@SpStanSampler/useStanSampler"; import { FileNames } from "@SpCore/FileMapping"; -import PlottingScriptEditor from "app/Scripting/PlottingScriptEditor"; -import { writeConsoleOutToDiv } from "app/Scripting/ScriptEditor"; -import usePyodideWorker from "app/Scripting/pyodide/usePyodideWorker"; import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; +import useTemplatedFillerText from "@SpScripting/useTemplatedFillerText"; +import { writeConsoleOutToDiv } from "@SpScripting/ScriptEditor"; +import usePyodideWorker from "@SpScripting/pyodide/usePyodideWorker"; +import PlottingScriptEditor from "@SpScripting/PlottingScriptEditor"; import useAnalysisState from "./useAnalysisState"; -import useTemplatedFillerText from "../useTemplatedFillerText"; import analysisPyTemplate from "./analysis_template.py?raw"; diff --git a/gui/src/app/Scripting/AnalysisWindow/AnalysisRWindow.tsx b/gui/src/app/Scripting/Analysis/AnalysisRWindow.tsx similarity index 86% rename from gui/src/app/Scripting/AnalysisWindow/AnalysisRWindow.tsx rename to gui/src/app/Scripting/Analysis/AnalysisRWindow.tsx index 906359f3..0de21485 100644 --- a/gui/src/app/Scripting/AnalysisWindow/AnalysisRWindow.tsx +++ b/gui/src/app/Scripting/Analysis/AnalysisRWindow.tsx @@ -1,13 +1,13 @@ import { FunctionComponent, useCallback } from "react"; import { StanRun } from "@SpStanSampler/useStanSampler"; import { FileNames } from "@SpCore/FileMapping"; -import PlottingScriptEditor from "app/Scripting/PlottingScriptEditor"; import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; +import PlottingScriptEditor from "@SpScripting/PlottingScriptEditor"; +import runR from "@SpScripting/webR/runR"; +import useTemplatedFillerText from "@SpScripting/useTemplatedFillerText"; +import loadDrawsCode from "@SpScripting/webR/sp_load_draws.R?raw"; import useAnalysisState from "./useAnalysisState"; -import runR from "../webR/runR"; -import useTemplatedFillerText from "../useTemplatedFillerText"; -import loadDrawsCode from "../webR/sp_load_draws.R?raw"; import analysisRTemplate from "./analysis_template.R?raw"; type AnalysisWindowProps = { diff --git a/gui/src/app/Scripting/AnalysisWindow/analysis_template.R b/gui/src/app/Scripting/Analysis/analysis_template.R similarity index 100% rename from gui/src/app/Scripting/AnalysisWindow/analysis_template.R rename to gui/src/app/Scripting/Analysis/analysis_template.R diff --git a/gui/src/app/Scripting/AnalysisWindow/analysis_template.py b/gui/src/app/Scripting/Analysis/analysis_template.py similarity index 100% rename from gui/src/app/Scripting/AnalysisWindow/analysis_template.py rename to gui/src/app/Scripting/Analysis/analysis_template.py diff --git a/gui/src/app/Scripting/AnalysisWindow/useAnalysisState.ts b/gui/src/app/Scripting/Analysis/useAnalysisState.ts similarity index 95% rename from gui/src/app/Scripting/AnalysisWindow/useAnalysisState.ts rename to gui/src/app/Scripting/Analysis/useAnalysisState.ts index 1b5ec654..5c3446ee 100644 --- a/gui/src/app/Scripting/AnalysisWindow/useAnalysisState.ts +++ b/gui/src/app/Scripting/Analysis/useAnalysisState.ts @@ -1,6 +1,9 @@ +import { + InterpreterStatus, + isInterpreterBusy, +} from "@SpScripting/InterpreterTypes"; import { StanRun } from "@SpStanSampler/useStanSampler"; import { useEffect, useMemo, useRef, useState } from "react"; -import { InterpreterStatus, isInterpreterBusy } from "../InterpreterTypes"; export type GlobalDataForAnalysis = { draws: number[][]; diff --git a/gui/src/app/Scripting/DataGenerationWindow/DataPyWindow.tsx b/gui/src/app/Scripting/DataGeneration/DataPyWindow.tsx similarity index 88% rename from gui/src/app/Scripting/DataGenerationWindow/DataPyWindow.tsx rename to gui/src/app/Scripting/DataGeneration/DataPyWindow.tsx index 3132d93d..e95c26a5 100644 --- a/gui/src/app/Scripting/DataGenerationWindow/DataPyWindow.tsx +++ b/gui/src/app/Scripting/DataGeneration/DataPyWindow.tsx @@ -1,10 +1,10 @@ import { FunctionComponent, useCallback, useMemo } from "react"; -import usePyodideWorker from "app/Scripting/pyodide/usePyodideWorker"; -import ScriptEditor, { writeConsoleOutToDiv } from "app/Scripting/ScriptEditor"; import { FileNames } from "@SpCore/FileMapping"; import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; +import useTemplatedFillerText from "@SpScripting/useTemplatedFillerText"; +import ScriptEditor, { writeConsoleOutToDiv } from "@SpScripting/ScriptEditor"; +import usePyodideWorker from "@SpScripting/pyodide/usePyodideWorker"; import useDataGenState from "./useDataGenState"; -import useTemplatedFillerText from "../useTemplatedFillerText"; import dataPyTemplate from "./data_template.py?raw"; diff --git a/gui/src/app/Scripting/DataGenerationWindow/DataRWindow.tsx b/gui/src/app/Scripting/DataGeneration/DataRWindow.tsx similarity index 88% rename from gui/src/app/Scripting/DataGenerationWindow/DataRWindow.tsx rename to gui/src/app/Scripting/DataGeneration/DataRWindow.tsx index 3788f4a8..550ad416 100644 --- a/gui/src/app/Scripting/DataGenerationWindow/DataRWindow.tsx +++ b/gui/src/app/Scripting/DataGeneration/DataRWindow.tsx @@ -1,10 +1,10 @@ import { FunctionComponent, useCallback } from "react"; -import ScriptEditor from "app/Scripting/ScriptEditor"; +import ScriptEditor from "@SpScripting/ScriptEditor"; import { FileNames } from "@SpCore/FileMapping"; import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; -import runR from "../webR/runR"; +import runR from "@SpScripting/webR/runR"; +import useTemplatedFillerText from "@SpScripting/useTemplatedFillerText"; import useDataGenState from "./useDataGenState"; -import useTemplatedFillerText from "../useTemplatedFillerText"; import dataRTemplate from "./data_template.R?raw"; diff --git a/gui/src/app/Scripting/DataGenerationWindow/data_template.R b/gui/src/app/Scripting/DataGeneration/data_template.R similarity index 100% rename from gui/src/app/Scripting/DataGenerationWindow/data_template.R rename to gui/src/app/Scripting/DataGeneration/data_template.R diff --git a/gui/src/app/Scripting/DataGenerationWindow/data_template.py b/gui/src/app/Scripting/DataGeneration/data_template.py similarity index 100% rename from gui/src/app/Scripting/DataGenerationWindow/data_template.py rename to gui/src/app/Scripting/DataGeneration/data_template.py diff --git a/gui/src/app/Scripting/DataGenerationWindow/useDataGenState.ts b/gui/src/app/Scripting/DataGeneration/useDataGenState.ts similarity index 90% rename from gui/src/app/Scripting/DataGenerationWindow/useDataGenState.ts rename to gui/src/app/Scripting/DataGeneration/useDataGenState.ts index 6d6e3592..84354fbc 100644 --- a/gui/src/app/Scripting/DataGenerationWindow/useDataGenState.ts +++ b/gui/src/app/Scripting/DataGeneration/useDataGenState.ts @@ -1,7 +1,7 @@ -import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; -import { writeConsoleOutToDiv } from "../ScriptEditor"; import { useCallback, useContext, useRef, useState } from "react"; -import { InterpreterStatus } from "../InterpreterTypes"; +import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; +import { writeConsoleOutToDiv } from "@SpScripting/ScriptEditor"; +import { InterpreterStatus } from "@SpScripting/InterpreterTypes"; import { ProjectContext } from "@SpCore/ProjectContextProvider"; const useDataGenState = () => { diff --git a/gui/src/app/Scripting/pyodide/pyodideWorker.ts b/gui/src/app/Scripting/pyodide/pyodideWorker.ts index afd436d4..049e0be2 100644 --- a/gui/src/app/Scripting/pyodide/pyodideWorker.ts +++ b/gui/src/app/Scripting/pyodide/pyodideWorker.ts @@ -1,11 +1,11 @@ import { PyodideInterface, loadPyodide } from "pyodide"; +import { InterpreterStatus } from "@SpScripting/InterpreterTypes"; import { MessageFromPyodideWorker, PyodideRunSettings, } from "./pyodideWorkerTypes"; import spDrawsScript from "./sp_load_draws.py?raw"; import spMPLScript from "./sp_patch_matplotlib.py?raw"; -import { InterpreterStatus } from "../InterpreterTypes"; let pyodide: PyodideInterface | null = null; const loadPyodideInstance = async () => { diff --git a/gui/src/app/Scripting/pyodide/pyodideWorkerTypes.ts b/gui/src/app/Scripting/pyodide/pyodideWorkerTypes.ts index 6f037acd..5e869d57 100644 --- a/gui/src/app/Scripting/pyodide/pyodideWorkerTypes.ts +++ b/gui/src/app/Scripting/pyodide/pyodideWorkerTypes.ts @@ -1,5 +1,8 @@ import baseObjectCheck from "@SpUtil/baseObjectCheck"; -import { InterpreterStatus, isInterpreterStatus } from "../InterpreterTypes"; +import { + InterpreterStatus, + isInterpreterStatus, +} from "@SpScripting/InterpreterTypes"; export type PyodideRunSettings = Partial<{ loadsDraws: boolean; diff --git a/gui/src/app/Scripting/pyodide/usePyodideWorker.ts b/gui/src/app/Scripting/pyodide/usePyodideWorker.ts index 83ca7380..8b1151b1 100644 --- a/gui/src/app/Scripting/pyodide/usePyodideWorker.ts +++ b/gui/src/app/Scripting/pyodide/usePyodideWorker.ts @@ -1,13 +1,14 @@ +import { useCallback, useEffect, useState } from "react"; +import { InterpreterStatus } from "@SpScripting/InterpreterTypes"; + // https://vitejs.dev/guide/assets#importing-script-as-a-worker // https://vitejs.dev/guide/assets#importing-asset-as-url import pyodideWorkerURL from "./pyodideWorker?worker&url"; -import { useCallback, useEffect, useState } from "react"; import { MessageToPyodideWorker, isMessageFromPyodideWorker, PyodideRunSettings, } from "./pyodideWorkerTypes"; -import { InterpreterStatus } from "../InterpreterTypes"; type PyodideWorkerCallbacks = { onStdout: (data: string) => void; diff --git a/gui/src/app/Scripting/webR/runR.ts b/gui/src/app/Scripting/webR/runR.ts index 4f88ab02..c9c25882 100644 --- a/gui/src/app/Scripting/webR/runR.ts +++ b/gui/src/app/Scripting/webR/runR.ts @@ -1,7 +1,7 @@ import { RefObject } from "react"; import { RString, WebR } from "webr"; -import { InterpreterStatus } from "../InterpreterTypes"; -import { writeConsoleOutToDiv } from "../ScriptEditor"; +import { InterpreterStatus } from "@SpScripting/InterpreterTypes"; +import { writeConsoleOutToDiv } from "@SpScripting/ScriptEditor"; let webR: WebR | null = null; export const loadWebRInstance = async ( diff --git a/gui/src/app/pages/HomePage/HomePage.tsx b/gui/src/app/pages/HomePage/HomePage.tsx index 17186818..134485b4 100644 --- a/gui/src/app/pages/HomePage/HomePage.tsx +++ b/gui/src/app/pages/HomePage/HomePage.tsx @@ -21,8 +21,8 @@ import { import TabWidget from "@SpComponents/TabWidget"; import SamplingWindow from "./SamplingWindow/SamplingWindow"; import { FileNames } from "@SpCore/FileMapping"; -import DataPyWindow from "app/Scripting/DataGenerationWindow/DataPyWindow"; -import DataRWindow from "app/Scripting/DataGenerationWindow/DataRWindow"; +import DataRWindow from "@SpScripting/DataGeneration/DataRWindow"; +import DataPyWindow from "@SpScripting/DataGeneration/DataPyWindow"; type Props = { // diff --git a/gui/src/app/pages/HomePage/SamplingWindow/SamplingWindow.tsx b/gui/src/app/pages/HomePage/SamplingWindow/SamplingWindow.tsx index 1467f7b0..23310e16 100644 --- a/gui/src/app/pages/HomePage/SamplingWindow/SamplingWindow.tsx +++ b/gui/src/app/pages/HomePage/SamplingWindow/SamplingWindow.tsx @@ -12,9 +12,9 @@ import { modelHasUnsavedDataFileChanges, SamplingOpts, } from "@SpCore/ProjectDataModel"; -import AnalysisPyWindow from "app/Scripting/AnalysisWindow/AnalysisPyWindow"; +import AnalysisPyWindow from "@SpScripting/Analysis/AnalysisPyWindow"; import useStanSampler, { StanRun } from "@SpStanSampler/useStanSampler"; -import AnalysisRWindow from "app/Scripting/AnalysisWindow/AnalysisRWindow"; +import AnalysisRWindow from "@SpScripting/Analysis/AnalysisRWindow"; type SamplingWindowProps = { compiledMainJsUrl?: string; diff --git a/gui/tsconfig.json b/gui/tsconfig.json index 387b4fb6..765fbda1 100644 --- a/gui/tsconfig.json +++ b/gui/tsconfig.json @@ -45,7 +45,7 @@ "@SpStanSampler/*": ["app/StanSampler/*"], "@SpUtil/*": ["app/util/*"], "@SpStanStats/*": ["app/util/stan_stats/*"], - "@SpPyodide/*": ["app/pyodide/*"] + "@SpScripting/*": ["app/Scripting/*"] } }, "include": ["src", "test"], From f1e86662754ba564d0c8d7e0211f76e5b21328b9 Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Fri, 26 Jul 2024 15:46:20 +0000 Subject: [PATCH 09/16] Update wording --- gui/src/app/Scripting/Analysis/AnalysisRWindow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/src/app/Scripting/Analysis/AnalysisRWindow.tsx b/gui/src/app/Scripting/Analysis/AnalysisRWindow.tsx index 0de21485..2be1ea2b 100644 --- a/gui/src/app/Scripting/Analysis/AnalysisRWindow.tsx +++ b/gui/src/app/Scripting/Analysis/AnalysisRWindow.tsx @@ -48,7 +48,7 @@ const AnalysisRWindow: FunctionComponent = ({ ); const contentOnEmpty = useTemplatedFillerText( - "Use the draws object to access the samples. ", + "Use the draws object (a posterior::draws_array) to access the samples. ", analysisRTemplate, ProjectKnownFiles.ANALYSISRFILE, ); From e2f0cefb9bf93b39450539e65b658362c3bd53a8 Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Fri, 26 Jul 2024 16:05:12 +0000 Subject: [PATCH 10/16] More cleanup --- gui/src/app/FileEditor/DataFileEditor.tsx | 41 ------------------- .../Scripting/DataGeneration/DataPyWindow.tsx | 5 ++- .../Scripting/DataGeneration/DataRWindow.tsx | 3 ++ gui/src/app/Scripting/webR/runR.ts | 2 +- gui/src/app/pages/HomePage/HomePage.tsx | 15 +++---- 5 files changed, 16 insertions(+), 50 deletions(-) delete mode 100644 gui/src/app/FileEditor/DataFileEditor.tsx diff --git a/gui/src/app/FileEditor/DataFileEditor.tsx b/gui/src/app/FileEditor/DataFileEditor.tsx deleted file mode 100644 index b83ca6d3..00000000 --- a/gui/src/app/FileEditor/DataFileEditor.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import TextEditor, { ToolbarItem } from "@SpComponents/TextEditor"; -import { FunctionComponent, useMemo } from "react"; - -type Props = { - fileName: string; - fileContent: string; - onSaveContent: () => void; - editedFileContent: string; - setEditedFileContent: (text: string) => void; - onDeleteFile?: () => void; - readOnly: boolean; -}; - -const DataFileEditor: FunctionComponent = ({ - fileName, - fileContent, - onSaveContent, - editedFileContent, - setEditedFileContent, - readOnly, -}) => { - const toolbarItems: ToolbarItem[] = useMemo(() => { - const ret: ToolbarItem[] = []; - return ret; - }, []); - - return ( - - ); -}; - -export default DataFileEditor; diff --git a/gui/src/app/Scripting/DataGeneration/DataPyWindow.tsx b/gui/src/app/Scripting/DataGeneration/DataPyWindow.tsx index e95c26a5..6040eea7 100644 --- a/gui/src/app/Scripting/DataGeneration/DataPyWindow.tsx +++ b/gui/src/app/Scripting/DataGeneration/DataPyWindow.tsx @@ -34,6 +34,9 @@ const DataPyWindow: FunctionComponent = () => { const handleRun = useCallback( (code: string) => { + if (consoleRef.current) { + consoleRef.current.innerHTML = ""; + } run( code, {}, @@ -44,7 +47,7 @@ const DataPyWindow: FunctionComponent = () => { }, ); }, - [run], + [consoleRef, run], ); const contentOnEmpty = useTemplatedFillerText( diff --git a/gui/src/app/Scripting/DataGeneration/DataRWindow.tsx b/gui/src/app/Scripting/DataGeneration/DataRWindow.tsx index 550ad416..4563fe93 100644 --- a/gui/src/app/Scripting/DataGeneration/DataRWindow.tsx +++ b/gui/src/app/Scripting/DataGeneration/DataRWindow.tsx @@ -22,6 +22,9 @@ const DataRWindow: FunctionComponent = () => { const handleRun = useCallback( async (code: string) => { + if (consoleRef.current) { + consoleRef.current.innerHTML = ""; + } await runR({ code, consoleRef, onStatus, onData }); }, [consoleRef, onData, onStatus], diff --git a/gui/src/app/Scripting/webR/runR.ts b/gui/src/app/Scripting/webR/runR.ts index c9c25882..d712850e 100644 --- a/gui/src/app/Scripting/webR/runR.ts +++ b/gui/src/app/Scripting/webR/runR.ts @@ -75,7 +75,7 @@ if (typeof(data) != "list") { stop("[stan-playground] data must be a list") } .SP_DATA <- jsonlite::toJSON(data, pretty = TRUE, auto_unbox = TRUE) -.SP_DATA`; +invisible(.SP_DATA)`; } try { const globals: { [key: string]: any } = { diff --git a/gui/src/app/pages/HomePage/HomePage.tsx b/gui/src/app/pages/HomePage/HomePage.tsx index 134485b4..01ff8d67 100644 --- a/gui/src/app/pages/HomePage/HomePage.tsx +++ b/gui/src/app/pages/HomePage/HomePage.tsx @@ -1,7 +1,6 @@ import Box from "@mui/material/Box"; import styled from "@mui/material/styles/styled"; import useMediaQuery from "@mui/material/useMediaQuery"; -import DataFileEditor from "@SpComponents/DataFileEditor"; import { GutterTheme, SplitDirection, Splitter } from "@SpComponents/Splitter"; import StanFileEditor from "@SpComponents/StanFileEditor"; import { ProjectContext } from "@SpCore/ProjectContextProvider"; @@ -23,6 +22,7 @@ import SamplingWindow from "./SamplingWindow/SamplingWindow"; import { FileNames } from "@SpCore/FileMapping"; import DataRWindow from "@SpScripting/DataGeneration/DataRWindow"; import DataPyWindow from "@SpScripting/DataGeneration/DataPyWindow"; +import TextEditor from "@SpComponents/TextEditor"; type Props = { // @@ -128,17 +128,18 @@ const LeftView: FunctionComponent = ({ readOnly={false} setCompiledUrl={setCompiledMainJsUrl} /> - + update({ type: "commitFile", filename: ProjectKnownFiles.DATAFILE, }) } - editedFileContent={data.ephemera.dataFileContent} - setEditedFileContent={(content: string) => + editedText={data.ephemera.dataFileContent} + onSetEditedText={(content: string) => update({ type: "editFile", content, From 01e9783aa4a7d88a89d597b7675f6809b321cbf1 Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Fri, 26 Jul 2024 16:11:17 +0000 Subject: [PATCH 11/16] Fix missed todo --- gui/src/app/Scripting/ScriptEditor.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gui/src/app/Scripting/ScriptEditor.tsx b/gui/src/app/Scripting/ScriptEditor.tsx index 415911ec..057a24b6 100644 --- a/gui/src/app/Scripting/ScriptEditor.tsx +++ b/gui/src/app/Scripting/ScriptEditor.tsx @@ -176,9 +176,9 @@ type ConsoleOutputWindowProps = { consoleRef: RefObject; }; -export /* todo don't export */ const ConsoleOutputWindow: FunctionComponent< - ConsoleOutputWindowProps -> = ({ consoleRef }) => { +const ConsoleOutputWindow: FunctionComponent = ({ + consoleRef, +}) => { return
; }; From 805ca1b67dbb4d88c2e34013ea8ee4b5b648b379 Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Fri, 26 Jul 2024 16:47:56 +0000 Subject: [PATCH 12/16] Fix spurious data.py re-loads --- gui/src/app/Scripting/DataGeneration/useDataGenState.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gui/src/app/Scripting/DataGeneration/useDataGenState.ts b/gui/src/app/Scripting/DataGeneration/useDataGenState.ts index 84354fbc..143519b0 100644 --- a/gui/src/app/Scripting/DataGeneration/useDataGenState.ts +++ b/gui/src/app/Scripting/DataGeneration/useDataGenState.ts @@ -10,11 +10,14 @@ const useDataGenState = () => { const { data, update } = useContext(ProjectContext); + // we don't want the callback to force itself to re-render when data is set + const lastData = useRef(data.dataFileContent); const onData = useCallback( (newData: unknown) => { const dataJson = JSON.stringify(newData, null, 2); - if (dataJson !== data.dataFileContent) { + if (dataJson !== lastData.current) { + lastData.current = dataJson; update({ type: "editFile", content: dataJson, @@ -35,7 +38,7 @@ const useDataGenState = () => { ); } }, - [update, consoleRef, data.dataFileContent], + [update, consoleRef], ); return { consoleRef, status, onStatus: setStatus, onData }; From 2bf3729d13052a33c8f6391a3d937c1e4ba0006c Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Fri, 26 Jul 2024 18:21:34 +0000 Subject: [PATCH 13/16] Make onHelp optional --- .../Scripting/Analysis/AnalysisPyWindow.tsx | 1 - .../app/Scripting/Analysis/AnalysisRWindow.tsx | 1 - gui/src/app/Scripting/ScriptEditor.tsx | 18 ++++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/gui/src/app/Scripting/Analysis/AnalysisPyWindow.tsx b/gui/src/app/Scripting/Analysis/AnalysisPyWindow.tsx index 8201046e..7976ba59 100644 --- a/gui/src/app/Scripting/Analysis/AnalysisPyWindow.tsx +++ b/gui/src/app/Scripting/Analysis/AnalysisPyWindow.tsx @@ -87,7 +87,6 @@ const AnalysisPyWindow: FunctionComponent = ({ onRun={handleRun} runnable={runnable} notRunnableReason={notRunnableReason} - onHelp={() => {}} imagesRef={imagesRef} consoleRef={consoleRef} contentOnEmpty={contentOnEmpty} diff --git a/gui/src/app/Scripting/Analysis/AnalysisRWindow.tsx b/gui/src/app/Scripting/Analysis/AnalysisRWindow.tsx index 2be1ea2b..6ef65a53 100644 --- a/gui/src/app/Scripting/Analysis/AnalysisRWindow.tsx +++ b/gui/src/app/Scripting/Analysis/AnalysisRWindow.tsx @@ -62,7 +62,6 @@ const AnalysisRWindow: FunctionComponent = ({ onRun={handleRun} runnable={runnable} notRunnableReason={notRunnableReason} - onHelp={() => {}} imagesRef={imagesRef} consoleRef={consoleRef} contentOnEmpty={contentOnEmpty} diff --git a/gui/src/app/Scripting/ScriptEditor.tsx b/gui/src/app/Scripting/ScriptEditor.tsx index 057a24b6..2a2489a8 100644 --- a/gui/src/app/Scripting/ScriptEditor.tsx +++ b/gui/src/app/Scripting/ScriptEditor.tsx @@ -23,7 +23,7 @@ export type ScriptEditorProps = { onRun: (code: string) => void; runnable: boolean; notRunnableReason?: string; - onHelp: () => void; + onHelp?: () => void; contentOnEmpty?: string | HTMLSpanElement; consoleRef: RefObject; }; @@ -113,16 +113,18 @@ const makeToolbar = (o: { runnable: boolean; notRunnableReason?: string; onRun: () => void; - onHelp: () => void; + onHelp?: () => void; }): ToolbarItem[] => { const { status, onRun, runnable, onHelp, name } = o; const ret: ToolbarItem[] = []; - ret.push({ - type: "button", - tooltip: "Help", - icon: , - onClick: onHelp, - }); + if (onHelp !== undefined) { + ret.push({ + type: "button", + tooltip: "Help", + icon: , + onClick: onHelp, + }); + } if (runnable) { ret.push({ type: "button", From 8779e91d4578f1284d6e6b135abe32f4026099c8 Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Fri, 26 Jul 2024 18:24:15 +0000 Subject: [PATCH 14/16] Explanatory comments --- gui/src/app/Scripting/Analysis/useAnalysisState.ts | 3 +++ gui/src/app/Scripting/DataGeneration/useDataGenState.ts | 2 ++ gui/src/app/Scripting/useTemplatedFillerText.ts | 2 ++ 3 files changed, 7 insertions(+) diff --git a/gui/src/app/Scripting/Analysis/useAnalysisState.ts b/gui/src/app/Scripting/Analysis/useAnalysisState.ts index 5c3446ee..1923073d 100644 --- a/gui/src/app/Scripting/Analysis/useAnalysisState.ts +++ b/gui/src/app/Scripting/Analysis/useAnalysisState.ts @@ -11,6 +11,9 @@ export type GlobalDataForAnalysis = { numChains: number; }; +// A custom hook to share logic between the Python and R analysis windows +// This contains the output div refs, the interpreter state, and the data from +// the latest run. const useAnalysisState = (latestRun: StanRun) => { const consoleRef = useRef(null); const imagesRef = useRef(null); diff --git a/gui/src/app/Scripting/DataGeneration/useDataGenState.ts b/gui/src/app/Scripting/DataGeneration/useDataGenState.ts index 143519b0..c9357dfb 100644 --- a/gui/src/app/Scripting/DataGeneration/useDataGenState.ts +++ b/gui/src/app/Scripting/DataGeneration/useDataGenState.ts @@ -4,6 +4,8 @@ import { writeConsoleOutToDiv } from "@SpScripting/ScriptEditor"; import { InterpreterStatus } from "@SpScripting/InterpreterTypes"; import { ProjectContext } from "@SpCore/ProjectContextProvider"; +// A custom hook to share logic between the Python and R data generation windows +// This contains the output div ref, the interpreter state, and the callback to update the data. const useDataGenState = () => { const [status, setStatus] = useState("idle"); const consoleRef = useRef(null); diff --git a/gui/src/app/Scripting/useTemplatedFillerText.ts b/gui/src/app/Scripting/useTemplatedFillerText.ts index 1d7e92b4..a85877ac 100644 --- a/gui/src/app/Scripting/useTemplatedFillerText.ts +++ b/gui/src/app/Scripting/useTemplatedFillerText.ts @@ -2,6 +2,8 @@ import { ProjectContext } from "@SpCore/ProjectContextProvider"; import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; import { useContext, useMemo } from "react"; +// This is used to create the text span used in the ScriptEditor component when the file is empty. +// It features a brief description and a clickable link to generate an example template. const useTemplatedFillerText = ( text: string, template: string, From afeec3c876f92932bb91419f4de0125ce95db553 Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Fri, 26 Jul 2024 19:04:52 +0000 Subject: [PATCH 15/16] Pull out clearing innerHTML --- .../Scripting/Analysis/AnalysisPyWindow.tsx | 12 ++++----- .../Scripting/Analysis/AnalysisRWindow.tsx | 8 ++---- .../Scripting/Analysis/useAnalysisState.ts | 8 ++---- .../Scripting/DataGeneration/DataPyWindow.tsx | 10 +++++--- .../Scripting/DataGeneration/DataRWindow.tsx | 5 ++-- .../DataGeneration/useDataGenState.ts | 2 +- gui/src/app/Scripting/OutputDivUtils.tsx | 25 +++++++++++++++++++ gui/src/app/Scripting/ScriptEditor.tsx | 18 ------------- gui/src/app/Scripting/webR/runR.ts | 2 +- 9 files changed, 44 insertions(+), 46 deletions(-) create mode 100644 gui/src/app/Scripting/OutputDivUtils.tsx diff --git a/gui/src/app/Scripting/Analysis/AnalysisPyWindow.tsx b/gui/src/app/Scripting/Analysis/AnalysisPyWindow.tsx index 7976ba59..34076612 100644 --- a/gui/src/app/Scripting/Analysis/AnalysisPyWindow.tsx +++ b/gui/src/app/Scripting/Analysis/AnalysisPyWindow.tsx @@ -3,7 +3,10 @@ import { StanRun } from "@SpStanSampler/useStanSampler"; import { FileNames } from "@SpCore/FileMapping"; import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; import useTemplatedFillerText from "@SpScripting/useTemplatedFillerText"; -import { writeConsoleOutToDiv } from "@SpScripting/ScriptEditor"; +import { + clearOutputDivs, + writeConsoleOutToDiv, +} from "@SpScripting/OutputDivUtils"; import usePyodideWorker from "@SpScripting/pyodide/usePyodideWorker"; import PlottingScriptEditor from "@SpScripting/PlottingScriptEditor"; import useAnalysisState from "./useAnalysisState"; @@ -57,12 +60,7 @@ const AnalysisPyWindow: FunctionComponent = ({ const handleRun = useCallback( (code: string) => { - if (consoleRef.current) { - consoleRef.current.innerHTML = ""; - } - if (imagesRef.current) { - imagesRef.current.innerHTML = ""; - } + clearOutputDivs(consoleRef, imagesRef); run(code, spData, { loadsDraws: true, showsPlots: true, diff --git a/gui/src/app/Scripting/Analysis/AnalysisRWindow.tsx b/gui/src/app/Scripting/Analysis/AnalysisRWindow.tsx index 6ef65a53..b8138577 100644 --- a/gui/src/app/Scripting/Analysis/AnalysisRWindow.tsx +++ b/gui/src/app/Scripting/Analysis/AnalysisRWindow.tsx @@ -5,6 +5,7 @@ import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; import PlottingScriptEditor from "@SpScripting/PlottingScriptEditor"; import runR from "@SpScripting/webR/runR"; import useTemplatedFillerText from "@SpScripting/useTemplatedFillerText"; +import { clearOutputDivs } from "@SpScripting/OutputDivUtils"; import loadDrawsCode from "@SpScripting/webR/sp_load_draws.R?raw"; import useAnalysisState from "./useAnalysisState"; @@ -29,12 +30,7 @@ const AnalysisRWindow: FunctionComponent = ({ const handleRun = useCallback( async (userCode: string) => { - if (consoleRef.current) { - consoleRef.current.innerHTML = ""; - } - if (imagesRef.current) { - imagesRef.current.innerHTML = ""; - } + clearOutputDivs(consoleRef, imagesRef); const code = loadDrawsCode + userCode; await runR({ code, diff --git a/gui/src/app/Scripting/Analysis/useAnalysisState.ts b/gui/src/app/Scripting/Analysis/useAnalysisState.ts index 1923073d..7bdec3f7 100644 --- a/gui/src/app/Scripting/Analysis/useAnalysisState.ts +++ b/gui/src/app/Scripting/Analysis/useAnalysisState.ts @@ -2,6 +2,7 @@ import { InterpreterStatus, isInterpreterBusy, } from "@SpScripting/InterpreterTypes"; +import { clearOutputDivs } from "@SpScripting/OutputDivUtils"; import { StanRun } from "@SpStanSampler/useStanSampler"; import { useEffect, useMemo, useRef, useState } from "react"; @@ -19,12 +20,7 @@ const useAnalysisState = (latestRun: StanRun) => { const imagesRef = useRef(null); useEffect(() => { - if (imagesRef.current) { - imagesRef.current.innerHTML = ""; - } - if (consoleRef.current) { - consoleRef.current.innerHTML = ""; - } + clearOutputDivs(consoleRef, imagesRef); }, [latestRun.draws]); const { draws, paramNames, samplingOpts, status: samplerStatus } = latestRun; diff --git a/gui/src/app/Scripting/DataGeneration/DataPyWindow.tsx b/gui/src/app/Scripting/DataGeneration/DataPyWindow.tsx index 6040eea7..bd6f5241 100644 --- a/gui/src/app/Scripting/DataGeneration/DataPyWindow.tsx +++ b/gui/src/app/Scripting/DataGeneration/DataPyWindow.tsx @@ -2,7 +2,11 @@ import { FunctionComponent, useCallback, useMemo } from "react"; import { FileNames } from "@SpCore/FileMapping"; import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; import useTemplatedFillerText from "@SpScripting/useTemplatedFillerText"; -import ScriptEditor, { writeConsoleOutToDiv } from "@SpScripting/ScriptEditor"; +import ScriptEditor from "@SpScripting/ScriptEditor"; +import { + clearOutputDivs, + writeConsoleOutToDiv, +} from "@SpScripting/OutputDivUtils"; import usePyodideWorker from "@SpScripting/pyodide/usePyodideWorker"; import useDataGenState from "./useDataGenState"; @@ -34,9 +38,7 @@ const DataPyWindow: FunctionComponent = () => { const handleRun = useCallback( (code: string) => { - if (consoleRef.current) { - consoleRef.current.innerHTML = ""; - } + clearOutputDivs(consoleRef); run( code, {}, diff --git a/gui/src/app/Scripting/DataGeneration/DataRWindow.tsx b/gui/src/app/Scripting/DataGeneration/DataRWindow.tsx index 4563fe93..91facd92 100644 --- a/gui/src/app/Scripting/DataGeneration/DataRWindow.tsx +++ b/gui/src/app/Scripting/DataGeneration/DataRWindow.tsx @@ -1,5 +1,6 @@ import { FunctionComponent, useCallback } from "react"; import ScriptEditor from "@SpScripting/ScriptEditor"; +import { clearOutputDivs } from "@SpScripting/OutputDivUtils"; import { FileNames } from "@SpCore/FileMapping"; import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; import runR from "@SpScripting/webR/runR"; @@ -22,9 +23,7 @@ const DataRWindow: FunctionComponent = () => { const handleRun = useCallback( async (code: string) => { - if (consoleRef.current) { - consoleRef.current.innerHTML = ""; - } + clearOutputDivs(consoleRef); await runR({ code, consoleRef, onStatus, onData }); }, [consoleRef, onData, onStatus], diff --git a/gui/src/app/Scripting/DataGeneration/useDataGenState.ts b/gui/src/app/Scripting/DataGeneration/useDataGenState.ts index c9357dfb..892d8597 100644 --- a/gui/src/app/Scripting/DataGeneration/useDataGenState.ts +++ b/gui/src/app/Scripting/DataGeneration/useDataGenState.ts @@ -1,6 +1,6 @@ import { useCallback, useContext, useRef, useState } from "react"; import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; -import { writeConsoleOutToDiv } from "@SpScripting/ScriptEditor"; +import { writeConsoleOutToDiv } from "@SpScripting/OutputDivUtils"; import { InterpreterStatus } from "@SpScripting/InterpreterTypes"; import { ProjectContext } from "@SpCore/ProjectContextProvider"; diff --git a/gui/src/app/Scripting/OutputDivUtils.tsx b/gui/src/app/Scripting/OutputDivUtils.tsx new file mode 100644 index 00000000..11558a91 --- /dev/null +++ b/gui/src/app/Scripting/OutputDivUtils.tsx @@ -0,0 +1,25 @@ +import { RefObject } from "react"; + +type ConsoleOutType = "stdout" | "stderr"; + +export const writeConsoleOutToDiv = ( + parentDiv: RefObject, + x: string, + type: ConsoleOutType, +) => { + if (x === "") return; + if (!parentDiv.current) return; + const styleClass = type === "stdout" ? "WorkerStdout" : "WorkerStderr"; + const preElement = document.createElement("pre"); + preElement.textContent = x; + const divElement = document.createElement("div"); + divElement.className = styleClass; + divElement.appendChild(preElement); + parentDiv.current.appendChild(divElement); +}; + +export const clearOutputDivs = (...parentDiv: RefObject[]) => { + for (const div of parentDiv) { + if (div.current) div.current.innerHTML = ""; + } +}; diff --git a/gui/src/app/Scripting/ScriptEditor.tsx b/gui/src/app/Scripting/ScriptEditor.tsx index 2a2489a8..97ec153f 100644 --- a/gui/src/app/Scripting/ScriptEditor.tsx +++ b/gui/src/app/Scripting/ScriptEditor.tsx @@ -184,22 +184,4 @@ const ConsoleOutputWindow: FunctionComponent = ({ return
; }; -type ConsoleOutType = "stdout" | "stderr"; - -export const writeConsoleOutToDiv = ( - parentDiv: RefObject, - x: string, - type: ConsoleOutType, -) => { - if (x === "") return; - if (!parentDiv.current) return; - const styleClass = type === "stdout" ? "WorkerStdout" : "WorkerStderr"; - const preElement = document.createElement("pre"); - preElement.textContent = x; - const divElement = document.createElement("div"); - divElement.className = styleClass; - divElement.appendChild(preElement); - parentDiv.current.appendChild(divElement); -}; - export default ScriptEditor; diff --git a/gui/src/app/Scripting/webR/runR.ts b/gui/src/app/Scripting/webR/runR.ts index d712850e..9d56ee87 100644 --- a/gui/src/app/Scripting/webR/runR.ts +++ b/gui/src/app/Scripting/webR/runR.ts @@ -1,7 +1,7 @@ import { RefObject } from "react"; import { RString, WebR } from "webr"; import { InterpreterStatus } from "@SpScripting/InterpreterTypes"; -import { writeConsoleOutToDiv } from "@SpScripting/ScriptEditor"; +import { writeConsoleOutToDiv } from "@SpScripting/OutputDivUtils"; let webR: WebR | null = null; export const loadWebRInstance = async ( From a91f329f242a464e85668c0a9758b4d914bad593 Mon Sep 17 00:00:00 2001 From: Brian Ward Date: Fri, 26 Jul 2024 19:09:12 +0000 Subject: [PATCH 16/16] Clean up analysis.py callback --- .../Scripting/Analysis/AnalysisPyWindow.tsx | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/gui/src/app/Scripting/Analysis/AnalysisPyWindow.tsx b/gui/src/app/Scripting/Analysis/AnalysisPyWindow.tsx index 34076612..2adf7f0c 100644 --- a/gui/src/app/Scripting/Analysis/AnalysisPyWindow.tsx +++ b/gui/src/app/Scripting/Analysis/AnalysisPyWindow.tsx @@ -1,4 +1,4 @@ -import { FunctionComponent, useCallback, useMemo } from "react"; +import { FunctionComponent, RefObject, useCallback, useMemo } from "react"; import { StanRun } from "@SpStanSampler/useStanSampler"; import { FileNames } from "@SpCore/FileMapping"; import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; @@ -40,17 +40,7 @@ const AnalysisPyWindow: FunctionComponent = ({ () => ({ onStdout: (x: string) => writeConsoleOutToDiv(consoleRef, x, "stdout"), onStderr: (x: string) => writeConsoleOutToDiv(consoleRef, x, "stderr"), - onImage: (b64: string) => { - const imageUrl = `data:image/png;base64,${b64}`; - - const img = document.createElement("img"); - img.style.width = "100%"; - img.src = imageUrl; - - const divElement = document.createElement("div"); - divElement.appendChild(img); - imagesRef.current?.appendChild(divElement); - }, + onImage: (b64: string) => addImageToDiv(imagesRef, b64), onStatus, }), [consoleRef, imagesRef, onStatus], @@ -92,4 +82,16 @@ const AnalysisPyWindow: FunctionComponent = ({ ); }; +const addImageToDiv = (imagesRef: RefObject, b64: string) => { + const imageUrl = `data:image/png;base64,${b64}`; + + const img = document.createElement("img"); + img.style.width = "100%"; + img.src = imageUrl; + + const divElement = document.createElement("div"); + divElement.appendChild(img); + imagesRef.current?.appendChild(divElement); +}; + export default AnalysisPyWindow;