Skip to content
Open
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/snapshot-sharing-doc-catalog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hashintel/petrinaut-core": patch
---

Let AI assistants find the guide to sharing net snapshots.
5 changes: 5 additions & 0 deletions .changeset/snapshot-sharing-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hashintel/petrinaut": patch
---

Document sharing complete net snapshots through links on the demo website.
19 changes: 19 additions & 0 deletions apps/petrinaut-website/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,25 @@ With Brunch configured, `/?brunchTracer=construction` opens a separately identif

This candidate proves only root arc/weight progression. Other root classes, deletion/recreation, arc connectivity changes, layout/title, components and subnets are unavailable, not silently approximated. The why result keeps origin separate from subsequent recorded changes and attempts; weight corrections resolve their own governing revision. Basis remains operation-level and semantic utility unassessed. Unrecorded intervening content prevents attribution; failed, stale, no-op and conflicting results are not causes. `test:construction-progression` uses actual Chrome with synthetic native SDK responses, not paid or genuine/provider-class admission.

## Snapshot links

**Share** in the top bar captures a document for a self-contained `/share#v1.br.<payload>`
link. The optional current view uses the same validated query parameters as example
and local-document routes. View navigation preserves the fragment, and browser
history can move between snapshots.

Snapshots use compact canonical JSON, Brotli quality 11, and unpadded base64url.
The codec loads in a dedicated worker when needed. Encoding and decoding cap the
document at 2 MiB and the fragment at 16,000 characters; decoding enforces its
output limit incrementally. Each worker is terminated on completion, cancellation,
or a 30-second timeout. Larger documents can be downloaded from the Share dialog.

A snapshot opens read-only without writing a saved document. **Make a local copy**
creates a new UUID and preserves the current view. Snapshot fragments are removed
from Sentry events, transactions, spans, and breadcrumbs before transmission.

See the [sharing guide](../../libs/@hashintel/petrinaut/docs/sharing.md) for the user flow.

## Example embeds and oEmbed

Canonical example pages live below `/examples`. The JSON oEmbed endpoint at
Expand Down
1 change: 1 addition & 0 deletions apps/petrinaut-website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@sentry/react": "10.64.0",
"@tanstack/react-router": "1.170.31",
"ai": "6.0.182",
"brotli-wasm": "3.0.1",
"immer": "10.1.3",
"react": "19.2.6",
"react-dom": "19.2.6",
Expand Down
142 changes: 24 additions & 118 deletions apps/petrinaut-website/src/examples/full-example-page.tsx
Original file line number Diff line number Diff line change
@@ -1,126 +1,32 @@
import { useEffect, useState } from "react";

import { Button } from "@hashintel/ds-components";
import { css } from "@hashintel/ds-helpers/css";
import { Petrinaut } from "@hashintel/petrinaut/ui";

import {
ReadonlyDocumentPage,
type ReadonlyDocumentPageProps,
} from "../main/app/readonly-document-page";
import { getOEmbedDiscoveryUrl } from "./oembed-discovery";
import { getReadonlyExampleHandle } from "./readonly-example-handle";
import { useSharedSearchNavigation } from "./use-shared-search-navigation";

import type { LoadedExample } from "./catalog";
import type { SharedExampleSearch } from "./example-search";

const pageStyle = css({
width: "[100vw]",
height: "[100vh]",
minWidth: "0",
minHeight: "0",
overflow: "hidden",
});

const titleStyle = css({
minWidth: "0",
overflow: "hidden",
color: "neutral.s90",
fontSize: "sm",
fontWeight: "medium",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
});

export type FullExamplePageProps = {
example: LoadedExample;
onFork: () => void;
/** Writes the shared search subset back to the page URL. */
onSearchChange: (
search: SharedExampleSearch,
history: "push" | "replace",
) => void;
search: SharedExampleSearch;
};
export type FullExamplePageProps = Omit<
ReadonlyDocumentPageProps,
"handle" | "title"
> & { example: LoadedExample };

