Skip to content
Closed
5 changes: 5 additions & 0 deletions .changeset/ad-hoc-value-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hashintel/petrinaut": patch
---

Improve scenario and experiment forms with keyboard navigation, stacked section headers, overlay scrollbars, default starting-place filtering, and source expressions over selected computed values. Keep validation errors visible while editing ad-hoc values.
5 changes: 5 additions & 0 deletions .changeset/experiment-importance-order.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hashintel/petrinaut": patch
---

Simplify experiment result headers and chart summaries, move configuration and compute information into Details, and show the most influential parameters first in Sensitivity analysis.
5 changes: 5 additions & 0 deletions .changeset/experiment-interval-feedback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hashintel/petrinaut": patch
---

Simplify experiment creation with automatic metric names, objectives selected on metric rows, compact constraint editors, and keyboard navigation throughout the form. Explain invalid sweep intervals beside the creation button.
5 changes: 5 additions & 0 deletions .changeset/routed-simulation-panels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hashintel/petrinaut": patch
---

Add routed panels with fullscreen controls for experiments and scenarios, keeping edits and chart choices while resizing beside the AI assistant.
5 changes: 5 additions & 0 deletions .changeset/simulation-panel-guidance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hashintel/petrinaut-core": patch
---

Add simulation panel guidance to the AI assistant's documentation catalog.
40 changes: 40 additions & 0 deletions apps/petrinaut-website/src/examples/example-search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,46 @@ import {
} from "./example-search";

