Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
ef4ff07
Add presets
sverhoeven Nov 11, 2024
de00f07
color preset button same as others
sverhoeven Nov 11, 2024
13d8e82
Presist preset when sharing links or downloading/uploading
sverhoeven Nov 11, 2024
7745f75
prune config
sverhoeven Nov 11, 2024
f673732
Add preset to schema
sverhoeven Nov 12, 2024
09cb218
Merge branch '61-persist-app-state' into presets
sverhoeven Nov 12, 2024
1076119
Add version to preset + try to make preset always set
sverhoeven Nov 12, 2024
914c3da
Merge remote-tracking branch 'origin/main' into presets
sverhoeven Jan 8, 2025
ee1ac87
Merge remote-tracking branch 'origin/presets' into presets
sverhoeven Jan 8, 2025
090bca1
cli needs Config not ExperimentConfigSchema, use jq to filter
sverhoeven Jan 8, 2025
87b94cc
Moved presets resource to own module
sverhoeven Jan 8, 2025
9c8cc23
Make share link preset aware
sverhoeven Jan 8, 2025
a4f7706
When modifying reference config then also update permutation
sverhoeven Jan 8, 2025
825e2ac
Use preset config as default values
sverhoeven Jan 8, 2025
f942324
Combined preset and reference as defaults for editing a permutation
sverhoeven Jan 8, 2025
cc791e6
Perm has full config with reference parameters, after swap new refere…
sverhoeven Jan 9, 2025
e225a7a
Big refacfor
sverhoeven Jan 13, 2025
72190f4
Move config utility function to own module in class package
sverhoeven Jan 17, 2025
18db020
preset is single config not a ExperimentConfig
sverhoeven Jan 17, 2025
514dc90
Make Config compatible with modular forms
sverhoeven Jan 17, 2025
e0c4fc0
Fix reading from url and local storage + Move parsing of experiment c…
sverhoeven Jan 17, 2025
b71489f
Adjust to new schema + Move preset name to form header + make output …
sverhoeven Jan 17, 2025
dd67ef0
Merge remote-tracking branch 'origin/main' into presets
sverhoeven Jan 17, 2025
19e9984
Sync docs to implementation
sverhoeven Jan 17, 2025
dcc3e37
Fix tests
sverhoeven Jan 17, 2025
67e34b8
No more unit tests in app
sverhoeven Jan 17, 2025
b285614
Self review
sverhoeven Jan 17, 2025
5c6ba95
get rid of some partial configs
Peter9192 Jan 20, 2025
8f4e429
Simplify initialExperimentConfig
Peter9192 Jan 20, 2025
b2cc225
spellcheck
sverhoeven Jan 21, 2025
3838927
add llm disclaimer
sverhoeven Jan 21, 2025
acd213b
spellcheck
sverhoeven Jan 21, 2025
cb3a894
Merge remote-tracking branch 'origin/presets' into presets
sverhoeven Jan 21, 2025
10535e1
cast
sverhoeven Jan 21, 2025
a2707a8
Typo in component name
sverhoeven Jan 21, 2025
361df65
No need to set name+description already set in add form
sverhoeven Jan 21, 2025
db415e8
When uploading without preset fallback to name of default preset
sverhoeven Jan 21, 2025
ec5d964
Merge remote-tracking branch 'origin/presets' into presets
sverhoeven Jan 21, 2025
59ab045
Merge remote-tracking branch 'origin/review-presets' into presets
sverhoeven Jan 21, 2025
61d76cb
Not enough to do shallow copy, need full.
sverhoeven Jan 21, 2025
36f1bab
Format
sverhoeven Jan 21, 2025
26df268
Use generate to make a config
sverhoeven Jan 21, 2025
333f79f
Remove star icon
sverhoeven Jan 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions apps/class-solid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,16 @@ pnpm test -- --ui --headed
This allows you to trigger tests from the [playwright ui](https://playwright.dev/docs/test-ui-mode) and enable [watch mode](https://playwright.dev/docs/test-ui-mode#watch-mode).

## This project was created with the [Solid CLI](https://solid-cli.netlify.app)

## Presets

An experiment can get started from a preset.

The presets are stored in the `src/lib/presets/` directory.
The format is JSON with content adhering to the [JSON schema](https://github.com/classmodel/class-web/blob/main/packages/class/src/config.json).

The `src/lib/presets.ts` is used as an index of presets.
If you add a preset the `src/lib/presets.ts` file needs to be updated.

An experiment from a preset can be opened from a url like `?preset=<preset-name>`.
For example to load <src/lib/presets/death-valley.json> use `http://localhost:3000/?preset=Death%20Valley`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's cool! But now we have a share URL that says /?experiments=... and the preset url that says /?preset=... which defaults to a single experiment. Makes me wonder if we shouldn't provide one consistent way to encode URLs. See #102

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, yep lets look at the different search params later

29 changes: 16 additions & 13 deletions apps/class-solid/src/components/Analysis.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BmiClass } from "@classmodel/class/bmi";
import type { Config } from "@classmodel/class/config";
import type { ClassOutput } from "@classmodel/class/runner";
import type { PartialConfig } from "@classmodel/class/validate";
import {
type Accessor,
For,
Expand Down Expand Up @@ -59,30 +59,33 @@ interface FlatExperiment {
label: string;
color: string;
linestyle: string;
config: PartialConfig;
config: Config;
output?: ClassOutput;
}

// Create a derived store for looping over all outputs:
const flatExperiments: () => FlatExperiment[] = createMemo(() => {
return experiments
.filter((e) => e.running === false) // skip running experiments
.filter((e) => e.output.running === false) // skip running experiments
.flatMap((e, i) => {
const reference = {
color: colors[0],
linestyle: linestyles[i % 5],
label: e.name,
config: e.reference.config,
output: e.reference.output,
label: e.config.reference.name,
config: e.config.reference,
output: e.output.reference,
};

const permutations = e.permutations.map((p, j) => ({
label: `${e.name}/${p.name}`,
color: colors[(j + 1) % 10],
linestyle: linestyles[i % 5],
config: p.config,
output: p.output,
}));
const permutations = e.config.permutations.map((config, j) => {
const output = e.output.permutations[j];
return {
label: `${e.config.reference.name}/${config.name}`,
color: colors[(j + 1) % 10],
linestyle: linestyles[i % 5],
config,
output,
};
});
return [reference, ...permutations];
});
});
Expand Down
67 changes: 35 additions & 32 deletions apps/class-solid/src/components/Experiment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import {
createUniqueId,
onCleanup,
} from "solid-js";
import { unwrap } from "solid-js/store";
import { Button, buttonVariants } from "~/components/ui/button";
import { createArchive, toConfigBlob } from "~/lib/download";
import { findPresetByName } from "~/lib/presets";
import {
type Experiment,
addExperiment,
Expand Down Expand Up @@ -46,15 +48,17 @@ export function AddExperimentDialog(props: {
onClose: () => void;
open: boolean;
}) {
const initialExperiment = () => {
const defaultPreset = findPresetByName();
const initialExperimentConfig = createMemo(() => {
return {
name: `My experiment ${props.nextIndex}`,
description: "",
reference: { config: {} },
preset: "Default",
reference: {
...structuredClone(defaultPreset.config),
name: `My experiment ${props.nextIndex}`,
},
permutations: [],
running: false as const,
};
};
});

function setOpen(value: boolean) {
if (!value) {
Expand All @@ -66,19 +70,19 @@ export function AddExperimentDialog(props: {
<Dialog open={props.open} onOpenChange={setOpen}>
<DialogContent class="min-w-[33%]">
<DialogHeader>
<DialogTitle class="mr-10">Experiment</DialogTitle>
<DialogTitle class="mr-10 flex justify-between gap-1">
Experiment
<span class="text-gray-300 text-sm ">
Preset: {defaultPreset.config.name}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How hard would it be to make this editable via a dropdown?

</span>
</DialogTitle>
</DialogHeader>
<ExperimentConfigForm
id="experiment-form"
experiment={initialExperiment()}
experiment={initialExperimentConfig()}
onSubmit={(newConfig) => {
props.onClose();
const { title, description, ...strippedConfig } = newConfig;
addExperiment(
strippedConfig,
title ?? initialExperiment().name,
description ?? initialExperiment().description,
);
addExperiment(newConfig);
}}
/>
<DialogFooter>
Expand All @@ -104,20 +108,19 @@ export function ExperimentSettingsDialog(props: {
</DialogTrigger>
<DialogContent class="min-w-[33%]">
<DialogHeader>
<DialogTitle class="mr-10">Experiment</DialogTitle>
<DialogTitle class="mr-10 flex justify-between gap-1">
Experiment
<span class="text-gray-300 text-sm ">
Preset: {props.experiment.config.preset}
</span>
</DialogTitle>
Comment on lines +111 to +116
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should combine "from scratch (default preset)" and "from preset" into one.

</DialogHeader>
<ExperimentConfigForm
id="experiment-form"
experiment={props.experiment}
experiment={props.experiment.config}
onSubmit={(newConfig) => {
setOpen(false);
const { title, description, ...strippedConfig } = newConfig;
modifyExperiment(
props.experimentIndex,
strippedConfig,
title ?? props.experiment.name,
description ?? props.experiment.description,
);
modifyExperiment(props.experimentIndex, newConfig);
}}
/>
<DialogFooter>
Expand Down Expand Up @@ -164,14 +167,14 @@ function RunningIndicator(props: { progress: number | false }) {

function DownloadExperimentConfiguration(props: { experiment: Experiment }) {
const downloadUrl = createMemo(() => {
return URL.createObjectURL(toConfigBlob(props.experiment));
return URL.createObjectURL(toConfigBlob(unwrap(props.experiment.config)));
});

onCleanup(() => {
URL.revokeObjectURL(downloadUrl());
});

const filename = `class-${props.experiment.name}.json`;
const filename = `class-${props.experiment.config.reference.name}.json`;
return (
<a href={downloadUrl()} download={filename} type="application/json">
Configuration
Expand All @@ -182,7 +185,7 @@ function DownloadExperimentConfiguration(props: { experiment: Experiment }) {
function DownloadExperimentArchive(props: { experiment: Experiment }) {
const [url, setUrl] = createSignal<string>("");
createEffect(async () => {
const archive = await createArchive(props.experiment);
const archive = await createArchive(unwrap(props.experiment));
if (!archive) {
return;
}
Expand All @@ -191,7 +194,7 @@ function DownloadExperimentArchive(props: { experiment: Experiment }) {
onCleanup(() => URL.revokeObjectURL(objectUrl));
});

const filename = `class-${props.experiment.name}.zip`;
const filename = `class-${props.experiment.config.reference.name}.zip`;
return (
<a href={url()} download={filename} type="application/zip">
Configuration and output
Expand Down Expand Up @@ -236,9 +239,9 @@ export function ExperimentCard(props: {
aria-describedby={descriptionId}
>
<CardHeader>
<CardTitle id={id}>{experiment().name}</CardTitle>
<CardTitle id={id}>{experiment().config.reference.name}</CardTitle>
<CardDescription id={descriptionId}>
{experiment().description}
{experiment().config.reference.description}
</CardDescription>
</CardHeader>
<CardContent>
Expand All @@ -247,10 +250,10 @@ export function ExperimentCard(props: {
experimentIndex={experimentIndex()}
/>
</CardContent>
<CardFooter>
<CardFooter class="gap-1">
<Show
when={!experiment().running}
fallback={<RunningIndicator progress={experiment().running} />}
when={!experiment().output.running}
fallback={<RunningIndicator progress={experiment().output.running} />}
>
<DownloadExperiment experiment={experiment()} />
<ExperimentSettingsDialog
Expand Down
42 changes: 19 additions & 23 deletions apps/class-solid/src/components/ExperimentConfigForm.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { pruneDefaults } from "@classmodel/class/validate";
import type { Config } from "@classmodel/class/config";
import { pruneConfig } from "@classmodel/class/config_utils";
import { type SubmitHandler, createForm } from "@modular-forms/solid";
import { createMemo } from "solid-js";
import type { Experiment } from "~/lib/store";
import {
type NamedConfig,
jsonSchemaOfNamedConfig,
validate,
} from "./NamedConfig";
import { unwrap } from "solid-js/store";
import type { ExperimentConfig } from "~/lib/experiment_config";
import { findPresetByName } from "~/lib/presets";
import { ObjectField } from "./ObjectField";
import { ajvForm } from "./ajvForm";

Expand All @@ -16,24 +14,22 @@ export function ExperimentConfigForm({
onSubmit,
}: {
id: string;
experiment: Experiment;
onSubmit: (c: NamedConfig) => void;
experiment: ExperimentConfig;
onSubmit: (c: Config) => void;
}) {
const initialValues = createMemo(() => {
return {
title: experiment.name,
description: experiment.description,
...pruneDefaults(experiment.reference.config),
};
});
const [_, { Form, Field }] = createForm<NamedConfig>({
const preset = createMemo(() => findPresetByName(experiment.preset));

const initialValues = createMemo(() =>
pruneConfig(unwrap(experiment.reference), unwrap(preset().config)),
);
const [_, { Form, Field }] = createForm<Config>({
initialValues: initialValues(),
validate: ajvForm(validate),
validate: ajvForm(preset().validate),
});

const handleSubmit: SubmitHandler<NamedConfig> = (values, event) => {
// Use validate to coerce strings to numbers
validate(values);
const handleSubmit: SubmitHandler<Config> = (values, event) => {
// Use ajv to coerce strings to numbers and fill in defaults
preset().validate(values);
onSubmit(values);
};

Expand All @@ -46,8 +42,8 @@ export function ExperimentConfigForm({
>
<div>
<ObjectField
schema={jsonSchemaOfNamedConfig}
value={pruneDefaults(experiment.reference.config)}
schema={preset().schema}
value={initialValues()}
Field={Field}
/>
</div>
Expand Down
21 changes: 0 additions & 21 deletions apps/class-solid/src/components/NamedConfig.tsx

This file was deleted.

43 changes: 19 additions & 24 deletions apps/class-solid/src/components/PermutationSweepButton.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import { type Sweep, performSweep } from "@classmodel/class/sweep";
import type { Config } from "@classmodel/class/config";
import {
type PartialConfig,
overwriteDefaultsInJsonSchema,
} from "@classmodel/class/validate";
import { For, createMemo, createSignal } from "solid-js";
mergeConfigurations,
} from "@classmodel/class/config_utils";
import { type Sweep, performSweep } from "@classmodel/class/sweep";
import { For, createSignal } from "solid-js";
import { unwrap } from "solid-js/store";
import { Button } from "~/components/ui/button";
import {
type Experiment,
type Permutation,
runExperiment,
setExperiments,
} from "~/lib/store";
import { jsonSchemaOfNamedConfig } from "./NamedConfig";
import { type Experiment, runExperiment, setExperiments } from "~/lib/store";
import {
Dialog,
DialogContent,
Expand All @@ -35,28 +30,25 @@ function nameForPermutation(config: PartialConfig): string {
return chunks.join(",");
}

function config2permutation(config: PartialConfig): Permutation {
function config2permutation(reference: Config, config: PartialConfig): Config {
return {
config,
...mergeConfigurations(reference, config),
name: nameForPermutation(config),
description: "",
};
}

function configs2Permutations(configs: PartialConfig[]): Permutation[] {
return configs.map(config2permutation);
function configs2Permutations(
reference: Config,
configs: PartialConfig[],
): Config[] {
return configs.map((c) => config2permutation(reference, c));
}

export function PermutationSweepButton(props: {
experiment: Experiment;
experimentIndex: number;
}) {
const jsonSchemaOfPermutation = createMemo(() => {
return overwriteDefaultsInJsonSchema(
jsonSchemaOfNamedConfig,
unwrap(props.experiment.reference.config),
);
});

const sweeps: Sweep[] = [
{
section: "initialState",
Expand All @@ -76,9 +68,12 @@ export function PermutationSweepButton(props: {

function addSweep() {
const configs = performSweep(sweeps);
const perms = configs2Permutations(configs);
const perms = configs2Permutations(
unwrap(props.experiment.config.reference),
configs,
);
setOpen(false);
setExperiments(props.experimentIndex, "permutations", perms);
setExperiments(props.experimentIndex, "config", "permutations", perms);
runExperiment(props.experimentIndex);
}
const [open, setOpen] = createSignal(false);
Expand Down
Loading
Loading