From 406eb2eff94d47759abf6b8911294b77efe5cc93 Mon Sep 17 00:00:00 2001 From: Jeremy Magland Date: Wed, 24 Jul 2024 18:00:50 -0400 Subject: [PATCH 1/5] click to generate code templates --- gui/src/app/FileEditor/TextEditor.tsx | 28 +++++++++++++++---- .../DataGenerationWindow/DataPyFileEditor.tsx | 6 ++++ .../DataGenerationWindow/DataRFileEditor.tsx | 6 ++++ gui/src/app/pyodide/AnalysisPyFileEditor.tsx | 18 ++++++++++++ 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/gui/src/app/FileEditor/TextEditor.tsx b/gui/src/app/FileEditor/TextEditor.tsx index a77b4b2f..b2a364c8 100644 --- a/gui/src/app/FileEditor/TextEditor.tsx +++ b/gui/src/app/FileEditor/TextEditor.tsx @@ -28,6 +28,7 @@ type Props = { label: string; codeMarkers?: CodeMarker[]; hintTextOnEmpty?: string; + onClickHintText?: () => void; }; export type ToolbarItem = @@ -56,6 +57,7 @@ const TextEditor: FunctionComponent = ({ label, codeMarkers, hintTextOnEmpty, + onClickHintText, }) => { const handleChange = useCallback( (value: string | undefined) => { @@ -108,12 +110,15 @@ const TextEditor: FunctionComponent = ({ if (text || editedText) { return; } - const contentWidget = createHintTextContentWidget(hintTextOnEmpty); + const contentWidget = createHintTextContentWidget( + hintTextOnEmpty, + onClickHintText, + ); editorInstance.addContentWidget(contentWidget); return () => { editorInstance.removeContentWidget(contentWidget); }; - }, [text, editorInstance, editedText, hintTextOnEmpty]); + }, [text, editorInstance, editedText, hintTextOnEmpty, onClickHintText]); ///////////////////////////////////////////////// @@ -242,14 +247,27 @@ const NotSelectable: FunctionComponent = ({ children }) => { return
{children}
; }; -const createHintTextContentWidget = (hintText: string) => { +const createHintTextContentWidget = ( + hintText: string, + onClick?: () => void, +) => { return { getDomNode: () => { const node = document.createElement("div"); node.style.width = "max-content"; - node.style.pointerEvents = "none"; - node.textContent = hintText; node.style.fontStyle = "italic"; + if (onClick) { + const link = document.createElement("a"); + link.textContent = hintText; + link.style.cursor = "pointer"; + link.style.color = "gray"; + link.style.textDecoration = "underline"; + link.onclick = onClick; + node.appendChild(link); + } else { + node.style.pointerEvents = "none"; + node.textContent = hintText; + } return node; }, getId: () => "hintText", diff --git a/gui/src/app/pages/HomePage/DataGenerationWindow/DataPyFileEditor.tsx b/gui/src/app/pages/HomePage/DataGenerationWindow/DataPyFileEditor.tsx index afbf4806..2e49b71f 100644 --- a/gui/src/app/pages/HomePage/DataGenerationWindow/DataPyFileEditor.tsx +++ b/gui/src/app/pages/HomePage/DataGenerationWindow/DataPyFileEditor.tsx @@ -96,8 +96,14 @@ const DataPyFileEditor: FunctionComponent = ({ onSetEditedText={setEditedFileContent} readOnly={readOnly} toolbarItems={toolbarItems} + hintTextOnEmpty="Click to create template for data generation" + onClickHintText={() => setEditedFileContent(dataPyTemplate)} /> ); }; +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 index b3296b05..b30bf330 100644 --- a/gui/src/app/pages/HomePage/DataGenerationWindow/DataRFileEditor.tsx +++ b/gui/src/app/pages/HomePage/DataGenerationWindow/DataRFileEditor.tsx @@ -149,8 +149,14 @@ json_result onSetEditedText={setEditedFileContent} readOnly={readOnly} toolbarItems={toolbarItems} + hintTextOnEmpty="Click to create template for data generation" + onClickHintText={() => setEditedFileContent(dataRTemplate)} /> ); }; +const dataRTemplate = `data <- list( + a = c(1, 2, 3) +)`; + export default DataRFileEditor; diff --git a/gui/src/app/pyodide/AnalysisPyFileEditor.tsx b/gui/src/app/pyodide/AnalysisPyFileEditor.tsx index 59afef92..96bc111a 100644 --- a/gui/src/app/pyodide/AnalysisPyFileEditor.tsx +++ b/gui/src/app/pyodide/AnalysisPyFileEditor.tsx @@ -168,10 +168,28 @@ const AnalysisPyFileEditor: FunctionComponent = ({ onSetEditedText={setEditedFileContent} readOnly={readOnly} toolbarItems={toolbarItems} + hintTextOnEmpty="Click to create analysis template" + onClickHintText={() => setEditedFileContent(analysisPyTemplate)} /> ); }; +const analysisPyTemplate = `import matplotlib.pyplot as plt + +# Get the parameter names +pnames = draws.parameter_names + +# Show histogram for first parameter +for pname in pnames: + print(pname) + samples = draws.get(pname) + print(samples.shape) + plt.hist(samples.ravel(), bins=30) + plt.title(pname) + plt.show() + break +`; + type ConsoleOutType = "stdout" | "stderr"; export const writeConsoleOutToDiv = ( From fb8ccf980921b7cf968244ad8caf3b2857094610 Mon Sep 17 00:00:00 2001 From: Jeremy Magland Date: Thu, 25 Jul 2024 11:12:17 -0400 Subject: [PATCH 2/5] Pass arbitrary contentOnEmpty span element to TextEditor --- gui/src/app/FileEditor/StanFileEditor.tsx | 2 +- gui/src/app/FileEditor/TextEditor.tsx | 36 +++++++------------ .../DataGenerationWindow/DataPyFileEditor.tsx | 18 ++++++++-- .../DataGenerationWindow/DataRFileEditor.tsx | 18 ++++++++-- gui/src/app/pyodide/AnalysisPyFileEditor.tsx | 18 ++++++++-- gui/src/localStyles.css | 4 ++- 6 files changed, 64 insertions(+), 32 deletions(-) diff --git a/gui/src/app/FileEditor/StanFileEditor.tsx b/gui/src/app/FileEditor/StanFileEditor.tsx index 922f1824..1ed6df8a 100644 --- a/gui/src/app/FileEditor/StanFileEditor.tsx +++ b/gui/src/app/FileEditor/StanFileEditor.tsx @@ -234,7 +234,7 @@ const StanFileEditor: FunctionComponent = ({ readOnly={!isCompiling ? readOnly : true} toolbarItems={toolbarItems} codeMarkers={stancErrorsToCodeMarkers(stancErrors)} - hintTextOnEmpty="Begin editing or select an example from the left panel" + contentOnEmpty="Begin editing or select an example from the left panel" /> {window} diff --git a/gui/src/app/FileEditor/TextEditor.tsx b/gui/src/app/FileEditor/TextEditor.tsx index 36173125..fc762abe 100644 --- a/gui/src/app/FileEditor/TextEditor.tsx +++ b/gui/src/app/FileEditor/TextEditor.tsx @@ -27,8 +27,7 @@ type Props = { toolbarItems?: ToolbarItem[]; label: string; codeMarkers?: CodeMarker[]; - hintTextOnEmpty?: string; - onClickHintText?: () => void; + contentOnEmpty?: string | HTMLSpanElement; }; export type ToolbarItem = @@ -56,8 +55,7 @@ const TextEditor: FunctionComponent = ({ language, label, codeMarkers, - hintTextOnEmpty, - onClickHintText, + contentOnEmpty, }) => { const handleChange = useCallback( (value: string | undefined) => { @@ -106,19 +104,17 @@ const TextEditor: FunctionComponent = ({ useEffect(() => { if (!editorInstance) return; - if (!hintTextOnEmpty) return; + if (!contentOnEmpty) return; if (text || editedText) { return; } - const contentWidget = createHintTextContentWidget( - hintTextOnEmpty, - onClickHintText, - ); + console.log("--- contentOnEmpty:", contentOnEmpty); + const contentWidget = createHintTextContentWidget(contentOnEmpty); editorInstance.addContentWidget(contentWidget); return () => { editorInstance.removeContentWidget(contentWidget); }; - }, [text, editorInstance, editedText, hintTextOnEmpty, onClickHintText]); + }, [text, editorInstance, editedText, contentOnEmpty]); ///////////////////////////////////////////////// @@ -247,26 +243,18 @@ const NotSelectable: FunctionComponent = ({ children }) => { return
{children}
; }; -const createHintTextContentWidget = ( - hintText: string, - onClick?: () => void, -) => { +const createHintTextContentWidget = (content: string | HTMLSpanElement) => { return { getDomNode: () => { const node = document.createElement("div"); node.style.width = "max-content"; node.className = "EditorHintText"; - if (onClick) { - const link = document.createElement("a"); - link.textContent = hintText; - link.style.cursor = "pointer"; - link.className = "EditorHintTextLink"; - link.onclick = onClick; - node.appendChild(link); - } else { - node.style.pointerEvents = "none"; - node.textContent = hintText; + const spanElement = + typeof content === "string" ? document.createElement("span") : content; + if (typeof content === "string") { + spanElement.textContent = content; } + node.appendChild(spanElement); return node; }, getId: () => "hintText", diff --git a/gui/src/app/pages/HomePage/DataGenerationWindow/DataPyFileEditor.tsx b/gui/src/app/pages/HomePage/DataGenerationWindow/DataPyFileEditor.tsx index 42fb517a..eed5aa0f 100644 --- a/gui/src/app/pages/HomePage/DataGenerationWindow/DataPyFileEditor.tsx +++ b/gui/src/app/pages/HomePage/DataGenerationWindow/DataPyFileEditor.tsx @@ -87,6 +87,21 @@ const DataPyFileEditor: FunctionComponent = ({ [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 ( = ({ onSetEditedText={setEditedFileContent} readOnly={readOnly} toolbarItems={toolbarItems} - hintTextOnEmpty="Click to create template for data generation" - onClickHintText={() => setEditedFileContent(dataPyTemplate)} + contentOnEmpty={contentOnEmpty} /> ); }; diff --git a/gui/src/app/pages/HomePage/DataGenerationWindow/DataRFileEditor.tsx b/gui/src/app/pages/HomePage/DataGenerationWindow/DataRFileEditor.tsx index 5c7cfdba..72b25e17 100644 --- a/gui/src/app/pages/HomePage/DataGenerationWindow/DataRFileEditor.tsx +++ b/gui/src/app/pages/HomePage/DataGenerationWindow/DataRFileEditor.tsx @@ -121,6 +121,21 @@ if (typeof(data) != "list") { [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(dataRTemplate); + }; + a1.textContent = "Click here to generate an example"; + spanElement.appendChild(t1); + spanElement.appendChild(a1); + return spanElement; + }, [setEditedFileContent]); + return ( setEditedFileContent(dataRTemplate)} + contentOnEmpty={contentOnEmpty} /> ); }; diff --git a/gui/src/app/pyodide/AnalysisPyFileEditor.tsx b/gui/src/app/pyodide/AnalysisPyFileEditor.tsx index 96bc111a..187a6543 100644 --- a/gui/src/app/pyodide/AnalysisPyFileEditor.tsx +++ b/gui/src/app/pyodide/AnalysisPyFileEditor.tsx @@ -158,6 +158,21 @@ const AnalysisPyFileEditor: FunctionComponent = ({ 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 ( = ({ onSetEditedText={setEditedFileContent} readOnly={readOnly} toolbarItems={toolbarItems} - hintTextOnEmpty="Click to create analysis template" - onClickHintText={() => setEditedFileContent(analysisPyTemplate)} + contentOnEmpty={contentOnEmpty} /> ); }; diff --git a/gui/src/localStyles.css b/gui/src/localStyles.css index d4ed6abc..1024392e 100644 --- a/gui/src/localStyles.css +++ b/gui/src/localStyles.css @@ -196,9 +196,11 @@ span.EditorTitle { .EditorHintText { font-style: italic; color: #aaa; + pointer-events: none; } -.EditorHintTextLink { +.EditorHintText a { color: gray; text-decoration: underline; + pointer-events: all; } From 7b768e5f4a3d006c36581189f227921aca6e533d Mon Sep 17 00:00:00 2001 From: Jeremy Magland Date: Thu, 25 Jul 2024 11:39:07 -0400 Subject: [PATCH 3/5] Remove unnecessary console log statement --- gui/src/app/FileEditor/TextEditor.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/gui/src/app/FileEditor/TextEditor.tsx b/gui/src/app/FileEditor/TextEditor.tsx index fc762abe..2eb3a34d 100644 --- a/gui/src/app/FileEditor/TextEditor.tsx +++ b/gui/src/app/FileEditor/TextEditor.tsx @@ -108,7 +108,6 @@ const TextEditor: FunctionComponent = ({ if (text || editedText) { return; } - console.log("--- contentOnEmpty:", contentOnEmpty); const contentWidget = createHintTextContentWidget(contentOnEmpty); editorInstance.addContentWidget(contentWidget); return () => { From 4c3df0f9e6405da06dc2b901f97cd6d1d580bcc2 Mon Sep 17 00:00:00 2001 From: Jeremy Magland Date: Thu, 25 Jul 2024 12:20:49 -0400 Subject: [PATCH 4/5] Update message for data.json and lp__ hist plotting --- .../DataGenerationWindow/DataRFileEditor.tsx | 2 +- gui/src/app/pyodide/AnalysisPyFileEditor.tsx | 19 ++++++++----------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/gui/src/app/pages/HomePage/DataGenerationWindow/DataRFileEditor.tsx b/gui/src/app/pages/HomePage/DataGenerationWindow/DataRFileEditor.tsx index 72b25e17..a8cb7841 100644 --- a/gui/src/app/pages/HomePage/DataGenerationWindow/DataRFileEditor.tsx +++ b/gui/src/app/pages/HomePage/DataGenerationWindow/DataRFileEditor.tsx @@ -124,7 +124,7 @@ if (typeof(data) != "list") { const contentOnEmpty = useMemo(() => { const spanElement = document.createElement("span"); const t1 = document.createTextNode( - "Define a dictionary called data to update the data.json. ", + "Define a list called data to update the data.json. ", ); const a1 = document.createElement("a"); a1.onclick = () => { diff --git a/gui/src/app/pyodide/AnalysisPyFileEditor.tsx b/gui/src/app/pyodide/AnalysisPyFileEditor.tsx index 187a6543..17cf0f32 100644 --- a/gui/src/app/pyodide/AnalysisPyFileEditor.tsx +++ b/gui/src/app/pyodide/AnalysisPyFileEditor.tsx @@ -191,17 +191,14 @@ const AnalysisPyFileEditor: FunctionComponent = ({ const analysisPyTemplate = `import matplotlib.pyplot as plt # Get the parameter names -pnames = draws.parameter_names - -# Show histogram for first parameter -for pname in pnames: - print(pname) - samples = draws.get(pname) - print(samples.shape) - plt.hist(samples.ravel(), bins=30) - plt.title(pname) - plt.show() - break +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"; From 7d507e5014fad150c5d984e6daffbbec17acf2f8 Mon Sep 17 00:00:00 2001 From: Jeremy Magland Date: Thu, 25 Jul 2024 12:28:42 -0400 Subject: [PATCH 5/5] Add prints for draws object and parameter names --- gui/src/app/pyodide/AnalysisPyFileEditor.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gui/src/app/pyodide/AnalysisPyFileEditor.tsx b/gui/src/app/pyodide/AnalysisPyFileEditor.tsx index 17cf0f32..a572aae2 100644 --- a/gui/src/app/pyodide/AnalysisPyFileEditor.tsx +++ b/gui/src/app/pyodide/AnalysisPyFileEditor.tsx @@ -190,7 +190,10 @@ const AnalysisPyFileEditor: FunctionComponent = ({ const analysisPyTemplate = `import matplotlib.pyplot as plt -# Get the parameter names +# Print the draws object +print(draws) + +# Print parameter names print(draws.parameter_names) # plot the lp parameter