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
18 changes: 18 additions & 0 deletions pocs/petrinaut-hazel/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended",
],
ignorePatterns: ["dist", ".eslintrc.cjs"],
parser: "@typescript-eslint/parser",
plugins: ["react-refresh"],
rules: {
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
},
};
5 changes: 5 additions & 0 deletions pocs/petrinaut-hazel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Petrinaut x Hazel

Experiment to create Petrinaut as a Hazel Livelit.

Following the instructions set out [here](https://github.com/hazelgrove/hazel/blob/exolivelits/HAZEL_EXOLIVELIT_GUIDE.md).
16 changes: 16 additions & 0 deletions pocs/petrinaut-hazel/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">

<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/hash.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Petrinaut Hazel Livelet</title>
</head>

<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>

</html>
41 changes: 41 additions & 0 deletions pocs/petrinaut-hazel/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "petrinaut-hazel",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives",
"preview": "vite preview"
},
"dependencies": {
"@emotion/react": "11.14.0",
"@emotion/styled": "11.14.1",
"@fortawesome/free-solid-svg-icons": "6.7.2",
"@hashintel/block-design-system": "0.0.5",
"@hashintel/design-system": "0.0.9-canary.2",
"@hashintel/petrinaut": "0.0.5",
"@mui/material": "5.18.0",
"@mui/system": "5.18.0",
"elkjs": "0.10.0",
"immer": "10.1.3",
"reactflow": "11.11.4",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-use": "17.6.0",
"uuid": "11.1.0"
},
"devDependencies": {
"@types/react": "18.3.12",
"@types/react-dom": "18.3.1",
"@vitejs/plugin-react": "4.3.3",
"eslint": "8.45.0",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-react-refresh": "0.4.3",
"typescript": "5.7.3",
"typescript-eslint": "7.3.1",
"vite": "5"
},
"packageManager": "yarn@1.22.22+sha256.c17d3797fb9a9115bf375e31bfd30058cac6bc9c3b8807a3d8cb2094794b51ca"
}
3 changes: 3 additions & 0 deletions pocs/petrinaut-hazel/public/hash.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions pocs/petrinaut-hazel/src/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
html,
body,
#root {
height: 100%;
}
24 changes: 24 additions & 0 deletions pocs/petrinaut-hazel/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React, { Suspense } from "react";
import ReactDOM from "react-dom/client";
import { CssBaseline, ThemeProvider } from "@mui/material";
import { CacheProvider } from "@emotion/react";
import { createEmotionCache, theme } from "@hashintel/design-system/theme";

import "./index.css";
import { App } from "./main/app";

const emotionCache = createEmotionCache();

// biome-ignore lint/style/noNonNullAssertion: we know it exists
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<Suspense fallback={<div>Suspense fallback...</div>}>
<CacheProvider value={emotionCache}>
<ThemeProvider theme={theme}>
<CssBaseline />
<App />
</ThemeProvider>
</CacheProvider>
</Suspense>
</React.StrictMode>,
);
119 changes: 119 additions & 0 deletions pocs/petrinaut-hazel/src/main/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { useState } from "react";
import {
Petrinaut,
defaultTokenTypes,
type PetriNetDefinitionObject,
} from "@hashintel/petrinaut";
import { useHazelIntegration } from "./app/use-hazel-integration";
import { produce } from "immer";

const createDefaultNetDefinition = (): PetriNetDefinitionObject => {
return {
nodes: [],
arcs: [],
tokenTypes: structuredClone(defaultTokenTypes),
};
};

/**
* An incomplete type guard to check if a value is a valid Petri net definition.
* Does not check the content of arrays.
*/
const isValidNetDefinition = (
definition: unknown,
): definition is PetriNetDefinitionObject => {
if (typeof definition !== "object" || definition === null) {
return false;
}

if (!("nodes" in definition) || !Array.isArray(definition.nodes)) {
return false;
}

if (!("arcs" in definition) || !Array.isArray(definition.arcs)) {
return false;
}

if (!("tokenTypes" in definition) || !Array.isArray(definition.tokenTypes)) {
return false;
}

return true;
};

