Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
5 changes: 5 additions & 0 deletions .changeset/fold-optimization-into-experiments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hashintel/petrinaut": patch
---

Removes the Optimizations tab; a study now runs from a sweep's Parameters card.
Comment thread
kube marked this conversation as resolved.
5 changes: 5 additions & 0 deletions .changeset/metric-sample-not-errored-runs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hashintel/petrinaut-core": patch
---

Metric specs can sample every run that has not errored with `sampleRuns: "notErrored"`.
5 changes: 0 additions & 5 deletions .changeset/optimization-dedicated-view.md

This file was deleted.

2 changes: 1 addition & 1 deletion .changeset/simulate-drawer-frame.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
"@hashintel/petrinaut": patch
---

The Simulate drawers and the full optimization view share one frame with a condensing header, a Parameters band across the body and fixed-height cards, and the Summary section folds into the header.
The Simulate drawers share one frame with a condensing header, a Parameters band across the body and fixed-height cards, and the Summary section folds into the header.
2 changes: 0 additions & 2 deletions apps/petrinaut-website/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,3 @@ OPENAI_API_KEY=sk-xxxx
OPENAI_VOICE_API_KEY=
PETRINAUT_OPENAI_VOICE_ENABLED=false
VITE_BRUNCH_CHAT_ENDPOINT=
# "service" enables the /optimization route against the Python optimizer service behind the dev proxy; `turbo run dev -- --with-optimizer-service` sets it. Unset hides the route. The main demo runs its optimizer in the browser.
# VITE_PETRINAUT_OPT_PROVIDER=service
36 changes: 8 additions & 28 deletions apps/petrinaut-website/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,32 +73,14 @@ use provider-pattern discovery instead.

### Optimization demo

