Merge branch 'develop' into june-17-2022-adjustements

* develop:
  useSquiggle and useSquigglePartial hooks
This commit is contained in:
Ozzie Gooen 2022-06-17 06:51:10 -07:00
commit ca73bdc97f
8 changed files with 438 additions and 405 deletions

View File

@ -1,5 +1,5 @@
import _ from "lodash";
import React, { FC } from "react";
import React, { FC, useMemo } from "react";
import AceEditor from "react-ace";
import "ace-builds/src-noconflict/mode-golang";
@ -21,14 +21,15 @@ export const CodeEditor: FC<CodeEditorProps> = ({
showGutter = false,
height,
}) => {
let lineCount = value.split("\n").length;
let id = _.uniqueId();
const lineCount = value.split("\n").length;
const id = useMemo(() => _.uniqueId(), []);
return (
<AceEditor
value={value}
mode="golang"
theme="github"
width={"100%"}
width="100%"
fontSize={14}
height={String(height) + "px"}
minLines={oneLine ? lineCount : undefined}

View File

@ -1,7 +1,5 @@
import * as React from "react";
import {
run,
errorValueToString,
squiggleExpression,
bindings,
environment,
@ -9,253 +7,11 @@ import {
defaultImports,
defaultBindings,
defaultEnvironment,
declaration,
} from "@quri/squiggle-lang";
import { NumberShower } from "./NumberShower";
import { DistributionChart } from "./DistributionChart";
import { ErrorAlert } from "./Alert";
import { FunctionChart, FunctionChartSettings } from "./FunctionChart";
function getRange<a>(x: declaration<a>) {
let first = x.args[0];
switch (first.tag) {
case "Float": {
return { floats: { min: first.value.min, max: first.value.max } };
}
case "Date": {
return { time: { min: first.value.min, max: first.value.max } };
}
}
}
function getChartSettings<a>(x: declaration<a>): FunctionChartSettings {
let range = getRange(x);
let min = range.floats ? range.floats.min : 0;
let max = range.floats ? range.floats.max : 10;
return {
start: min,
stop: max,
count: 20,
};
}
interface VariableBoxProps {
heading: string;
children: React.ReactNode;
showTypes: boolean;
}
export const VariableBox: React.FC<VariableBoxProps> = ({
heading = "Error",
children,
showTypes = false,
}) => {
if (showTypes) {
return (
<div className="bg-white border border-grey-200 m-2">
<div className="border-b border-grey-200 p-3">
<header className="font-mono">{heading}</header>
</div>
<div className="p-3">{children}</div>
</div>
);
} else {
return <div>{children}</div>;
}
};
export interface SquiggleItemProps {
/** The input string for squiggle */
expression: squiggleExpression;
width?: number;
height: number;
/** Whether to show a summary of statistics for distributions */
showSummary: boolean;
/** Whether to show type information */
showTypes: boolean;
/** Whether to show users graph controls (scale etc) */
showControls: boolean;
/** Settings for displaying functions */
chartSettings: FunctionChartSettings;
/** Environment for further function executions */
environment: environment;
}
const SquiggleItem: React.FC<SquiggleItemProps> = ({
expression,
width,
height,
showSummary,
showTypes = false,
showControls = false,
chartSettings,
environment,
}) => {
switch (expression.tag) {
case "number":
return (
<VariableBox heading="Number" showTypes={showTypes}>
<div className="font-semibold text-slate-600">
<NumberShower precision={3} number={expression.value} />
</div>
</VariableBox>
);
case "distribution": {
let distType = expression.value.type();
return (
<VariableBox
heading={`Distribution (${distType})`}
showTypes={showTypes}
>
{distType === "Symbolic" && showTypes ? (
<div>{expression.value.toString()}</div>
) : null}
<DistributionChart
distribution={expression.value}
height={height}
width={width}
showSummary={showSummary}
showControls={showControls}
/>
</VariableBox>
);
}
case "string":
return (
<VariableBox heading="String" showTypes={showTypes}>
<span className="text-slate-400">"</span>
<span className="text-slate-600 font-semibold">
{expression.value}
</span>
<span className="text-slate-400">"</span>
</VariableBox>
);
case "boolean":
return (
<VariableBox heading="Boolean" showTypes={showTypes}>
{expression.value.toString()}
</VariableBox>
);
case "symbol":
return (
<VariableBox heading="Symbol" showTypes={showTypes}>
<span className="text-slate-500 mr-2">Undefined Symbol:</span>
<span className="text-slate-600">{expression.value}</span>
</VariableBox>
);
case "call":
return (
<VariableBox heading="Call" showTypes={showTypes}>
{expression.value}
</VariableBox>
);
case "array":
return (
<VariableBox heading="Array" showTypes={showTypes}>
{expression.value.map((r, i) => (
<div key={i} className="flex pt-1">
<div className="flex-none bg-slate-100 rounded-sm px-1">
<header className="text-slate-400 font-mono">{i}</header>
</div>
<div className="px-2 mb-2 grow">
<SquiggleItem
key={i}
expression={r}
width={width !== undefined ? width - 20 : width}
height={50}
showTypes={showTypes}
showControls={showControls}
chartSettings={chartSettings}
environment={environment}
showSummary={showSummary}
/>
</div>
</div>
))}
</VariableBox>
);
case "record":
return (
<VariableBox heading="Record" showTypes={showTypes}>
<div className="space-y-3">
{Object.entries(expression.value).map(([key, r]) => (
<div key={key} className="flex space-x-2">
<div className="flex-none">
<header className="text-slate-500 font-mono">{key}:</header>
</div>
<div className="px-2 grow bg-gray-50 border border-gray-100 rounded-sm">
<SquiggleItem
expression={r}
width={width !== undefined ? width - 20 : width}
height={height / 3}
showTypes={showTypes}
showSummary={showSummary}
showControls={showControls}
chartSettings={chartSettings}
environment={environment}
/>
</div>
</div>
))}
</div>
</VariableBox>
);
case "arraystring":
return (
<VariableBox heading="Array String" showTypes={showTypes}>
{expression.value.map((r) => `"${r}"`).join(", ")}
</VariableBox>
);
case "date":
return (
<VariableBox heading="Date" showTypes={showTypes}>
{expression.value.toDateString()}
</VariableBox>
);
case "timeDuration": {
return (
<VariableBox heading="Time Duration" showTypes={showTypes}>
<NumberShower precision={3} number={expression.value} />
</VariableBox>
);
}
case "lambda":
return (
<VariableBox heading="Function" showTypes={showTypes}>
<div className="text-amber-700 bg-amber-100 rounded-md font-mono p-1 pl-2 mb-3 mt-1 text-sm">{`function(${expression.value.parameters.join(
","
)})`}</div>
<FunctionChart
fn={expression.value}
chartSettings={chartSettings}
height={height}
environment={{
sampleCount: environment.sampleCount / 10,
xyPointLength: environment.xyPointLength / 10,
}}
/>
</VariableBox>
);
case "lambdaDeclaration": {
return (
<VariableBox heading="Function Declaration" showTypes={showTypes}>
<FunctionChart
fn={expression.value.fn}
chartSettings={getChartSettings(expression.value)}
height={height}
environment={{
sampleCount: environment.sampleCount / 10,
xyPointLength: environment.xyPointLength / 10,
}}
/>
</VariableBox>
);
}
default: {
return <>Should be unreachable</>;
}
}
};
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 */
@ -266,8 +22,8 @@ export interface SquiggleChartProps {
environment?: environment;
/** If the result is a function, where the function starts, ends and the amount of stops */
chartSettings?: FunctionChartSettings;
/** When the environment changes */
onChange?(expr: squiggleExpression): void;
/** When the squiggle code gets reevaluated */
onChange?(expr: squiggleExpression | undefined): void;
/** CSS width of the element */
width?: number;
height?: number;
@ -275,7 +31,7 @@ export interface SquiggleChartProps {
bindings?: bindings;
/** JS imported parameters */
jsImports?: jsImports;
/** Whether to show a summary of the distirbution */
/** Whether to show a summary of the distribution */
showSummary?: boolean;
/** Whether to show type information about returns, default false */
showTypes?: boolean;
@ -283,12 +39,13 @@ export interface SquiggleChartProps {
showControls?: boolean;
}
const defaultOnChange = () => {};
const defaultChartSettings = { start: 0, stop: 10, count: 20 };
export const SquiggleChart: React.FC<SquiggleChartProps> = ({
squiggleString = "",
environment,
onChange = () => {},
onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here
height = 200,
bindings = defaultBindings,
jsImports = defaultImports,
@ -298,28 +55,28 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = ({
showControls = false,
chartSettings = defaultChartSettings,
}) => {
let expressionResult = run(squiggleString, bindings, environment, jsImports);
if (expressionResult.tag !== "Ok") {
return (
<ErrorAlert heading={"Parse Error"}>
{errorValueToString(expressionResult.value)}
</ErrorAlert>
);
const { result } = useSquiggle({
code: squiggleString,
bindings,
environment,
jsImports,
onChange,
});
if (result.tag !== "Ok") {
return <SquiggleErrorAlert error={result.value} />;
}
let e = environment ?? defaultEnvironment;
let expression = expressionResult.value;
onChange(expression);
return (
<SquiggleItem
expression={expression}
expression={result.value}
width={width}
height={height}
showSummary={showSummary}
showTypes={showTypes}
showControls={showControls}
chartSettings={chartSettings}
environment={e}
environment={environment ?? defaultEnvironment}
/>
);
};

View File

@ -1,39 +1,51 @@
import * as React from "react";
import React, { useState } from "react";
import * as ReactDOM from "react-dom";
import { SquiggleChart } from "./SquiggleChart";
import { CodeEditor } from "./CodeEditor";
import type {
import {
squiggleExpression,
environment,
bindings,
jsImports,
defaultEnvironment,
} from "@quri/squiggle-lang";
import {
runPartial,
errorValueToString,
defaultImports,
defaultBindings,
} from "@quri/squiggle-lang";
import { ErrorAlert } from "./Alert";
import { defaultImports, defaultBindings } from "@quri/squiggle-lang";
import { SquiggleContainer } from "./SquiggleContainer";
import { useSquiggle, useSquigglePartial } from "../lib/hooks";
import { SquiggleErrorAlert } from "./SquiggleErrorAlert";
import { SquiggleItem } from "./SquiggleItem";
const WrappedCodeEditor: React.FC<{
code: string;
setCode: (code: string) => void;
}> = ({ code, setCode }) => (
<div className="border border-grey-200 p-2 m-4">
<CodeEditor
value={code}
onChange={setCode}
oneLine={true}
showGutter={false}
height={20}
/>
</div>
);
export interface SquiggleEditorProps {
/** The input string for squiggle */
initialSquiggleString?: string;
/** If the output requires monte carlo sampling, the amount of samples */
environment?: environment;
/** 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): void;
/** The width of the element */
width?: 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 */
@ -44,169 +56,109 @@ export interface SquiggleEditorProps {
showSummary?: boolean;
}
export let SquiggleEditor: React.FC<SquiggleEditorProps> = ({
export const SquiggleEditor: React.FC<SquiggleEditorProps> = ({
initialSquiggleString = "",
width,
environment,
diagramStart = 0,
diagramStop = 10,
diagramCount = 20,
onChange,
bindings = defaultBindings,
environment,
jsImports = defaultImports,
showTypes = false,
showControls = false,
showSummary = false,
}: SquiggleEditorProps) => {
const [expression, setExpression] = React.useState(initialSquiggleString);
const [code, setCode] = useState(initialSquiggleString);
const { result, observableRef } = useSquiggle({
code,
bindings,
environment,
jsImports,
onChange,
});
const chartSettings = {
start: diagramStart,
stop: diagramStop,
count: diagramCount,
};
return (
<div ref={observableRef}>
<SquiggleContainer>
<div>
<div className="border border-grey-200 p-2 m-4">
<CodeEditor
value={expression}
onChange={setExpression}
oneLine={true}
showGutter={false}
height={20}
/>
</div>
<SquiggleChart
<WrappedCodeEditor code={code} setCode={setCode} />
{result.tag === "Ok" ? (
<SquiggleItem
expression={result.value}
width={width}
environment={environment}
squiggleString={expression}
chartSettings={chartSettings}
onChange={onChange}
bindings={bindings}
jsImports={jsImports}
height={200}
showSummary={showSummary}
showTypes={showTypes}
showControls={showControls}
showSummary={showSummary}
chartSettings={chartSettings}
environment={environment ?? defaultEnvironment}
/>
</div>
) : (
<SquiggleErrorAlert error={result.value} />
)}
</SquiggleContainer>
</div>
);
};
export function renderSquiggleEditorToDom(props: SquiggleEditorProps) {
let parent = document.createElement("div");
ReactDOM.render(
<SquiggleEditor
{...props}
onChange={(expr) => {
// Typescript complains on two levels here.
// - Div elements don't have a value property
// - Even if it did (like it was an input element), it would have to
// be a string
//
// Which are reasonable in most web contexts.
//
// However we're using observable, neither of those things have to be
// true there. div elements can contain the value property, and can have
// the value be any datatype they wish.
//
// This is here to get the 'viewof' part of:
// viewof env = cell('normal(0,1)')
// to work
// @ts-ignore
parent.value = expr;
parent.dispatchEvent(new CustomEvent("input"));
if (props.onChange) props.onChange(expr);
}}
/>,
parent
);
const parent = document.createElement("div");
ReactDOM.render(<SquiggleEditor {...props} />, parent);
return parent;
}
export interface SquigglePartialProps {
/** The input string for squiggle */
initialSquiggleString?: string;
/** If the output requires monte carlo sampling, the amount of samples */
environment?: environment;
/** 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: bindings): void;
onChange?(expr: bindings | undefined): void;
/** Previously declared variables */
bindings?: bindings;
/** If the output requires monte carlo sampling, the amount of samples */
environment?: environment;
/** Variables imported from js */
jsImports?: jsImports;
/** Whether to give users access to graph controls */
showControls?: boolean;
}
export let SquigglePartial: React.FC<SquigglePartialProps> = ({
export const SquigglePartial: React.FC<SquigglePartialProps> = ({
initialSquiggleString = "",
onChange,
bindings = defaultBindings,
environment,
jsImports = defaultImports,
}: SquigglePartialProps) => {
const [expression, setExpression] = React.useState(initialSquiggleString);
const [error, setError] = React.useState<string | null>(null);
const [code, setCode] = useState(initialSquiggleString);
const runSquiggleAndUpdateBindings = () => {
const squiggleResult = runPartial(
expression,
const { result, observableRef } = useSquigglePartial({
code,
bindings,
environment,
jsImports
);
if (squiggleResult.tag === "Ok") {
if (onChange) onChange(squiggleResult.value);
setError(null);
} else {
setError(errorValueToString(squiggleResult.value));
}
};
React.useEffect(runSquiggleAndUpdateBindings, [expression]);
jsImports,
onChange,
});
return (
<div ref={observableRef}>
<SquiggleContainer>
<div>
<div className="border border-grey-200 p-2 m-4">
<CodeEditor
value={expression}
onChange={setExpression}
oneLine={true}
showGutter={false}
height={20}
/>
</div>
{error !== null ? (
<ErrorAlert heading="Error">{error}</ErrorAlert>
<WrappedCodeEditor code={code} setCode={setCode} />
{result.tag !== "Ok" ? (
<SquiggleErrorAlert error={result.value} />
) : null}
</div>
</SquiggleContainer>
</div>
);
};
export function renderSquigglePartialToDom(props: SquigglePartialProps) {
let parent = document.createElement("div");
ReactDOM.render(
<SquigglePartial
{...props}
onChange={(bindings) => {
// @ts-ignore
parent.value = bindings;
parent.dispatchEvent(new CustomEvent("input"));
if (props.onChange) props.onChange(bindings);
}}
/>,
parent
);
const parent = document.createElement("div");
ReactDOM.render(<SquigglePartial {...props} />, parent);
return parent;
}

View File

@ -0,0 +1,11 @@
import { errorValue, errorValueToString } from "@quri/squiggle-lang";
import React from "react";
import { ErrorAlert } from "./Alert";
type Props = {
error: errorValue;
};
export const SquiggleErrorAlert: React.FC<Props> = ({ error }) => {
return <ErrorAlert heading="Error">{errorValueToString(error)}</ErrorAlert>;
};

View File

@ -0,0 +1,250 @@
import * as React from "react";
import {
squiggleExpression,
environment,
declaration,
} from "@quri/squiggle-lang";
import { NumberShower } from "./NumberShower";
import { DistributionChart } from "./DistributionChart";
import { FunctionChart, FunctionChartSettings } from "./FunctionChart";
function getRange<a>(x: declaration<a>) {
const first = x.args[0];
switch (first.tag) {
case "Float": {
return { floats: { min: first.value.min, max: first.value.max } };
}
case "Date": {
return { time: { min: first.value.min, max: first.value.max } };
}
}
}
function getChartSettings<a>(x: declaration<a>): FunctionChartSettings {
const range = getRange(x);
const min = range.floats ? range.floats.min : 0;
const max = range.floats ? range.floats.max : 10;
return {
start: min,
stop: max,
count: 20,
};
}
interface VariableBoxProps {
heading: string;
children: React.ReactNode;
showTypes: boolean;
}
export const VariableBox: React.FC<VariableBoxProps> = ({
heading = "Error",
children,
showTypes = false,
}) => {
if (showTypes) {
return (
<div className="bg-white border border-grey-200 m-2">
<div className="border-b border-grey-200 p-3">
<header className="font-mono">{heading}</header>
</div>
<div className="p-3">{children}</div>
</div>
);
} else {
return <div>{children}</div>;
}
};
export interface SquiggleItemProps {
/** The input string for squiggle */
expression: squiggleExpression;
width?: number;
height: number;
/** Whether to show a summary of statistics for distributions */
showSummary: boolean;
/** Whether to show type information */
showTypes: boolean;
/** Whether to show users graph controls (scale etc) */
showControls: boolean;
/** Settings for displaying functions */
chartSettings: FunctionChartSettings;
/** Environment for further function executions */
environment: environment;
}
export const SquiggleItem: React.FC<SquiggleItemProps> = ({
expression,
width,
height,
showSummary,
showTypes = false,
showControls = false,
chartSettings,
environment,
}) => {
switch (expression.tag) {
case "number":
return (
<VariableBox heading="Number" showTypes={showTypes}>
<div className="font-semibold text-slate-600">
<NumberShower precision={3} number={expression.value} />
</div>
</VariableBox>
);
case "distribution": {
const distType = expression.value.type();
return (
<VariableBox
heading={`Distribution (${distType})`}
showTypes={showTypes}
>
{distType === "Symbolic" && showTypes ? (
<div>{expression.value.toString()}</div>
) : null}
<DistributionChart
distribution={expression.value}
height={height}
width={width}
showSummary={showSummary}
showControls={showControls}
/>
</VariableBox>
);
}
case "string":
return (
<VariableBox heading="String" showTypes={showTypes}>
<span className="text-slate-400">"</span>
<span className="text-slate-600 font-semibold">
{expression.value}
</span>
<span className="text-slate-400">"</span>
</VariableBox>
);
case "boolean":
return (
<VariableBox heading="Boolean" showTypes={showTypes}>
{expression.value.toString()}
</VariableBox>
);
case "symbol":
return (
<VariableBox heading="Symbol" showTypes={showTypes}>
<span className="text-slate-500 mr-2">Undefined Symbol:</span>
<span className="text-slate-600">{expression.value}</span>
</VariableBox>
);
case "call":
return (
<VariableBox heading="Call" showTypes={showTypes}>
{expression.value}
</VariableBox>
);
case "array":
return (
<VariableBox heading="Array" showTypes={showTypes}>
{expression.value.map((r, i) => (
<div key={i} className="flex pt-1">
<div className="flex-none bg-slate-100 rounded-sm px-1">
<header className="text-slate-400 font-mono">{i}</header>
</div>
<div className="px-2 mb-2 grow">
<SquiggleItem
key={i}
expression={r}
width={width !== undefined ? width - 20 : width}
height={50}
showTypes={showTypes}
showControls={showControls}
chartSettings={chartSettings}
environment={environment}
showSummary={showSummary}
/>
</div>
</div>
))}
</VariableBox>
);
case "record":
return (
<VariableBox heading="Record" showTypes={showTypes}>
<div className="space-y-3">
{Object.entries(expression.value).map(([key, r]) => (
<div key={key} className="flex space-x-2">
<div className="flex-none">
<header className="text-slate-500 font-mono">{key}:</header>
</div>
<div className="px-2 grow bg-gray-50 border border-gray-100 rounded-sm">
<SquiggleItem
expression={r}
width={width !== undefined ? width - 20 : width}
height={height / 3}
showTypes={showTypes}
showSummary={showSummary}
showControls={showControls}
chartSettings={chartSettings}
environment={environment}
/>
</div>
</div>
))}
</div>
</VariableBox>
);
case "arraystring":
return (
<VariableBox heading="Array String" showTypes={showTypes}>
{expression.value.map((r) => `"${r}"`).join(", ")}
</VariableBox>
);
case "date":
return (
<VariableBox heading="Date" showTypes={showTypes}>
{expression.value.toDateString()}
</VariableBox>
);
case "timeDuration": {
return (
<VariableBox heading="Time Duration" showTypes={showTypes}>
<NumberShower precision={3} number={expression.value} />
</VariableBox>
);
}
case "lambda":
return (
<VariableBox heading="Function" showTypes={showTypes}>
<div className="text-amber-700 bg-amber-100 rounded-md font-mono p-1 pl-2 mb-3 mt-1 text-sm">{`function(${expression.value.parameters.join(
","
)})`}</div>
<FunctionChart
fn={expression.value}
chartSettings={chartSettings}
height={height}
environment={{
sampleCount: environment.sampleCount / 10,
xyPointLength: environment.xyPointLength / 10,
}}
/>
</VariableBox>
);
case "lambdaDeclaration": {
return (
<VariableBox heading="Function Declaration" showTypes={showTypes}>
<FunctionChart
fn={expression.value.fn}
chartSettings={getChartSettings(expression.value)}
height={height}
environment={{
sampleCount: environment.sampleCount / 10,
xyPointLength: environment.xyPointLength / 10,
}}
/>
</VariableBox>
);
}
default: {
return <>Should be unreachable</>;
}
}
};

View File

@ -190,7 +190,7 @@ function Checkbox<T>({
);
}
const SquigglePlayground: FC<PlaygroundProps> = ({
export const SquigglePlayground: FC<PlaygroundProps> = ({
initialSquiggleString = "",
height = 500,
showTypes = false,
@ -207,9 +207,9 @@ const SquigglePlayground: FC<PlaygroundProps> = ({
sampleCount: 1000,
xyPointLength: 1000,
chartHeight: 150,
showTypes: showTypes,
showControls: showControls,
showSummary: showSummary,
showTypes,
showControls,
showSummary,
leftSizePercent: 50,
showSettingsPage: false,
diagramStart: 0,
@ -414,9 +414,9 @@ const SquigglePlayground: FC<PlaygroundProps> = ({
height={vars.chartHeight}
showTypes={vars.showTypes}
showControls={vars.showControls}
showSummary={vars.showSummary}
bindings={defaultBindings}
jsImports={imports}
showSummary={vars.showSummary}
/>
</div>
</div>
@ -426,7 +426,6 @@ const SquigglePlayground: FC<PlaygroundProps> = ({
);
};
export default SquigglePlayground;
export function renderSquigglePlaygroundToDom(props: PlaygroundProps) {
const parent = document.createElement("div");
ReactDOM.render(<SquigglePlayground {...props} />, parent);

View File

@ -6,7 +6,7 @@ export {
renderSquigglePartialToDom,
} from "./components/SquiggleEditor";
export {
default as SquigglePlayground,
SquigglePlayground,
renderSquigglePlaygroundToDom,
} from "./components/SquigglePlayground";
export { SquiggleContainer } from "./components/SquiggleContainer";

View File

@ -0,0 +1,63 @@
import {
bindings,
environment,
jsImports,
run,
runPartial,
} from "@quri/squiggle-lang";
import { useEffect, useMemo, useRef } from "react";
type SquiggleArgs<T extends ReturnType<typeof run | typeof runPartial>> = {
code: string;
bindings?: bindings;
jsImports?: jsImports;
environment?: environment;
onChange?: (expr: Extract<T, { tag: "Ok" }>["value"] | undefined) => void;
};
const useSquiggleAny = <T extends ReturnType<typeof run | typeof runPartial>>(
args: SquiggleArgs<T>,
f: (...args: Parameters<typeof run>) => T
) => {
// We're using observable, where div elements can have a `value` property:
// https://observablehq.com/@observablehq/introduction-to-views
//
// This is here to get the 'viewof' part of:
// viewof env = cell('normal(0,1)')
// to work
const ref = useRef<
HTMLDivElement & { value?: Extract<T, { tag: "Ok" }>["value"] }
>(null);
const result: T = useMemo<T>(
() => f(args.code, args.bindings, args.environment, args.jsImports),
[f, args.code, args.bindings, args.environment, args.jsImports]
);
useEffect(() => {
if (!ref.current) return;
ref.current.value = result.tag === "Ok" ? result.value : undefined;
ref.current.dispatchEvent(new CustomEvent("input"));
}, [result]);
const { onChange } = args;
useEffect(() => {
onChange?.(result.tag === "Ok" ? result.value : undefined);
}, [result, onChange]);
return {
result, // squiggleExpression or externalBindings
observableRef: ref, // can be passed to outermost <div> if you want to use your component as an observablehq's view
};
};
export const useSquigglePartial = (
args: SquiggleArgs<ReturnType<typeof runPartial>>
) => {
return useSquiggleAny(args, runPartial);
};
export const useSquiggle = (args: SquiggleArgs<ReturnType<typeof run>>) => {
return useSquiggleAny(args, run);
};