diff --git a/packages/components/README.md b/packages/components/README.md index a74ee32d..63a98d34 100644 --- a/packages/components/README.md +++ b/packages/components/README.md @@ -20,7 +20,7 @@ Add to `App.js`: ```jsx import { SquiggleEditor } from "@quri/squiggle-components"; ; ``` @@ -50,7 +50,7 @@ export function DynamicSquiggleChart({ squiggleString }) { } else { return ( void; + onSubmit?: () => void; oneLine?: boolean; width?: number; height: number; @@ -17,6 +18,7 @@ interface CodeEditorProps { export const CodeEditor: FC = ({ value, onChange, + onSubmit, oneLine = false, showGutter = false, height, @@ -24,6 +26,10 @@ export const CodeEditor: FC = ({ const lineCount = value.split("\n").length; const id = useMemo(() => _.uniqueId(), []); + // this is necessary because AceEditor binds commands on mount, see https://github.com/securingsincity/react-ace/issues/684 + const onSubmitRef = useRef(null); + onSubmitRef.current = onSubmit; + return ( = ({ enableBasicAutocompletion: false, enableLiveAutocompletion: false, }} + commands={[ + { + name: "submit", + bindKey: { mac: "Cmd-Enter", win: "Ctrl-Enter" }, + exec: () => onSubmitRef.current?.(), + }, + ]} /> ); }; diff --git a/packages/components/src/components/SquiggleChart.tsx b/packages/components/src/components/SquiggleChart.tsx index e01d6920..cd480e69 100644 --- a/packages/components/src/components/SquiggleChart.tsx +++ b/packages/components/src/components/SquiggleChart.tsx @@ -8,20 +8,23 @@ import { defaultBindings, defaultEnvironment, } from "@quri/squiggle-lang"; -import { FunctionChartSettings } from "./FunctionChart"; import { useSquiggle } from "../lib/hooks"; import { SquiggleErrorAlert } from "./SquiggleErrorAlert"; import { SquiggleItem } from "./SquiggleItem"; export interface SquiggleChartProps { /** The input string for squiggle */ - squiggleString?: string; + code?: string; /** If the output requires monte carlo sampling, the amount of samples */ sampleCount?: number; /** The amount of points returned to draw the distribution */ environment?: environment; - /** If the result is a function, where the function starts, ends and the amount of stops */ - chartSettings?: FunctionChartSettings; + /** If the result is a function, where the function domain starts */ + diagramStart?: number; + /** If the result is a function, where the function domain ends */ + diagramStop?: number; + /** If the result is a function, the amount of stops sampled */ + diagramCount?: number; /** When the squiggle code gets reevaluated */ onChange?(expr: squiggleExpression | undefined): void; /** CSS width of the element */ @@ -56,10 +59,9 @@ export interface SquiggleChartProps { } const defaultOnChange = () => {}; -const defaultChartSettings = { start: 0, stop: 10, count: 20 }; -export const SquiggleChart: React.FC = ({ - squiggleString = "", +export const SquiggleChart: React.FC = React.memo(({ + code = "", environment, onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here height = 200, @@ -71,7 +73,9 @@ export const SquiggleChart: React.FC = ({ showControls = false, logX = false, expY = false, - chartSettings = defaultChartSettings, + diagramStart = 0, + diagramStop = 10, + diagramCount = 100, tickFormat, minX, maxX, @@ -79,40 +83,47 @@ export const SquiggleChart: React.FC = ({ title, distributionChartActions, }) => { - const { result } = useSquiggle({ - code: squiggleString, - bindings, - environment, - jsImports, - onChange, - }); + const result = useSquiggle({ + code, + bindings, + environment, + jsImports, + onChange, + }); - if (result.tag !== "Ok") { - return ; + if (result.tag !== "Ok") { + return ; + } + + let distributionPlotSettings = { + showControls, + showSummary, + logX, + expY, + format: tickFormat, + minX, + maxX, + color, + title, + actions: distributionChartActions, + }; + + let chartSettings = { + start: diagramStart, + stop: diagramStop, + count: diagramCount, + }; + + return ( + + ); } - - let distributionPlotSettings = { - showControls, - showSummary, - logX, - expY, - format: tickFormat, - minX, - maxX, - color, - title, - actions: distributionChartActions, - }; - - return ( - - ); -}; +); diff --git a/packages/components/src/components/SquiggleEditor.tsx b/packages/components/src/components/SquiggleEditor.tsx index cd51b7a0..027eecb9 100644 --- a/packages/components/src/components/SquiggleEditor.tsx +++ b/packages/components/src/components/SquiggleEditor.tsx @@ -1,18 +1,11 @@ -import React, { useState } from "react"; -import * as ReactDOM from "react-dom"; +import React from "react"; import { CodeEditor } from "./CodeEditor"; -import { - squiggleExpression, - environment, - bindings, - jsImports, - defaultEnvironment, -} from "@quri/squiggle-lang"; +import { environment, bindings, jsImports } from "@quri/squiggle-lang"; import { defaultImports, defaultBindings } from "@quri/squiggle-lang"; import { SquiggleContainer } from "./SquiggleContainer"; -import { useSquiggle, useSquigglePartial } from "../lib/hooks"; +import { SquiggleChart, SquiggleChartProps } from "./SquiggleChart"; +import { useSquigglePartial, useMaybeControlledValue } from "../lib/hooks"; import { SquiggleErrorAlert } from "./SquiggleErrorAlert"; -import { SquiggleItem } from "./SquiggleItem"; const WrappedCodeEditor: React.FC<{ code: string; @@ -29,113 +22,36 @@ const WrappedCodeEditor: React.FC<{ ); -export interface SquiggleEditorProps { - /** The input string for squiggle */ - initialSquiggleString?: string; - /** The width of the element */ - width?: number; - /** If the result is a function, where the function starts */ - diagramStart?: number; - /** If the result is a function, where the function ends */ - diagramStop?: number; - /** If the result is a function, how many points along the function it samples */ - diagramCount?: number; - /** When the environment changes. Used again for notebook magic */ - onChange?(expr: squiggleExpression | undefined): void; - /** Previous variable declarations */ - bindings?: bindings; - /** If the output requires monte carlo sampling, the amount of samples */ - environment?: environment; - /** JS Imports */ - jsImports?: jsImports; - /** Whether to show detail about types of the returns, default false */ - showTypes?: boolean; - /** Whether to give users access to graph controls */ - showControls?: boolean; - /** Whether to show a summary table */ - showSummary?: boolean; - /** Whether to log the x coordinate on distribution charts */ - logX?: boolean; - /** Whether to exp the y coordinate on distribution charts */ - expY?: boolean; -} +export type SquiggleEditorProps = SquiggleChartProps & { + defaultCode?: string; + onCodeChange?: (code: string) => void; +}; -export const SquiggleEditor: React.FC = ({ - initialSquiggleString = "", - width, - diagramStart = 0, - diagramStop = 10, - diagramCount = 20, - onChange, - bindings = defaultBindings, - environment, - jsImports = defaultImports, - showTypes = false, - showControls = false, - showSummary = false, - logX = false, - expY = false, -}: SquiggleEditorProps) => { - const [code, setCode] = useState(initialSquiggleString); - React.useEffect( - () => setCode(initialSquiggleString), - [initialSquiggleString] - ); - - const { result, observableRef } = useSquiggle({ - code, - bindings, - environment, - jsImports, - onChange, +export const SquiggleEditor: React.FC = (props) => { + const [code, setCode] = useMaybeControlledValue({ + value: props.code, + defaultValue: props.defaultCode ?? "", + onChange: props.onCodeChange, }); - const chartSettings = { - start: diagramStart, - stop: diagramStop, - count: diagramCount, - }; - - const distributionPlotSettings = { - showControls, - showSummary, - logX, - expY, - }; - + let chartProps = { ...props, code }; return ( -
- - - {result.tag === "Ok" ? ( - - ) : ( - - )} - -
+ + + + ); }; -export function renderSquiggleEditorToDom(props: SquiggleEditorProps) { - const parent = document.createElement("div"); - ReactDOM.render(, parent); - return parent; -} - export interface SquigglePartialProps { - /** The input string for squiggle */ - initialSquiggleString?: string; + /** The text inside the input (controlled) */ + code?: string; + /** The default text inside the input (unControlled) */ + defaultCode?: string; /** when the environment changes. Used again for notebook magic*/ onChange?(expr: bindings | undefined): void; + /** When the code changes */ + onCodeChange?(code: string): void; /** Previously declared variables */ bindings?: bindings; /** If the output requires monte carlo sampling, the amount of samples */ @@ -145,19 +61,21 @@ export interface SquigglePartialProps { } export const SquigglePartial: React.FC = ({ - initialSquiggleString = "", + code: controlledCode, + defaultCode = "", onChange, + onCodeChange, bindings = defaultBindings, environment, jsImports = defaultImports, }: SquigglePartialProps) => { - const [code, setCode] = useState(initialSquiggleString); - React.useEffect( - () => setCode(initialSquiggleString), - [initialSquiggleString] - ); + const [code, setCode] = useMaybeControlledValue({ + value: controlledCode, + defaultValue: defaultCode, + onChange: onCodeChange, + }); - const { result, observableRef } = useSquigglePartial({ + const result = useSquigglePartial({ code, bindings, environment, @@ -166,19 +84,9 @@ export const SquigglePartial: React.FC = ({ }); return ( -
- - - {result.tag !== "Ok" ? ( - - ) : null} - -
+ + + {result.tag !== "Ok" ? : null} + ); }; - -export function renderSquigglePartialToDom(props: SquigglePartialProps) { - const parent = document.createElement("div"); - ReactDOM.render(, parent); - return parent; -} diff --git a/packages/components/src/components/SquigglePlayground.tsx b/packages/components/src/components/SquigglePlayground.tsx index 2cd755b2..301d8892 100644 --- a/packages/components/src/components/SquigglePlayground.tsx +++ b/packages/components/src/components/SquigglePlayground.tsx @@ -1,15 +1,18 @@ -import React, { FC, Fragment, useState, useEffect } from "react"; -import ReactDOM from "react-dom"; +import React, { FC, useState, useEffect, useMemo } from "react"; import { Path, useForm, UseFormRegister, useWatch } from "react-hook-form"; import * as yup from "yup"; +import { useMaybeControlledValue } from "../lib/hooks"; import { yupResolver } from "@hookform/resolvers/yup"; -import { Tab } from "@headlessui/react"; import { ChartSquareBarIcon, + CheckCircleIcon, CodeIcon, CogIcon, CurrencyDollarIcon, EyeIcon, + PauseIcon, + PlayIcon, + RefreshIcon, } from "@heroicons/react/solid"; import clsx from "clsx"; @@ -20,10 +23,13 @@ import { CodeEditor } from "./CodeEditor"; import { JsonEditor } from "./JsonEditor"; import { ErrorAlert, SuccessAlert } from "./Alert"; import { SquiggleContainer } from "./SquiggleContainer"; +import { Toggle } from "./ui/Toggle"; +import { Checkbox } from "./ui/Checkbox"; +import { StyledTab } from "./ui/StyledTab"; interface PlaygroundProps { /** The initial squiggle string to put in the playground */ - initialSquiggleString?: string; + defaultCode?: string; /** How many pixels high is the playground */ height?: number; /** Whether to show the types of outputs in the playground */ @@ -44,104 +50,45 @@ interface PlaygroundProps { showEditor?: boolean; } -const schema = yup - .object() - .shape({ - sampleCount: yup - .number() - .required() - .positive() - .integer() - .default(1000) - .min(10) - .max(1000000), - xyPointLength: yup - .number() - .required() - .positive() - .integer() - .default(1000) - .min(10) - .max(10000), - chartHeight: yup.number().required().positive().integer().default(350), - leftSizePercent: yup - .number() - .required() - .positive() - .integer() - .min(10) - .max(100) - .default(50), - showTypes: yup.boolean(), - showControls: yup.boolean(), - showSummary: yup.boolean(), - showEditor: yup.boolean(), - logX: yup.boolean(), - expY: yup.boolean(), - showSettingsPage: yup.boolean().default(false), - diagramStart: yup - .number() - .required() - .positive() - .integer() - .default(0) - .min(0), - diagramStop: yup - .number() - .required() - .positive() - .integer() - .default(10) - .min(0), - diagramCount: yup - .number() - .required() - .positive() - .integer() - .default(20) - .min(2), - }) - .required(); +const schema = yup.object({}).shape({ + sampleCount: yup + .number() + .required() + .positive() + .integer() + .default(1000) + .min(10) + .max(1000000), + xyPointLength: yup + .number() + .required() + .positive() + .integer() + .default(1000) + .min(10) + .max(10000), + chartHeight: yup.number().required().positive().integer().default(350), + leftSizePercent: yup + .number() + .required() + .positive() + .integer() + .min(10) + .max(100) + .default(50), + showTypes: yup.boolean().required(), + showControls: yup.boolean().required(), + showSummary: yup.boolean().required(), + showEditor: yup.boolean().required(), + logX: yup.boolean().required(), + expY: yup.boolean().required(), + showSettingsPage: yup.boolean().default(false), + diagramStart: yup.number().required().positive().integer().default(0).min(0), + diagramStop: yup.number().required().positive().integer().default(10).min(0), + diagramCount: yup.number().required().positive().integer().default(20).min(2), +}); -type StyledTabProps = { - name: string; - icon: (props: React.ComponentProps<"svg">) => JSX.Element; -}; - -const StyledTab: React.FC = ({ name, icon: Icon }) => { - return ( - - {({ selected }) => ( - - )} - - ); -}; +type FormFields = yup.InferType; const HeadedSection: FC<{ title: string; children: React.ReactNode }> = ({ title, @@ -182,30 +129,256 @@ function InputItem({ ); } -function Checkbox({ - name, - label, +const SamplingSettings: React.FC<{ register: UseFormRegister }> = ({ register, -}: { - name: Path; - label: string; - register: UseFormRegister; -}) { - return ( -