The main demo at [http://localhost:5173](http://localhost:5173) runs the
optimizer in the browser: the Optuna study runs in a Pyodide web worker and
each optimization step runs on Petrinaut's own experiments backend, so no
Python service is involved. The **Optimizations** tab appears once the
experimental **In-browser optimization** setting is on, under **Viewport
controls > Settings > Simulation**. The first optimization in a browser
downloads the Python runtime from jsDelivr and Optuna from PyPI; later runs use
the browser cache.

The `/optimization` route is the Python-service variant. It returns the
website's not-found page unless `VITE_PETRINAUT_OPT_PROVIDER=service` is set.
To run it, from the repository root:

```sh
turbo run dev --filter @apps/petrinaut-website -- --with-optimizer-service
```

The flag builds and starts the local Petrinaut Opt Docker image, waits for its
health endpoint, and starts the website with
`VITE_PETRINAUT_OPT_PROVIDER=service`. Open
[http://localhost:5173/optimization](http://localhost:5173/optimization).
Stopping the command also stops and removes its optimizer container.

The development server proxies `/api/petrinaut-opt/*` to the optimizer on
`127.0.0.1:4004`, avoiding development-only CORS changes to the Python service.
Storybook provides a fake optimizer for isolated UI development.
The demo at [http://localhost:5173](http://localhost:5173) runs the optimizer
in the browser: the Optuna study runs in a Pyodide web worker and each
optimization step runs on Petrinaut's own experiments backend, so no Python
service is involved. With the experimental **Parameter sweeps** and
**In-browser optimization** settings on, under **Viewport controls > Settings >
Simulation**, a sweep's Parameters card in the Experiments tab offers
**Optimize**. The first optimization in a browser downloads the Python runtime
from jsDelivr and Optuna from PyPI; later runs use the browser cache.

## Environment variables

Expand All @@ -108,9 +90,7 @@ Storybook provides a fake optimizer for isolated UI development.
| `OPENAI_VOICE_API_KEY` | for voice | voice API | Dedicated OpenAI key used to create Realtime WebRTC calls. |
| `PETRINAUT_OPENAI_VOICE_ENABLED` | no | voice API | Set to `true` to enable voice, including in production. |
| `PETRINAUT_AI_MODEL` | no | `api/chat.ts` | Overrides the default OpenAI model id. |
| `PETRINAUT_OPT_ORIGIN` | no | `vite.config.ts` | Overrides the local optimizer proxy target. |
| `VITE_BRUNCH_CHAT_ENDPOINT` | for Brunch | website | Base URL of the mounted Brunch Flue route. |
| `VITE_PETRINAUT_OPT_PROVIDER` | no | website | Set to `service` to enable the `/optimization` route. |
| `SENTRY_DSN` | no | `vite.config.ts` | Wired into the bundle via `__SENTRY_DSN__` at build time. |

Local values live in `.env.local`; Vite's `loadEnv` (see [`vite.config.ts`](vite.config.ts)) copies them into `process.env` for both the dev server and the API functions. In production, set these in the Vercel project settings.
Expand Down
1 change: 0 additions & 1 deletion apps/petrinaut-website/docs/task-dependencies.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
"env": [
"SENTRY_DSN",
"VITE_BRUNCH_CHAT_ENDPOINT",
"VITE_PETRINAUT_OPT_PROVIDER",
"VITE_VERCEL_ENV"
]
},
Expand Down
11 changes: 4 additions & 7 deletions apps/petrinaut-website/scripts/dev.sh
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
#!/usr/bin/env bash
# The website's dev task. With --with-optimizer-service it also builds and
# starts the local Petrinaut Optimizer, so the /optimization route runs studies
# for real; every other argument goes to Vite:
# The website's dev task: regenerates the example artifacts, then starts Vite.
# Every argument goes to Vite:
#
# turbo run dev --filter @apps/petrinaut-website -- --with-optimizer-service
# turbo run dev --filter @apps/petrinaut-website -- --port 5175 --strictPort
set -euo pipefail
cd "$(dirname "$0")/.."
. ../../libs/@local/petrinaut-optimizer-client/scripts/optimizer-service.sh
optimizer_service_parse "$@"
yarn examples:generate
# Vite is run by path: Yarn hides a dependency's bin from `yarn run` when the
# workspace also declares one of that dependency's peers (`@types/node` here),
# so `yarn vite` fails with "Couldn't find a script named vite".
vite_bin="$(node -p 'require("path").join(require("path").dirname(require.resolve("vite/package.json")), "bin", "vite.js")')"
run_dev_server node "$vite_bin" ${OPTIMIZER_FORWARDED[@]+"${OPTIMIZER_FORWARDED[@]}"}
exec node "$vite_bin" "$@"
35 changes: 18 additions & 17 deletions apps/petrinaut-website/src/examples/example-search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,22 +73,23 @@ describe("example search contract", () => {
).toBe("itemId=place-1&itemType=place&scenario=scenario-1&subnet=subnet-1");
});

it("carries the presentation of an open optimization, dropping anything else", () => {
expect(validateSharedExampleSearch({ present: "full" }).present).toBe(
"full",
);
expect(validateSharedExampleSearch({ present: "drawer" }).present).toBe(
"drawer",
);
expect(
validateSharedExampleSearch({ present: "sideways" }).present,
).toBeUndefined();
expect(
canonicalSearchString({
present: "full",
view: "optimizations",
mode: "simulate",
}),
).toBe("mode=simulate&present=full&view=optimizations");
it("normalises a link to the retired Optimizations section", () => {
// Links shared before optimization folded into the Experiments tab named
// that section and a `present` param; both drop out, and the page opens on
// the editor's default section in Simulate mode.
const search = validateSharedExampleSearch({
mode: "simulate",
view: "optimizations",
present: "full",
overlay: "create-optimization",
});
expect(search).toEqual({
scenario: undefined,
subnet: undefined,
mode: "simulate",
view: undefined,
overlay: undefined,
});
expect(canonicalSearchString(search)).toBe("mode=simulate");
});
});
13 changes: 0 additions & 13 deletions apps/petrinaut-website/src/examples/example-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,18 @@ export const sharedSimulateViews = [
"scenarios",
"metrics",
"experiments",
"optimizations",
] as const;

export const sharedOverlays = [
"viewport-settings",
"create-scenario",
"create-metric",
"create-experiment",
"create-optimization",
] as const;

/** How the Simulate section presents an open optimization. */
export const sharedPresentations = ["drawer", "full"] as const;

export type SharedMode = (typeof sharedModes)[number];
export type SharedSimulateView = (typeof sharedSimulateViews)[number];
export type SharedOverlay = (typeof sharedOverlays)[number];
export type SharedPresentation = (typeof sharedPresentations)[number];

/**
* Search params understood by every example surface. A URL carries at most one
Expand All @@ -64,7 +58,6 @@ export type SharedExampleSearch = {
mode?: SharedMode;
view?: SharedSimulateView;
overlay?: SharedOverlay;
present?: SharedPresentation;
};

/** The keys this contract owns. Anything else in a URL is foreign. */
Expand All @@ -76,7 +69,6 @@ const sharedSearchKeys = [
"mode",
"view",
"overlay",
"present",
] as const satisfies readonly (keyof SharedExampleSearch)[];

// `.catch(undefined)` is the contract's whole validation story: anything a URL
Expand All @@ -94,10 +86,6 @@ const optionalSimulateView = z
.optional()
.catch(undefined);
const optionalOverlay = z.enum(sharedOverlays).optional().catch(undefined);
const optionalPresentation = z
.enum(sharedPresentations)
.optional()
.catch(undefined);

/** The focused item, when the URL names a complete one. */
export const selectionFromInput = (
Expand Down Expand Up @@ -129,7 +117,6 @@ export const validateSharedExampleSearch = (
mode: optionalMode.parse(input.mode),
view: optionalSimulateView.parse(input.view),
overlay: optionalOverlay.parse(input.overlay),
present: optionalPresentation.parse(input.present),
...selectionToSearch(selectionFromInput(input)),
});

Expand Down
40 changes: 28 additions & 12 deletions apps/petrinaut-website/src/examples/navigation-search.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";

import { sharedOverlays, sharedSimulateViews } from "./example-search";
import {
applyPreviewNavigationUpdate,
navigationStateToSharedSearch,
Expand Down Expand Up @@ -42,24 +43,39 @@ describe("navigation state projection", () => {
expect(state.mode).toBe("edit");
expect(state.overlay).toBeNull();
expect(state.simulateResource).toBeNull();
expect(state.simulatePresentation).toBe("drawer");
});

it("round-trips the full presentation and omits the drawer baseline", () => {
const full = sharedSearchToNavigationState({
it("round-trips every Simulate section and overlay the URL can name", () => {
for (const view of sharedSimulateViews) {
for (const overlay of sharedOverlays) {
const state = sharedSearchToNavigationState({
mode: "simulate",
view,
overlay,
});
expect(state.simulateView).toBe(view);
expect(state.overlay).toEqual({ type: overlay });
// The projection omits whatever sits at the baseline, so the property
// is that decoding it lands on the same location.
expect(
sharedSearchToNavigationState(navigationStateToSharedSearch(state)),
).toEqual(state);
}
}
});

it("omits the fields that sit at the baseline", () => {
const state = sharedSearchToNavigationState({
mode: "simulate",
view: "optimizations",
present: "full",
view: "experiments",
});
expect(full.simulatePresentation).toBe("full");
expect(navigationStateToSharedSearch(full).present).toBe("full");

const drawer = sharedSearchToNavigationState({
expect(navigationStateToSharedSearch(state)).toEqual({
scenario: undefined,
subnet: undefined,
mode: "simulate",
view: "optimizations",
view: undefined,
overlay: undefined,
});
expect(drawer.simulatePresentation).toBe("drawer");
expect(navigationStateToSharedSearch(drawer).present).toBeUndefined();
});
});

Expand Down
35 changes: 7 additions & 28 deletions apps/petrinaut-website/src/examples/navigation-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@
* Projects the example URL contract onto Petrinaut's navigation state.
*
* The URL carries the location a reader can act on: the scenario, the subnet,
* the focused item, the editor's mode, its Simulate section, the overlay it
* has open and how it presents an open optimization. It deliberately leaves
* out `simulateResource`, which names a run or a record inside the open
* document rather than a place in the app.
* the focused item, the editor's mode, its Simulate section and the overlay it
* has open. It deliberately leaves out `simulateResource`, which names a run
* or a record inside the open document rather than a place in the app.
*
* Every field is decoded against a BASELINE — the location its page starts
* from. A URL that does not name a field means "the baseline's value", which is
Expand All @@ -21,7 +20,6 @@ import {
type SharedExampleSearch,
type SharedMode,
type SharedOverlay,
type SharedPresentation,
type SharedSimulateView,
} from "./example-search";

Expand All @@ -31,7 +29,6 @@ import type {
PetrinautNavigationOverlay,
PetrinautNavigationState,
PetrinautNavigationUpdater,
PetrinautSimulatePresentation,
SimulateViewMode,
} from "@hashintel/petrinaut/react";

Expand Down Expand Up @@ -76,14 +73,6 @@ const overlayFromSearch = (
overlay: SharedOverlay,
): PetrinautNavigationOverlay => ({ type: overlay });

const presentationToSearch = (
presentation: PetrinautSimulatePresentation,
): SharedPresentation => presentation;

const presentationFromSearch = (
presentation: SharedPresentation,
): PetrinautSimulatePresentation => presentation;

export const sharedSearchToNavigationState = (
search: SharedExampleSearch,
baseline: PetrinautNavigationState = defaultPetrinautNavigationState,
Expand All @@ -98,10 +87,6 @@ export const sharedSearchToNavigationState = (
search.overlay === undefined
? baseline.overlay
: overlayFromSearch(search.overlay),
simulatePresentation:
search.present === undefined
? baseline.simulatePresentation
: presentationFromSearch(search.present),
});

export const navigationStateToSharedSearch = (
Expand All @@ -111,7 +96,6 @@ export const navigationStateToSharedSearch = (
const mode = modeToSearch(state.mode);
const view = simulateViewToSearch(state.simulateView);
const overlay = overlayToSearch(state.overlay);
const present = presentationToSearch(state.simulatePresentation);
return {
scenario: scenarioToSearch(state.scenarioId),
subnet: state.subnetId ?? undefined,
Expand All @@ -122,10 +106,6 @@ export const navigationStateToSharedSearch = (
view === simulateViewToSearch(baseline.simulateView) ? undefined : view,
overlay:
overlay === overlayToSearch(baseline.overlay) ? undefined : overlay,
present:
present === presentationToSearch(baseline.simulatePresentation)
? undefined
: present,
...selectionToSearch(state.selection),
};
};
Expand All @@ -147,10 +127,10 @@ export const navigationStateToPreviewSearch = (
* Applies a Preview navigation to a search, keeping the fields the Preview
* does not navigate.
*
* Writing the projection alone would drop `mode`, `view`, `overlay` and
* `present` on the first selection, and an embed can arrive carrying them:
* oEmbed copies the source page's `mode` into the iframe URL. A surface that
* does not understand a field must not destroy it.
* Writing the projection alone would drop `mode`, `view` and `overlay` on the
* first selection, and an embed can arrive carrying them: oEmbed copies the
* source page's `mode` into the iframe URL. A surface that does not understand
* a field must not destroy it.
*/
export const applyPreviewNavigationUpdate = (
search: SharedExampleSearch,
Expand All @@ -159,7 +139,6 @@ export const applyPreviewNavigationUpdate = (
mode: search.mode,
view: search.view,
overlay: search.overlay,
present: search.present,
...navigationStateToPreviewSearch(
update(previewSearchToNavigationState(search)),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ const mergeSharedSearch = (
mode: shared.mode,
simulateView: shared.simulateView,
overlay: shared.overlay,
simulatePresentation: shared.simulatePresentation,
};
};

Expand All @@ -63,9 +62,8 @@ export const withClearedSharedLocation = (

/**
* Navigation controller for pages whose URL carries the shared location: the
* scenario, the subnet, the focused item, the mode, the Simulate section, the
* open overlay and the presentation of an open optimization. The editor
* navigates one field more than that — the
* scenario, the subnet, the focused item, the mode, the Simulate section and
* the open overlay. The editor navigates one field more than that — the
* resource open inside Simulate — so the full location still lives in page
* state and only its shared projection reaches the URL.
*
Expand Down
Loading
Loading