/**
* Wraps Petrinaut with the event handlers necessary for a Hazel Livelit.
*/
export const App = () => {
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get("id") || "local-demo";

const [netDefinition, setNetDefinition] =
useState<PetriNetDefinitionObject | null>(null);

const { setSyntax } = useHazelIntegration({
id,
codec: "json",
onInit: (value) => {
console.log("Received value", value);

try {
const parsedValue = JSON.parse(value);

if (isValidNetDefinition(parsedValue)) {
setNetDefinition(parsedValue);
} else {
console.error("Invalid net definition", parsedValue);
const defaultNetDefinition = createDefaultNetDefinition();
setNetDefinition(defaultNetDefinition);
setSyntax(JSON.stringify(defaultNetDefinition));
}
} catch (error) {
console.error("Error parsing net definition as JSON", error);
}
},
});

if (!netDefinition) {
return null;
}

return (
<Petrinaut
key={id}
hideNetManagementControls
petriNetId={id}
petriNetDefinition={netDefinition}
existingNets={[]}
mutatePetriNetDefinition={(definitionMutationFn) => {
setNetDefinition((existingDefinition) => {
const newDefinition = produce(
existingDefinition,
definitionMutationFn,
);

setSyntax(JSON.stringify(newDefinition));

return newDefinition;
});
}}
parentNet={null}
createNewNet={() => {
throw new Error(
"Petrinaut should not be attemping to create new nets when wrapped by Patchwork",
);
}}
loadPetriNet={() => {
throw new Error(
"Petrinaut should not be attemping to load other nets when wrapped by Patchwork",
);
}}
setTitle={() => {
throw new Error(
"Petrinaut should not be attemping to set the net title when wrapped by Patchwork",
);
}}
title={""}
/>
);
};
97 changes: 97 additions & 0 deletions pocs/petrinaut-hazel/src/main/app/use-hazel-integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { useCallback, useEffect, useState } from "react";

export type MessageToHazel =
| { type: "ready"; id: string }
| { type: "setSyntax"; id: string; codec: string; value: string }
| { type: "resize"; id: string; width: number; height: number };

export type MessageFromHazel =
| { type: "init"; id: string; value: string }
| {
type: "constraints";
id: string;
maxWidth: number;
maxHeight: number;
minWidth?: number;
minHeight?: number;
};

export function isFromHazelMessage(data: unknown): data is MessageFromHazel {
return (
data !== null &&
typeof data === "object" &&
"type" in data &&
"id" in data &&
["init", "constraints"].includes(
(data as Record<string, unknown>).type as string,
) &&
typeof (data as Record<string, unknown>).id === "string"
);
}

type HazelIntegrationConfig = {
id: string;
codec: string;
onInit: (value: string) => void;
};

const sendToHazel = (message: MessageToHazel, targetOrigin: string) => {
if (window.parent && window.parent !== window) {
console.log("Sending message to Hazel", message);
window.parent.postMessage(message, targetOrigin);
}
};

/**
* Core Hazel integration for SolidJS - handles protocol, messaging, and setup
*/
export const useHazelIntegration = (config: HazelIntegrationConfig) => {
const { id, codec, onInit } = config;
const [hasInit, setHasInit] = useState(false);

const targetOrigin =
new URLSearchParams(window.location.search).get("parentOrigin") || "*";

const setSyntax = useCallback(
(value: string) => {
sendToHazel({ type: "setSyntax", id, codec, value }, targetOrigin);
},
[id, codec, targetOrigin],
);

useEffect(() => {
const handleMessage = (event: MessageEvent) => {
const data = event.data;

if (!isFromHazelMessage(data) || data.id !== id) {
return;
}

console.log("Received message from Hazel", data);

switch (data.type) {
case "init":
if (onInit) {
onInit(data.value);
}
break;
}
};

window.addEventListener("message", handleMessage);

// Send ready message when component mounts
if (!hasInit) {
sendToHazel({ type: "ready", id }, targetOrigin);
setHasInit(true);
}

return () => {
window.removeEventListener("message", handleMessage);
};
});

return {
setSyntax,
};
};
1 change: 1 addition & 0 deletions pocs/petrinaut-hazel/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
Loading