describe("example search contract", () => {
it("validates complete resource links and fullscreen creation links", () => {
const resource = {
resourceType: "scenario",
resourceId: "scenario / one",
presentation: "fullscreen",
};
const validated = validateSharedExampleSearch(resource);
expect(validated).toMatchObject(resource);
expect(canonicalSearchString(validated)).toBe(
"presentation=fullscreen&resourceId=scenario+%2F+one&resourceType=scenario",
);
expect(
validateSharedExampleSearch({
overlay: "create-experiment",
presentation: "fullscreen",
}).presentation,
).toBe("fullscreen");
for (const invalid of [
{ resourceType: "unknown", resourceId: "one" },
{ resourceType: "scenario" },
{ resourceType: "experiment", resourceId: "" },
{ resourceId: "one" },
]) {
const search = validateSharedExampleSearch({
...invalid,
presentation: "fullscreen",
});
expect(search.resourceType).toBeUndefined();
expect(search.resourceId).toBeUndefined();
expect(search.presentation).toBeUndefined();
}
expect(
validateSharedExampleSearch({
resourceType: "metric",
resourceId: "one",
presentation: "fullscreen",
}).presentation,
).toBeUndefined();
});

it("validates settings sections only for the user settings dialog", () => {
expect(
validateSharedExampleSearch({
Expand Down
64 changes: 48 additions & 16 deletions apps/petrinaut-website/src/examples/example-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ export const sharedSettingsSections = [
"labs",
] as const;

export const sharedResourceTypes = [
"scenario",
"metric",
"experiment",
] as const;

export type SharedMode = (typeof sharedModes)[number];
export type SharedSimulateView = (typeof sharedSimulateViews)[number];
export type SharedOverlay = (typeof sharedOverlays)[number];
Expand All @@ -67,6 +73,9 @@ export type SharedExampleSearch = {
view?: SharedSimulateView;
overlay?: SharedOverlay;
settings?: (typeof sharedSettingsSections)[number];
resourceType?: (typeof sharedResourceTypes)[number];
resourceId?: string;
presentation?: "fullscreen";
};

/** The keys this contract owns. Anything else in a URL is foreign. */
Expand All @@ -79,6 +88,9 @@ const sharedSearchKeys = [
"view",
"overlay",
"settings",
"resourceType",
"resourceId",
"presentation",
] as const satisfies readonly (keyof SharedExampleSearch)[];

// `.catch(undefined)` is the contract's whole validation story: anything a URL
Expand Down Expand Up @@ -121,22 +133,42 @@ export const selectionToSearch = (
*/
export const validateSharedExampleSearch = (
input: Record<string, unknown>,
): SharedExampleSearch => ({
scenario: optionalNonEmptyString.parse(input.scenario),
subnet: optionalNonEmptyString.parse(input.subnet),
mode: optionalMode.parse(input.mode),
view: optionalSimulateView.parse(input.view),
overlay: optionalOverlay.parse(input.overlay),
settings:
input.overlay === "user-settings"
? z
.enum(sharedSettingsSections)
.optional()
.catch(undefined)
.parse(input.settings)
: undefined,
...selectionToSearch(selectionFromInput(input)),
});
): SharedExampleSearch => {
const resourceType = z
.enum(sharedResourceTypes)
.optional()
.catch(undefined)
.parse(input.resourceType);
const resourceId = optionalNonEmptyString.parse(input.resourceId);
const hasResource = resourceType !== undefined && resourceId !== undefined;
const canExpand =
(hasResource && resourceType !== "metric") ||
input.overlay === "create-scenario" ||
input.overlay === "create-experiment";

return {
scenario: optionalNonEmptyString.parse(input.scenario),
subnet: optionalNonEmptyString.parse(input.subnet),
mode: optionalMode.parse(input.mode),
view: optionalSimulateView.parse(input.view),
overlay: optionalOverlay.parse(input.overlay),
settings:
input.overlay === "user-settings"
? z
.enum(sharedSettingsSections)
.optional()
.catch(undefined)
.parse(input.settings)
: undefined,
...selectionToSearch(selectionFromInput(input)),
resourceType: hasResource ? resourceType : undefined,
resourceId: hasResource ? resourceId : undefined,
presentation:
canExpand && input.presentation === "fullscreen"
? "fullscreen"
: undefined,
};
};

/** Canonical query string for a validated search: sorted, contract keys only. */
export const canonicalSearchString = (search: SharedExampleSearch): string => {
Expand Down
28 changes: 28 additions & 0 deletions apps/petrinaut-website/src/examples/navigation-search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,34 @@ import {
} from "./navigation-search";

describe("navigation state projection", () => {
it.each(["scenario", "experiment"] as const)(
"opens a direct %s link and preserves its presentation through Preview",
(resourceType) => {
const search = {
resourceType,
resourceId: "record / one",
presentation: "fullscreen" as const,
};
const state = sharedSearchToNavigationState(search);
expect(state.mode).toBe("simulate");
expect(state.simulateView).toBe(
resourceType === "scenario" ? "scenarios" : "experiments",
);
expect(state.simulateResource).toEqual({
type: resourceType,
id: "record / one",
});
expect(state.simulatePresentation).toBe("fullscreen");
expect(navigationStateToSharedSearch(state)).toMatchObject(search);
expect(
applyPreviewNavigationUpdate(search, (current) => ({
...current,
subnetId: "subnet",
})),
).toMatchObject(search);
},
);

it.each(["general", "viewport", "simulation", "labs"] as const)(
"round-trips the %s settings section in Simulate",
(settings) => {
Expand Down
38 changes: 32 additions & 6 deletions apps/petrinaut-website/src/examples/navigation-search.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
/**
* 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 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.
* The URL carries the selected scenario, subnet, focused item, editor mode,
* Simulate section, open record, overlay and panel presentation.
*
* 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 Down Expand Up @@ -83,8 +81,22 @@ export const sharedSearchToNavigationState = (
scenarioId: scenarioFromSearch(search),
subnetId: search.subnet ?? null,
selection: selectionFromInput(search as Record<string, unknown>),
mode: search.mode ?? baseline.mode,
simulateView: search.view ?? baseline.simulateView,
mode:
search.mode ??
(search.resourceType && search.resourceId ? "simulate" : baseline.mode),
simulateView:
search.resourceType && search.resourceId
? search.resourceType === "scenario"
? "scenarios"
: search.resourceType === "experiment"
? "experiments"
: "metrics"
: (search.view ?? baseline.simulateView),
simulateResource:
search.resourceType && search.resourceId
? { type: search.resourceType, id: search.resourceId }
: baseline.simulateResource,
simulatePresentation: search.presentation ?? baseline.simulatePresentation,
overlay:
search.overlay === undefined
? baseline.overlay
Expand All @@ -98,7 +110,18 @@ export const navigationStateToSharedSearch = (
const mode = modeToSearch(state.mode);
const view = simulateViewToSearch(state.simulateView);
const overlay = overlayToSearch(state.overlay);
const canExpand =
state.simulateResource?.type === "scenario" ||
state.simulateResource?.type === "experiment" ||
overlay === "create-scenario" ||
overlay === "create-experiment";
return {
resourceType: state.simulateResource?.type,
resourceId: state.simulateResource?.id,
presentation:
canExpand && state.simulatePresentation === "fullscreen"
? "fullscreen"
: undefined,
scenario: scenarioToSearch(state.scenarioId),
subnet: state.subnetId ?? undefined,
// Omitted at the baseline, so an untouched page keeps a clean URL and the
Expand Down Expand Up @@ -146,6 +169,9 @@ export const applyPreviewNavigationUpdate = (
view: search.view,
overlay: search.overlay,
settings: search.settings,
resourceType: search.resourceType,
resourceId: search.resourceId,
presentation: search.presentation,
...navigationStateToPreviewSearch(
update(previewSearchToNavigationState(search)),
),
Expand Down
Loading
Loading