Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/dialog-content-position.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hashintel/ds-components": patch
---

Support content refs and inline styles on Dialog for movable, resizable layouts.
5 changes: 5 additions & 0 deletions .changeset/petrinaut-user-settings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hashintel/petrinaut": patch
---

Provide movable, resizable User Settings with keyboard-accessible sections, inline experimental badges, and scroll-edge blur.
6 changes: 6 additions & 0 deletions .changeset/quiet-boats-dock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@hashintel/petrinaut": patch
---

Dock the AI assistant on the right, detach it into a movable floating panel, and toggle it with Cmd/Ctrl+Shift+K across editor views while preserving the conversation and keeping the toolbar centered.
Space out the header tabs, animate its icons, align the send button inside the composer, resize the floating panel from any edge or corner with fading border highlights, and hide Clear AI while the conversation is empty.
24 changes: 24 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,30 @@ import {
} from "./example-search";

describe("example search contract", () => {
it("validates settings sections only for the user settings dialog", () => {
expect(
validateSharedExampleSearch({
overlay: "user-settings",
settings: "viewport",
}),
).toMatchObject({ overlay: "user-settings", settings: "viewport" });
expect(
validateSharedExampleSearch({
overlay: "user-settings",
settings: "unknown",
}).settings,
).toBeUndefined();
expect(
validateSharedExampleSearch({
overlay: "create-experiment",
settings: "viewport",
}).settings,
).toBeUndefined();
expect(
validateSharedExampleSearch({ overlay: "viewport-settings" }).overlay,
).toBe("viewport-settings");
});

it("strips unsupported query values", () => {
expect(
validateSharedExampleSearch({
Expand Down
18 changes: 18 additions & 0 deletions apps/petrinaut-website/src/examples/example-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,20 @@ export const sharedSimulateViews = [
] as const;

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

export const sharedSettingsSections = [
"general",
"viewport",
"simulation",
"labs",
] as const;

export type SharedMode = (typeof sharedModes)[number];
export type SharedSimulateView = (typeof sharedSimulateViews)[number];
export type SharedOverlay = (typeof sharedOverlays)[number];
Expand All @@ -58,6 +66,7 @@ export type SharedExampleSearch = {
mode?: SharedMode;
view?: SharedSimulateView;
overlay?: SharedOverlay;
settings?: (typeof sharedSettingsSections)[number];
};

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

// `.catch(undefined)` is the contract's whole validation story: anything a URL
Expand Down Expand Up @@ -117,6 +127,14 @@ export const validateSharedExampleSearch = (
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)),
});

Expand Down
25 changes: 25 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,31 @@ import {
} from "./navigation-search";

describe("navigation state projection", () => {
it.each(["general", "viewport", "simulation", "labs"] as const)(
"round-trips the %s settings section in Simulate",
(settings) => {
const search = {
mode: "simulate",
view: "metrics",
overlay: "user-settings",
settings,
} as const;
const state = sharedSearchToNavigationState(search);
expect(state.overlay).toEqual({
type: "user-settings",
section: settings,
});
expect(navigationStateToSharedSearch(state)).toMatchObject(search);
expect(
applyPreviewNavigationUpdate(search, (current) => current),
).toMatchObject(search);
expect(
sharedSearchToNavigationState({ mode: "simulate", view: "metrics" })
.overlay,
).toBeNull();
},
);

it("round-trips scenario, subnet, and selection", () => {
const state = sharedSearchToNavigationState({
scenario: "none",
Expand Down
11 changes: 9 additions & 2 deletions apps/petrinaut-website/src/examples/navigation-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ const overlayToSearch = (

const overlayFromSearch = (
overlay: SharedOverlay,
): PetrinautNavigationOverlay => ({ type: overlay });
section: SharedExampleSearch["settings"],
): PetrinautNavigationOverlay =>
overlay === "user-settings" ? { type: overlay, section } : { type: overlay };

export const sharedSearchToNavigationState = (
search: SharedExampleSearch,
Expand All @@ -86,7 +88,7 @@ export const sharedSearchToNavigationState = (
overlay:
search.overlay === undefined
? baseline.overlay
: overlayFromSearch(search.overlay),
: overlayFromSearch(search.overlay, search.settings),
});

export const navigationStateToSharedSearch = (
Expand All @@ -106,6 +108,10 @@ export const navigationStateToSharedSearch = (
view === simulateViewToSearch(baseline.simulateView) ? undefined : view,
overlay:
overlay === overlayToSearch(baseline.overlay) ? undefined : overlay,
settings:
state.overlay?.type === "user-settings"
? state.overlay.section
: undefined,
...selectionToSearch(state.selection),
};
};
Expand Down Expand Up @@ -139,6 +145,7 @@ export const applyPreviewNavigationUpdate = (
mode: search.mode,
view: search.view,
overlay: search.overlay,
settings: search.settings,
...navigationStateToPreviewSearch(
update(previewSearchToNavigationState(search)),
),
Expand Down
15 changes: 14 additions & 1 deletion apps/petrinaut-website/src/main/app/command-palette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,12 @@ export const CommandPalette = () => {

useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
if (
!event.shiftKey &&
!event.altKey &&
(event.metaKey || event.ctrlKey) &&
event.key.toLowerCase() === "k"
) {
event.preventDefault();
setOpen((open) => !open);
setQuery("");
Expand Down Expand Up @@ -158,6 +163,14 @@ export const CommandPalette = () => {
aria-label="Command palette"
style={paletteStyle}
onPointerDown={(event) => event.stopPropagation()}
onBlur={(event) => {
if (
event.relatedTarget &&
!event.currentTarget.contains(event.relatedTarget)
) {
setOpen(false);
}
}}
>
<input
ref={(element) => element?.focus()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1241,6 +1241,22 @@ describe("local storage demo prepared fixture", () => {
brunchPreviewConfig.isBrunchConfigured = true;
});

test.each(["metaKey", "ctrlKey"])(
"reserves %s + Shift + K for the assistant and keeps plain K for the palette",
(modifier) => {
seedStoredNet();
render(<LocalStorageDemoApp onSearchChange={() => {}} search={{}} />);
fireEvent.keyDown(window, { key: "K", [modifier]: true, shiftKey: true });
expect(
screen.queryByRole("dialog", { name: "Command palette" }),
).toBeNull();
fireEvent.keyDown(window, { key: "k", [modifier]: true });
expect(
screen.getByRole("dialog", { name: "Command palette" }),
).not.toBeNull();
},
);

test("shows the fixture selector only while Brunch demo mode is on", () => {
seedStoredNet();
render(<LocalStorageDemoApp onSearchChange={() => {}} search={{}} />);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export type DialogSize = "xs" | "sm" | "md" | "lg" | "xl" | "fullScreen";
const backdropClassName = overlayPartsStyles({ component: "dialog" }).backdrop;

const DialogRoot = ({
ref,
style,
className,
size = "md",
variant = "partitionedFooter",
Expand All @@ -35,6 +37,8 @@ const DialogRoot = ({
onKeyDown,
...ariaAttributes
}: {
ref?: React.Ref<HTMLDivElement>;
style?: React.CSSProperties;
className?: string;
size?: DialogSize;
onKeyDown?: React.KeyboardEventHandler<Element>;
Expand Down Expand Up @@ -112,6 +116,8 @@ const DialogRoot = ({
<ArkDialog.Positioner className={classes.positioner}>
<ArkDialog.Content
{...ariaAttributes}
ref={ref}
style={style}
className={cx(classes.content, className)}
aria-busy={loading ?? undefined}
onKeyDown={onKeyDown}
Expand Down
2 changes: 1 addition & 1 deletion libs/@hashintel/petrinaut/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,6 @@ Petrinaut has three global modes in the top bar, though **Actual** is only enabl
- [Actual Mode](actual-mode.md) -- View a host-provided live Petri net execution, currently via Brunch.
- [Embedded Preview](preview.md) -- Explore a compact, read-only Petri net embedded in a host application.
- [AI Assistant](ai-assistant.md) -- Build, review, and revise nets with text or inline Voice mode.
- [Visual Settings](visual-settings.md) -- Configure the editor appearance and behavior.
- [User Settings](visual-settings.md) -- Open preferences from any workspace tab and configure General, Viewport, Simulation, and Labs.
- [Compilation Output](compilation-output.md) -- Inspect how your net's code compiled, and what stops it running on the GPU.
- [Examples](examples.md) -- Walkthrough of the built-in example nets.
29 changes: 20 additions & 9 deletions libs/@hashintel/petrinaut/docs/ai-assistant.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,33 @@ Petrinaut has an in-app AI assistant that can build a net from a natural-languag

## Opening the panel

There are two entry points:
Open the assistant in any of these ways:

1. **AI button** in the bottom toolbar (Edit mode only). Click it to open the panel; click again to close. The tooltip is "Show AI assistant" / "Hide AI assistant".
2. **File → New → Build with Brunch**. When the host enables its experimental Brunch demo mode, shows net-management controls, and provides an assistant, this creates a fresh empty net, opens the assistant, and offers two chips before the first message: **Interview first** (elicit before inventing missing detail or drawing the net) or **Quick preview** (fill reasonable gaps, mark them as provisional, and wait for assent). **Start blank** creates the same empty net without opening the assistant or showing the first-run prompt. Without that host flag, **New** retains its ordinary direct blank-net behavior.
3. **First-run prompt**. When you load Petrinaut against an empty net, a centred prompt appears. Type a description and its trailing action becomes **Send**; select it to open the panel with your message already in flight. When the host provides Voice mode, the empty prompt instead shows a waveform action titled **Start voice mode**. It opens the same assistant without creating an empty text message. Dismiss the prompt with the **X**, by clicking outside it, or by pressing **Escape**; it is hidden for the rest of the session once dismissed.
4. **Command palette**. Choose **Toggle AI assistant**, or press **Cmd/Ctrl+Shift+K** directly. This opens the assistant and focuses the message field, or closes it when already visible, including compact Voice mode. Reopening preserves the conversation and docked or floating layout. The command keeps you in your current view. This command is available when the host provides an AI assistant.

The assistant panel stays open when you switch to **Simulate** or **Notebook**
mode, so you can inspect an experiment while its chat result arrives. The panel
resizes by dragging its left edge. Text and voice share the **AI** transcript.
Some hosts add a second tab, such as **Workpiece**, for a saved document.
Select a tab to switch views, or use the left/right arrow keys while a tab is
focused. Switching does not end a response, clear your draft or interrupt Voice;
the composer and active controls remain available.
The assistant stays available across **Edit**, **Simulate**, **Actual**, and **Notebook** modes. Switching views preserves your conversation, draft, and active response. The panel resizes by dragging its left edge. Text and voice share the **AI** transcript. Some hosts add a second tab, such as **Workpiece**, for a saved document. Select a tab to switch views, or use the left/right arrow keys while a tab is focused. Switching does not end a response, clear your draft or interrupt Voice; the composer and active controls remain available.

### Docking and floating

The assistant opens in a sidebar at the far right of the editor. It sits flush against the viewport, beside the canvas and its properties panel. Its left divider and resize highlight span the full panel height. The sidebar slides in at its full width while the canvas makes room. Closing it returns that space to the canvas.

The bottom toolbar stays centered on the editor when the docked assistant opens, moving only as far as needed to avoid overlapping the panels.

Choose **Float AI assistant** in the header to detach it into a rounded panel over the canvas. The canvas expands smoothly to reclaim the sidebar's space, and the floating panel reserves no space at the right edge. Drag anywhere in the header outside the tabs and action buttons to move it, or focus **Move AI assistant** and use the arrow keys. Hold **Shift** with an arrow key to move farther. The floating panel stays within the editor when the window changes size.

Choose **Dock AI assistant** to return it to the right sidebar. Switching between these layouts keeps your draft, conversation, and active response. Resize the sidebar from its left edge. When floating, drag the left or right edge to change its width, the top or bottom edge to change its height, or any corner to change both together. The opposite edge or corner stays fixed, and the panel stays inside the editor. Resizing only one dimension preserves the saved size of the other, even when the editor is too small to display it in full. Closing and reopening the assistant keeps your layout choice and floating height for the editor session.

Hovering a floating side highlights its straight border, fading out before each corner. Hovering a corner highlights its rounded border and fades along the two adjoining edges. Drag the corner to resize both dimensions.

The header text is not selectable. Its icons animate on hover and click, respecting your reduced-motion preference.

## The conversation

Type in the message field and press **Enter** or choose the **Send message** button with the upward arrow at its right edge. **Shift+Enter** adds a line. The field grows with your message while the send button stays at the bottom right.

While a response is streaming you can:

- Watch the model's text and reasoning appear live. The **Reasoning** block is collapsible; while it is streaming, it auto-opens, shows a shimmer effect, and (once attached timing information arrives) an elapsed timer.
Expand Down Expand Up @@ -181,7 +192,7 @@ Voice ends when the panel closes. If Realtime Voice is interrupted, allow
microphone access or check the connection, then select **Reconnect voice
mode**. **Clear AI chat** is unavailable while a Voice session is active.

When no interview is active and the host permits clearing, **Clear AI chat** via the delete button in the top right of the panel wipes the local conversation, stops any in-flight stream, and tells the host app to forget the messages if it persists them. Hosts with canonical history may disable this control. The Brunch panel disables it because clearing only the browser view would not delete Flue history and the conversation would return on rehydration.
The delete button appears in the top right of the panel once the conversation contains messages. When no interview is active and the host permits clearing, **Clear AI chat** wipes the local conversation, stops any in-flight stream, and tells the host app to forget the messages if it persists them. Hosts with canonical history may disable this control. The Brunch panel disables it because clearing only the browser view would not delete Flue history and the conversation would return on rehydration.

## What the assistant can do

Expand Down
2 changes: 1 addition & 1 deletion libs/@hashintel/petrinaut/docs/compilation-output.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ It is a diagnostic view about the compiler, not about your model — for errors

## Turning it on

Under **Settings → Simulation**, switch on **Compilation output**. A **Compilation** tab appears in the bottom panel. It is off by default.
Under **User settings → Labs**, switch on **Compilation output**. A **Compilation** tab appears in the bottom panel. It is off by default.

## What it shows

Expand Down
4 changes: 3 additions & 1 deletion libs/@hashintel/petrinaut/docs/drawing-a-net.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ You can also focus the outgoing handle with **Tab**, press **Enter** or **Space*

## Component ports

Subnets can expose selected places as ports. If you don't see subnet or component controls, enable **Settings → Net Components** first.
Subnets can expose selected places as ports. If you don't see subnet or component controls, enable **User settings → Labs → Net Components** first.

Open the subnet, select a boundary place, and enable **Component port** in the place properties panel.

Expand Down Expand Up @@ -221,6 +221,8 @@ The recent history is displayed in the top-right corner. Click on a history entr
| Cmd+Z | Undo |
| Cmd+Shift+Z | Redo |
| Cmd+F | Search |
| Cmd+Shift+K | Toggle the AI assistant |
| Cmd+, | Open user settings |
| Delete / Backspace | Delete selection |

On Windows/Linux, use Ctrl instead of Cmd.
Expand Down
Loading
Loading