export const FullExamplePage = ({
example,
onFork,
onSearchChange,
search,
}: FullExamplePageProps) => {
const [forkError, setForkError] = useState<string | null>(null);
const handle = getReadonlyExampleHandle(example);
const navigation = useSharedSearchNavigation(search, onSearchChange);

const forkLocalCopy = () => {
try {
onFork();
setForkError(null);
} catch {
setForkError(
"Your browser couldn't save a copy. Free up browser storage and try again.",
);
}
};

useEffect(() => {
const previousTitle = document.title;
document.title = `${example.catalog.title} · Petrinaut`;
return () => {
document.title = previousTitle;
};
}, [example.catalog.title]);

// The website is a client-rendered SPA, so the oEmbed discovery link cannot
// be baked into index.html; React 19 hoists this <link> into document.head.
// Consumers that execute the page's JavaScript can then discover the same
// production oEmbed endpoint used by server integrations.
const discoveryUrl = getOEmbedDiscoveryUrl(example.catalog.slug, search);

return (
<main className={pageStyle}>
<link
href={discoveryUrl}
rel="alternate"
title={`${example.catalog.title} oEmbed profile`}
type="application/json+oembed"
/>
{forkError && (
<div
role="alert"
className={css({
position: "absolute",
top: "16",
right: "4",
zIndex: "[50]",
background: "neutral.s00",
borderRadius: "md",
padding: "3",
boxShadow: "md",
maxWidth: "[360px]",
fontSize: "sm",
})}
>
{forkError}
</div>
)}
<Petrinaut
handle={handle}
hideNetManagementControls="all"
navigation={navigation}
presentationProfile="review"
readonly
readOnlyAction={{ label: "Make a local copy", onClick: forkLocalCopy }}
slots={{
topBarEnd: (
<Button size="xs" variant="subtle" onClick={forkLocalCopy}>
Make a local copy
</Button>
),
topBarStart: (
<span className={titleStyle}>{example.catalog.title}</span>
),
}}
title={example.catalog.title}
/>
</main>
);
};
...props
}: FullExamplePageProps) => (
<>
<link
href={getOEmbedDiscoveryUrl(example.catalog.slug, props.search)}
rel="alternate"
title={`${example.catalog.title} oEmbed profile`}
type="application/json+oembed"
/>
<ReadonlyDocumentPage
{...props}
handle={getReadonlyExampleHandle(example)}
title={example.catalog.title}
/>
</>
);
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
useSharedSearchNavigation,
withClearedSharedLocation,
} from "../../../examples/use-shared-search-navigation";
import { ShareSnapshotButton } from "../../../sharing/share-snapshot-button";
import { VOICE_REQUEST_ID_HEADER } from "../../../voice-diagnostics";
import { CommandPalette } from "../command-palette";
import { useSentryFeedbackAction } from "../sentry-feedback-button";
Expand Down Expand Up @@ -1140,6 +1141,18 @@ export const LocalStorageDemoApp = ({
readonly={false}
setTitle={setTitle}
title={currentDocument.title}
slots={{
topBarEnd: (
<ShareSnapshotButton
getSnapshot={() => ({
title: currentDocument.title,
definition:
activeHandle.handle.doc() ?? currentDocument.definition,
})}
search={search}
/>
),
}}
viewportActions={[sentryFeedbackAction]}
/>
</WalkthroughProvider>
Expand Down
125 changes: 125 additions & 0 deletions apps/petrinaut-website/src/main/app/readonly-document-page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { useEffect, useState } from "react";

import { Button } from "@hashintel/ds-components";
import { css } from "@hashintel/ds-helpers/css";
import { Petrinaut } from "@hashintel/petrinaut/ui";

import { useSharedSearchNavigation } from "../../examples/use-shared-search-navigation";
import { ShareSnapshotButton } from "../../sharing/share-snapshot-button";

import type { SharedExampleSearch } from "../../examples/example-search";
import type { PetrinautDocHandle } from "@hashintel/petrinaut-core";

export type ReadonlyDocumentPageProps = {
handle: PetrinautDocHandle;
title: string;
onFork: () => void;
onSearchChange: (
search: SharedExampleSearch,
history: "push" | "replace",
) => void;
search: SharedExampleSearch;
};

export const ReadonlyDocumentPage = ({
handle,
title,
onFork,
onSearchChange,
search,
}: ReadonlyDocumentPageProps) => {
const [forkError, setForkError] = useState<string | null>(null);
const navigation = useSharedSearchNavigation(search, onSearchChange);
const forkLocalCopy = () => {
try {
onFork();
setForkError(null);
} catch {
setForkError(
"Your browser couldn't save a copy. Free up browser storage and try again.",
);
}
};
useEffect(() => {
const previousTitle = document.title;
document.title = `${title} · Petrinaut`;
return () => {
document.title = previousTitle;
};
}, [title]);

return (
<main
className={css({
width: "[100vw]",
height: "[100vh]",
minWidth: "0",
minHeight: "0",
overflow: "hidden",
})}
>
{forkError && (
<div
role="alert"
className={css({
position: "absolute",
top: "16",
right: "4",
zIndex: "[50]",
background: "neutral.s00",
borderRadius: "md",
padding: "3",
boxShadow: "md",
maxWidth: "[360px]",
fontSize: "sm",
})}
>
{forkError}
</div>
)}
<Petrinaut
handle={handle}
hideNetManagementControls="all"
navigation={navigation}
presentationProfile="review"
readonly
readOnlyAction={{ label: "Make a local copy", onClick: forkLocalCopy }}
slots={{
topBarEnd: (
<div
className={css({
display: "flex",
alignItems: "center",
gap: "2",
})}
>
<ShareSnapshotButton
getSnapshot={() => ({ title, definition: handle.doc()! })}
search={search}
/>
<Button size="xs" variant="subtle" onClick={forkLocalCopy}>
Make a local copy
</Button>
</div>
),
topBarStart: (
<span
className={css({
minWidth: "0",
overflow: "hidden",
color: "neutral.s90",
fontSize: "sm",
fontWeight: "medium",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
})}
>
{title}
</span>
),
}}
title={title}
/>
</main>
);
};
Loading
Loading