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/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/Scripting/Analysis/AnalysisPyWindow.tsx b/gui/src/app/Scripting/Analysis/AnalysisPyWindow.tsx new file mode 100644 index 00000000..2adf7f0c --- /dev/null +++ b/gui/src/app/Scripting/Analysis/AnalysisPyWindow.tsx @@ -0,0 +1,97 @@ +import { FunctionComponent, RefObject, useCallback, useMemo } from "react"; +import { StanRun } from "@SpStanSampler/useStanSampler"; +import { FileNames } from "@SpCore/FileMapping"; +import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; +import useTemplatedFillerText from "@SpScripting/useTemplatedFillerText"; +import { + clearOutputDivs, + writeConsoleOutToDiv, +} from "@SpScripting/OutputDivUtils"; +import usePyodideWorker from "@SpScripting/pyodide/usePyodideWorker"; +import PlottingScriptEditor from "@SpScripting/PlottingScriptEditor"; +import useAnalysisState from "./useAnalysisState"; + +import analysisPyTemplate from "./analysis_template.py?raw"; + +export type GlobalDataForAnalysis = { + draws: number[][]; + paramNames: string[]; + numChains: number; +}; + +type AnalysisWindowProps = { + latestRun: StanRun; +}; + +const AnalysisPyWindow: FunctionComponent = ({ + latestRun, +}) => { + const { + consoleRef, + imagesRef, + spData, + status, + onStatus, + runnable, + notRunnableReason, + } = useAnalysisState(latestRun); + + const callbacks = useMemo( + () => ({ + onStdout: (x: string) => writeConsoleOutToDiv(consoleRef, x, "stdout"), + onStderr: (x: string) => writeConsoleOutToDiv(consoleRef, x, "stderr"), + onImage: (b64: string) => addImageToDiv(imagesRef, b64), + onStatus, + }), + [consoleRef, imagesRef, onStatus], + ); + + const { run } = usePyodideWorker(callbacks); + + const handleRun = useCallback( + (code: string) => { + clearOutputDivs(consoleRef, imagesRef); + run(code, spData, { + loadsDraws: true, + showsPlots: true, + producesData: false, + }); + }, + [consoleRef, imagesRef, run, spData], + ); + + const contentOnEmpty = useTemplatedFillerText( + "Use the draws object to access the samples. ", + analysisPyTemplate, + ProjectKnownFiles.ANALYSISPYFILE, + ); + + return ( + + ); +}; + +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; diff --git a/gui/src/app/Scripting/Analysis/AnalysisRWindow.tsx b/gui/src/app/Scripting/Analysis/AnalysisRWindow.tsx new file mode 100644 index 00000000..b8138577 --- /dev/null +++ b/gui/src/app/Scripting/Analysis/AnalysisRWindow.tsx @@ -0,0 +1,68 @@ +import { FunctionComponent, useCallback } from "react"; +import { StanRun } from "@SpStanSampler/useStanSampler"; +import { FileNames } from "@SpCore/FileMapping"; +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"; + +import analysisRTemplate from "./analysis_template.R?raw"; + +type AnalysisWindowProps = { + latestRun: StanRun; +}; + +const AnalysisRWindow: FunctionComponent = ({ + latestRun, +}) => { + const { + consoleRef, + imagesRef, + spData, + status, + onStatus, + runnable, + notRunnableReason, + } = useAnalysisState(latestRun); + + const handleRun = useCallback( + async (userCode: string) => { + clearOutputDivs(consoleRef, imagesRef); + const code = loadDrawsCode + userCode; + await runR({ + code, + imagesRef, + consoleRef, + onStatus, + spData, + }); + }, + [consoleRef, imagesRef, onStatus, spData], + ); + + const contentOnEmpty = useTemplatedFillerText( + "Use the draws object (a posterior::draws_array) to access the samples. ", + analysisRTemplate, + ProjectKnownFiles.ANALYSISRFILE, + ); + + return ( + + ); +}; + +export default AnalysisRWindow; diff --git a/gui/src/app/Scripting/Analysis/analysis_template.R b/gui/src/app/Scripting/Analysis/analysis_template.R new file mode 100644 index 00000000..e18fc1eb --- /dev/null +++ b/gui/src/app/Scripting/Analysis/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/Analysis/analysis_template.py b/gui/src/app/Scripting/Analysis/analysis_template.py new file mode 100644 index 00000000..ec932233 --- /dev/null +++ b/gui/src/app/Scripting/Analysis/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/Analysis/useAnalysisState.ts b/gui/src/app/Scripting/Analysis/useAnalysisState.ts new file mode 100644 index 00000000..7bdec3f7 --- /dev/null +++ b/gui/src/app/Scripting/Analysis/useAnalysisState.ts @@ -0,0 +1,71 @@ +import { + InterpreterStatus, + isInterpreterBusy, +} from "@SpScripting/InterpreterTypes"; +import { clearOutputDivs } from "@SpScripting/OutputDivUtils"; +import { StanRun } from "@SpStanSampler/useStanSampler"; +import { useEffect, useMemo, useRef, useState } from "react"; + +export type GlobalDataForAnalysis = { + draws: number[][]; + paramNames: string[]; + 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); + + useEffect(() => { + clearOutputDivs(consoleRef, imagesRef); + }, [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"); + + 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/DataGeneration/DataPyWindow.tsx b/gui/src/app/Scripting/DataGeneration/DataPyWindow.tsx new file mode 100644 index 00000000..bd6f5241 --- /dev/null +++ b/gui/src/app/Scripting/DataGeneration/DataPyWindow.tsx @@ -0,0 +1,77 @@ +import { FunctionComponent, useCallback, useMemo } from "react"; +import { FileNames } from "@SpCore/FileMapping"; +import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; +import useTemplatedFillerText from "@SpScripting/useTemplatedFillerText"; +import ScriptEditor from "@SpScripting/ScriptEditor"; +import { + clearOutputDivs, + writeConsoleOutToDiv, +} from "@SpScripting/OutputDivUtils"; +import usePyodideWorker from "@SpScripting/pyodide/usePyodideWorker"; +import useDataGenState from "./useDataGenState"; + +import dataPyTemplate from "./data_template.py?raw"; + +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(); + + const callbacks = useMemo( + () => ({ + onStdout: (x: string) => writeConsoleOutToDiv(consoleRef, x, "stdout"), + onStderr: (x: string) => writeConsoleOutToDiv(consoleRef, x, "stderr"), + onStatus, + onData, + }), + [consoleRef, onData, onStatus], + ); + + const { run } = usePyodideWorker(callbacks); + + const handleRun = useCallback( + (code: string) => { + clearOutputDivs(consoleRef); + run( + code, + {}, + { + loadsDraws: false, + showsPlots: false, + producesData: true, + }, + ); + }, + [consoleRef, run], + ); + + const contentOnEmpty = useTemplatedFillerText( + "Define a dictionary called data to update the data.json. ", + dataPyTemplate, + ProjectKnownFiles.DATAPYFILE, + ); + + return ( + + ); +}; + +export default DataPyWindow; diff --git a/gui/src/app/Scripting/DataGeneration/DataRWindow.tsx b/gui/src/app/Scripting/DataGeneration/DataRWindow.tsx new file mode 100644 index 00000000..91facd92 --- /dev/null +++ b/gui/src/app/Scripting/DataGeneration/DataRWindow.tsx @@ -0,0 +1,54 @@ +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"; +import useTemplatedFillerText from "@SpScripting/useTemplatedFillerText"; +import useDataGenState from "./useDataGenState"; + +import dataRTemplate from "./data_template.R?raw"; + +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(); + + const handleRun = useCallback( + async (code: string) => { + clearOutputDivs(consoleRef); + await runR({ code, consoleRef, onStatus, onData }); + }, + [consoleRef, onData, onStatus], + ); + + const contentOnEmpty = useTemplatedFillerText( + "Define a list called data to update the data.json. ", + dataRTemplate, + ProjectKnownFiles.DATARFILE, + ); + + return ( + + ); +}; + +export default DataRWindow; diff --git a/gui/src/app/Scripting/DataGeneration/data_template.R b/gui/src/app/Scripting/DataGeneration/data_template.R new file mode 100644 index 00000000..9ca55987 --- /dev/null +++ b/gui/src/app/Scripting/DataGeneration/data_template.R @@ -0,0 +1 @@ +data <- list(a=c(1, 2, 3)) diff --git a/gui/src/app/Scripting/DataGeneration/data_template.py b/gui/src/app/Scripting/DataGeneration/data_template.py new file mode 100644 index 00000000..8ac2e196 --- /dev/null +++ b/gui/src/app/Scripting/DataGeneration/data_template.py @@ -0,0 +1 @@ +data = {"a": [1, 2, 3]} diff --git a/gui/src/app/Scripting/DataGeneration/useDataGenState.ts b/gui/src/app/Scripting/DataGeneration/useDataGenState.ts new file mode 100644 index 00000000..892d8597 --- /dev/null +++ b/gui/src/app/Scripting/DataGeneration/useDataGenState.ts @@ -0,0 +1,49 @@ +import { useCallback, useContext, useRef, useState } from "react"; +import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; +import { writeConsoleOutToDiv } from "@SpScripting/OutputDivUtils"; +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); + + 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 !== lastData.current) { + lastData.current = dataJson; + 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], + ); + + return { consoleRef, status, onStatus: setStatus, onData }; +}; + +export default useDataGenState; diff --git a/gui/src/app/Scripting/InterpreterTypes.ts b/gui/src/app/Scripting/InterpreterTypes.ts new file mode 100644 index 00000000..bf3a60ca --- /dev/null +++ b/gui/src/app/Scripting/InterpreterTypes.ts @@ -0,0 +1,22 @@ +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); +}; + +export const isInterpreterBusy = (status: InterpreterStatus) => { + return ["loading", "installing", "running"].includes(status); +}; 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/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..97ec153f --- /dev/null +++ b/gui/src/app/Scripting/ScriptEditor.tsx @@ -0,0 +1,187 @@ +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[] = []; + if (onHelp !== undefined) { + 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; +}; + +const ConsoleOutputWindow: FunctionComponent = ({ + consoleRef, +}) => { + return
; +}; + +export default ScriptEditor; diff --git a/gui/src/app/pyodide/pyodideWorker/pyodideWorker.ts b/gui/src/app/Scripting/pyodide/pyodideWorker.ts similarity index 97% rename from gui/src/app/pyodide/pyodideWorker/pyodideWorker.ts rename to gui/src/app/Scripting/pyodide/pyodideWorker.ts index ed1ef590..049e0be2 100644 --- a/gui/src/app/pyodide/pyodideWorker/pyodideWorker.ts +++ b/gui/src/app/Scripting/pyodide/pyodideWorker.ts @@ -1,7 +1,7 @@ import { PyodideInterface, loadPyodide } from "pyodide"; +import { InterpreterStatus } from "@SpScripting/InterpreterTypes"; import { MessageFromPyodideWorker, - PyodideWorkerStatus, PyodideRunSettings, } from "./pyodideWorkerTypes"; import spDrawsScript from "./sp_load_draws.py?raw"; @@ -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/pyodide/pyodideWorkerTypes.ts similarity index 75% rename from gui/src/app/pyodide/pyodideWorker/pyodideWorkerTypes.ts rename to gui/src/app/Scripting/pyodide/pyodideWorkerTypes.ts index 44bf7b27..5e869d57 100644 --- a/gui/src/app/pyodide/pyodideWorker/pyodideWorkerTypes.ts +++ b/gui/src/app/Scripting/pyodide/pyodideWorkerTypes.ts @@ -1,4 +1,8 @@ import baseObjectCheck from "@SpUtil/baseObjectCheck"; +import { + InterpreterStatus, + isInterpreterStatus, +} from "@SpScripting/InterpreterTypes"; export type PyodideRunSettings = Partial<{ loadsDraws: boolean; @@ -31,7 +35,7 @@ export type MessageFromPyodideWorker = } | { type: "setStatus"; - status: PyodideWorkerStatus; + status: InterpreterStatus; } | { type: "setData"; @@ -48,27 +52,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/pyodide/sp_load_draws.py similarity index 100% rename from gui/src/app/pyodide/pyodideWorker/sp_load_draws.py rename to gui/src/app/Scripting/pyodide/sp_load_draws.py diff --git a/gui/src/app/pyodide/pyodideWorker/sp_patch_matplotlib.py b/gui/src/app/Scripting/pyodide/sp_patch_matplotlib.py similarity index 100% rename from gui/src/app/pyodide/pyodideWorker/sp_patch_matplotlib.py rename to gui/src/app/Scripting/pyodide/sp_patch_matplotlib.py diff --git a/gui/src/app/pyodide/pyodideWorker/usePyodideWorker.ts b/gui/src/app/Scripting/pyodide/usePyodideWorker.ts similarity index 95% rename from gui/src/app/pyodide/pyodideWorker/usePyodideWorker.ts rename to gui/src/app/Scripting/pyodide/usePyodideWorker.ts index 9d74a2c6..8b1151b1 100644 --- a/gui/src/app/pyodide/pyodideWorker/usePyodideWorker.ts +++ b/gui/src/app/Scripting/pyodide/usePyodideWorker.ts @@ -1,10 +1,11 @@ +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, - PyodideWorkerStatus, isMessageFromPyodideWorker, PyodideRunSettings, } from "./pyodideWorkerTypes"; @@ -12,7 +13,7 @@ import { 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 +98,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/useTemplatedFillerText.ts b/gui/src/app/Scripting/useTemplatedFillerText.ts new file mode 100644 index 00000000..a85877ac --- /dev/null +++ b/gui/src/app/Scripting/useTemplatedFillerText.ts @@ -0,0 +1,34 @@ +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, + 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/webR/runR.ts b/gui/src/app/Scripting/webR/runR.ts new file mode 100644 index 00000000..9d56ee87 --- /dev/null +++ b/gui/src/app/Scripting/webR/runR.ts @@ -0,0 +1,133 @@ +import { RefObject } from "react"; +import { RString, WebR } from "webr"; +import { InterpreterStatus } from "@SpScripting/InterpreterTypes"; +import { writeConsoleOutToDiv } from "@SpScripting/OutputDivUtils"; + +let webR: WebR | null = null; +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(); + + onStatus("installing"); + await sleep(100); // let the UI update + await w.installPackages(["jsonlite", "posterior"]); + + webR = w; + return webR; + } else { + return webR; + } +}; + +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) => { + try { + const webR = await loadWebRInstance(onStatus); + const shelter = await new webR.Shelter(); + + onStatus("running"); + await sleep(100); // let the UI update + let rCode = + ` +# redirect install.packages to webr's version +webr::shim_install() + +` + code; + + if (onData) { + rCode += ` +if (typeof(data) != "list") { +stop("[stan-playground] data must be a list") +} +.SP_DATA <- jsonlite::toJSON(data, pretty = TRUE, auto_unbox = TRUE) +invisible(.SP_DATA)`; + } + try { + 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); + } + }); + + 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 (onData) { + const result = JSON.parse(await (ret.result as RString).toString()); + onData(result); + } + } finally { + shelter.purge(); + } + onStatus("completed"); + } catch (e: any) { + console.error(e); + writeConsoleOutToDiv(consoleRef, e.toString(), "stderr"); + onStatus("failed"); + } +}; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export default runR; 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) diff --git a/gui/src/app/pages/HomePage/AnalysisPyWindow/AnalysisPyWindow.tsx b/gui/src/app/pages/HomePage/AnalysisPyWindow/AnalysisPyWindow.tsx deleted file mode 100644 index bc91de48..00000000 --- a/gui/src/app/pages/HomePage/AnalysisPyWindow/AnalysisPyWindow.tsx +++ /dev/null @@ -1,124 +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"; - -type AnalysisPyWindowProps = { - latestRun: StanRun; -}; - -const AnalysisPyWindow: FunctionComponent = ({ - latestRun, -}) => { - const imagesRef = useRef(null); - - useEffect(() => { - if (imagesRef.current) { - imagesRef.current.innerHTML = ""; - } - }, [latestRun.draws]); - - return ( - - - - - ); -}; - -type LeftPaneProps = { - imagesRef: RefObject; - latestRun: StanRun; -}; - -export type GlobalDataForAnalysisPy = { - draws: number[][]; - paramNames: string[]; - numChains: number; -}; - -const LeftPane: FunctionComponent = ({ - imagesRef, - latestRun, -}) => { - 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]); - return ( - - { - update({ - type: "editFile", - content, - filename: ProjectKnownFiles.ANALYSISPYFILE, - }); - }} - onSaveContent={() => { - update({ - type: "commitFile", - filename: ProjectKnownFiles.ANALYSISPYFILE, - }); - }} - consoleRef={consoleRef} - imagesRef={imagesRef} - readOnly={false} - spData={spData} - /> - - - ); -}; - -type ConsoleOutputWindowProps = { - consoleRef: RefObject; -}; - -export const ConsoleOutputWindow: FunctionComponent< - ConsoleOutputWindowProps -> = ({ consoleRef }) => { - return
; -}; - -type ImageOutputWindowProps = { - imagesRef: RefObject; -}; - -const ImageOutputWindow: FunctionComponent = ({ - imagesRef, -}) => { - return
; -}; - -export default AnalysisPyWindow; 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 b4f9dc06..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 { ConsoleOutputWindow } from "../AnalysisPyWindow/AnalysisPyWindow"; -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"; - -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 e0e2d412..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; -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..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"; @@ -20,8 +19,10 @@ import { } from "react"; import TabWidget from "@SpComponents/TabWidget"; import SamplingWindow from "./SamplingWindow/SamplingWindow"; -import DataGenerationWindow from "./DataGenerationWindow/DataGenerationWindow"; import { FileNames } from "@SpCore/FileMapping"; +import DataRWindow from "@SpScripting/DataGeneration/DataRWindow"; +import DataPyWindow from "@SpScripting/DataGeneration/DataPyWindow"; +import TextEditor from "@SpComponents/TextEditor"; type Props = { // @@ -85,7 +86,10 @@ const RightView: FunctionComponent = ({ return ( - + + + + ); }; @@ -124,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, diff --git a/gui/src/app/pages/HomePage/SamplingWindow/SamplingWindow.tsx b/gui/src/app/pages/HomePage/SamplingWindow/SamplingWindow.tsx index a896a20e..23310e16 100644 --- a/gui/src/app/pages/HomePage/SamplingWindow/SamplingWindow.tsx +++ b/gui/src/app/pages/HomePage/SamplingWindow/SamplingWindow.tsx @@ -1,19 +1,20 @@ 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 AnalysisPyWindow from "@SpScripting/Analysis/AnalysisPyWindow"; import useStanSampler, { StanRun } from "@SpStanSampler/useStanSampler"; -import RunPanel from "@SpComponents/RunPanel"; -import SamplingOptsPanel from "@SpComponents/SamplingOptsPanel"; -import SamplerOutputView from "@SpComponents/SamplerOutputView"; +import AnalysisRWindow from "@SpScripting/Analysis/AnalysisRWindow"; type SamplingWindowProps = { compiledMainJsUrl?: string; @@ -79,9 +80,7 @@ const SamplingResultsArea: FunctionComponent = ({ -
-
R analysis not yet implemented
-
+ ); }; diff --git a/gui/src/app/pyodide/AnalysisPyFileEditor.tsx b/gui/src/app/pyodide/AnalysisPyFileEditor.tsx deleted file mode 100644 index a572aae2..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 { GlobalDataForAnalysisPy } from "../pages/HomePage/AnalysisPyWindow/AnalysisPyWindow"; -import usePyodideWorker from "./pyodideWorker/usePyodideWorker"; -import TextEditor, { ToolbarItem } from "@SpComponents/TextEditor"; - -type Props = { - fileName: string; - fileContent: string; - onSaveContent: () => void; - editedFileContent: string; - setEditedFileContent: (text: string) => void; - readOnly: boolean; - imagesRef: RefObject; - consoleRef: RefObject; - spData: GlobalDataForAnalysisPy | 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; 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"],