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
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export type DraggingStateByNodeId = Record<
{ dragging: boolean; position: { x: number; y: number } }
>;

export type EditorGlobalMode = "edit" | "simulate" | "actual";
export type EditorGlobalMode = "edit" | "simulate" | "actual" | "notebook";
type EditorEditionMode =
| "cursor"
| "add-place"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { use } from "react";

import { EditorContext } from "./editor-context";
import { UserSettingsContext } from "./user-settings-context";

import type { EditorGlobalMode } from "./editor-context";

/**
* The global mode the editor actually renders. The stored mode can say
* "notebook" while the experimental notebook flag is off (e.g. the flag was
* disabled while the view was active); every consumer must agree that this
* falls back to "edit" β€” deriving it in one consumer only would render the
* edit canvas while mutations are still refused with a notebook explanation.
*/
export const useEffectiveGlobalMode = (): EditorGlobalMode => {
const { globalMode } = use(EditorContext);
const { enableNotebookView } = use(UserSettingsContext);

return globalMode === "notebook" && !enableNotebookView ? "edit" : globalMode;
};
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { use } from "react";

import { SimulationContext } from "../simulation/context";
import { EditorContext } from "./editor-context";
import { SDCPNContext } from "./sdcpn-context";
import { useEffectiveGlobalMode } from "./use-effective-global-mode";

/**
* Why the editor currently disallows mutations, or `null` when mutations
Expand All @@ -26,7 +26,10 @@ export type ReadOnlyReason =
*/
export const useReadOnlyReason = (): ReadOnlyReason | null => {
const { readonly } = use(SDCPNContext);
const { globalMode } = use(EditorContext);
// The effective mode, not the stored one β€” the stored mode can say
// "notebook" while the flag is off, in which case the edit canvas renders
// and mutations must be allowed.
const globalMode = useEffectiveGlobalMode();
const { state: simulationState } = use(SimulationContext);

if (readonly) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { use, useEffect, useEffectEvent } from "react";

import { UndoRedoContext } from "./undo-redo-context";

/**
* Binds Cmd/Ctrl+Z (undo) and Cmd/Ctrl+Shift+Z (redo) for views that don't
* mount the canvas BottomBar, which owns the full editor shortcut set.
* Inputs, textareas and code editors are left alone so their native undo
* stacks keep working.
*/
export function useUndoRedoShortcuts() {
const undoRedo = use(UndoRedoContext);

const handleKeyDown = useEffectEvent((event: KeyboardEvent) => {
if (
!undoRedo ||
!(event.metaKey || event.ctrlKey) ||
event.key.toLowerCase() !== "z"
) {
return;
}
const target = event.target as HTMLElement;
const isInputFocused =
target.tagName === "INPUT" ||
target.tagName === "TEXTAREA" ||
target.isContentEditable ||
target.closest(".monaco-editor") !== null;
if (isInputFocused) {
return;
}
event.preventDefault();
if (event.shiftKey) {
undoRedo.redo();
} else {
undoRedo.undo();
}
});

useEffect(() => {
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, []);
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export type UserSettings = {
partialSelection: boolean;
useEntitiesTreeView: boolean;
enableNetComponents: boolean;
enableNotebookView: boolean;
/**
* Persisted preference controlling whether the product walkthrough opens
* automatically the next time the app initializes. The live open state is
Expand Down Expand Up @@ -72,6 +73,7 @@ export type UserSettingsActions = {
setPartialSelection: (value: boolean) => void;
setUseEntitiesTreeView: (value: boolean) => void;
setEnableNetComponents: (value: boolean) => void;
setEnableNotebookView: (value: boolean) => void;
setShowWalkthroughOnInit: (value: boolean) => void;
updateSubViewSection: (
containerName: string,
Expand Down Expand Up @@ -100,6 +102,7 @@ export const defaultUserSettings: UserSettings = {
partialSelection: true,
useEntitiesTreeView: false,
enableNetComponents: false,
enableNotebookView: false,
showWalkthroughOnInit: true,
subViewPanels: {},
};
Expand All @@ -123,6 +126,7 @@ const DEFAULT_CONTEXT_VALUE: UserSettingsContextValue = {
setPartialSelection: () => {},
setUseEntitiesTreeView: () => {},
setEnableNetComponents: () => {},
setEnableNotebookView: () => {},
setShowWalkthroughOnInit: () => {},
updateSubViewSection: () => {},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ export const UserSettingsProvider: React.FC<React.PropsWithChildren> = ({
setState((prev) => ({ ...prev, useEntitiesTreeView: value })),
setEnableNetComponents: (value: boolean) =>
setState((prev) => ({ ...prev, enableNetComponents: value })),
setEnableNotebookView: (value: boolean) =>
setState((prev) => ({ ...prev, enableNotebookView: value })),
setShowWalkthroughOnInit: (value: boolean) =>
setState((prev) => ({ ...prev, showWalkthroughOnInit: value })),
updateSubViewSection: (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,30 @@ import type { SegmentedControlItem } from "@hashintel/ds-components";

export interface ModeSelectorProps {
actualModeAvailable: boolean;
notebookViewAvailable: boolean;
mode: EditorGlobalMode;
onChange: (mode: EditorGlobalMode) => void;
}

const getOptions = (
actualModeAvailable: boolean,
notebookViewAvailable: boolean,
): SegmentedControlItem<EditorGlobalMode>[] => [
{
label: "Edit",
value: "edit",
iconName: "shapes",
},
...(notebookViewAvailable
? [
{
label: "Notebook",
value: "notebook",
iconName: "fileLines",
tooltip: "Read the net as a list of cells.",
} satisfies SegmentedControlItem<EditorGlobalMode>,
]
: []),
{
label: "Simulate",
value: "simulate",
Expand All @@ -35,14 +47,15 @@ const getOptions = (

export const ModeSelector: React.FC<ModeSelectorProps> = ({
actualModeAvailable,
notebookViewAvailable,
mode,
onChange,
}) => {
return (
<SegmentedControl
size="sm"
value={mode}
items={getOptions(actualModeAvailable)}
items={getOptions(actualModeAvailable, notebookViewAvailable)}
onChange={onChange}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const titleStyles = css({

interface TopBarProps {
actualModeAvailable: boolean;
notebookViewAvailable: boolean;
menuItems: MenuItem[];
title: string;
onTitleChange: (value: string) => void;
Expand All @@ -71,6 +72,7 @@ interface TopBarProps {

export const TopBar: React.FC<TopBarProps> = ({
actualModeAvailable,
notebookViewAvailable,
menuItems,
title,
onTitleChange,
Expand Down Expand Up @@ -132,6 +134,7 @@ export const TopBar: React.FC<TopBarProps> = ({
{/* Center section - mode switcher */}
<ModeSelector
actualModeAvailable={actualModeAvailable}
notebookViewAvailable={notebookViewAvailable}
mode={mode}
onChange={onModeChange}
/>
Expand Down
30 changes: 23 additions & 7 deletions libs/@hashintel/petrinaut/src/ui/views/Editor/editor-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { ActualModeContext } from "../../../react/actual-mode-context";
import { ExperimentsContext } from "../../../react/experiments/context";
import { EditorContext } from "../../../react/state/editor-context";
import { SDCPNContext } from "../../../react/state/sdcpn-context";
import { useEffectiveGlobalMode } from "../../../react/state/use-effective-global-mode";
import { useSelectionCleanup } from "../../../react/state/use-selection-cleanup";
import { UserSettingsContext } from "../../../react/state/user-settings-context";
import { Box } from "../../components/box";
Expand All @@ -39,6 +40,7 @@ import { WalkthroughDialog } from "../../components/walkthrough/walkthrough-dial
import { exportSDCPN } from "../../file-io/export-sdcpn";
import { exportTikZ } from "../../file-io/export-tikz";
import { importSDCPN } from "../../file-io/import-sdcpn";
import { NotebookView } from "../Notebook/notebook-view";
import { SDCPNView } from "../SDCPN/sdcpn-view";
import { AiCtaModal } from "./components/ai-cta-modal";
import { BottomBar } from "./components/BottomBar/bottom-bar";
Expand Down Expand Up @@ -80,8 +82,13 @@ const formatRelativeTime = (isoTimestamp: string): string => {
}).format(new Date(isoTimestamp));
};

// The remaining space under the TopBar, never 100% of the root: a full-height
// row overflows the root by the TopBar's height, and although the root hides
// overflow, scrollIntoView can still scroll it programmatically β€” pushing the
// TopBar out of view.
const rowContainerStyle = css({
height: "full",
flex: "[1]",
minHeight: "[0]",
userSelect: "none",
});

Expand Down Expand Up @@ -131,7 +138,6 @@ export const EditorView = ({

// Get editor context
const {
globalMode: mode,
isAiAssistantOpen,
setGlobalMode,
editionMode,
Expand All @@ -152,10 +158,17 @@ export const EditorView = ({
>(null);
const [isAiCtaDismissed, setIsAiCtaDismissed] = useState(false);

const { showWalkthroughOnInit, setShowWalkthroughOnInit } =
use(UserSettingsContext);
const {
enableNotebookView,
showWalkthroughOnInit,
setShowWalkthroughOnInit,
} = use(UserSettingsContext);
const walkthrough = use(WalkthroughContext);

// Shared with useReadOnlyReason so the rendered view and the mutation
// rules never disagree.
const effectiveMode = useEffectiveGlobalMode();

// Live open state for the walkthrough. Seeded once from the persisted
// "show on init" preference, so toggling that preference only takes effect
// on the next init rather than reopening the walkthrough mid-session.
Expand Down Expand Up @@ -433,11 +446,12 @@ export const EditorView = ({
{/* Top Bar - always visible */}
<TopBar
actualModeAvailable={actualMode.available}
notebookViewAvailable={enableNotebookView}
menuItems={menuItems}
title={title}
onTitleChange={setTitle}
hideNetManagementControls={hideNetManagementControls}
mode={mode}
mode={effectiveMode}
onModeChange={setGlobalMode}
onRunningExperimentClick={(experiment) =>
handleRunningExperimentClick(experiment.id)
Expand All @@ -446,8 +460,10 @@ export const EditorView = ({
/>

<Stack direction="row" className={rowContainerStyle}>
{mode === "simulate" ? (
{effectiveMode === "simulate" ? (
<SimulateView />
) : effectiveMode === "notebook" ? (
<NotebookView key={petriNetId ?? "no-net"} />
Comment thread
kube marked this conversation as resolved.
) : (
<Box className={canvasContainerStyle}>
{/* Left Sidebar - Tools and content panels */}
Expand All @@ -474,7 +490,7 @@ export const EditorView = ({
<BottomPanel />

<BottomBar
mode={mode}
mode={effectiveMode}
editionMode={editionMode}
onEditionModeChange={setEditionMode}
cursorMode={cursorMode}
Expand Down
23 changes: 23 additions & 0 deletions libs/@hashintel/petrinaut/src/ui/views/Notebook/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
layer: ui.views.notebook
role: Notebook view β€” the net as expandable cells with editable code, dependency analysis, and a whole-net graph explorer
---

The notebook renders the net as a flat list of cells, one per entity, so a
model reads like a program: declarations and the flow that uses them. It replaces the canvas and its
panels wholesale, which is what lets its Monaco editors reuse the LSP
document URIs β€” a model is never mounted twice.

Everything an expanded cell shows edits in place β€” names, fields, arc
weights, type assignments, and code β€” through the same guarded mutations as
the properties panel; only adding and removing nodes, arcs, and fields
stays in Edit mode.

The folder splits into a pure core and thin views. `notebook-model`,
`notebook-order`, `net-cycles`, `net-siphons` and `net-graph-layout` are
plain functions over the net definition, unit-tested without the DOM; the
`.tsx` files render their output and own only view state (selection comes
from the editor, expansion and search live here). The graph explorer draws
the whole net from the arc structure alone, ignoring canvas positions, so
the diagram answers "what feeds what" rather than "where did the author
drag things".
52 changes: 52 additions & 0 deletions libs/@hashintel/petrinaut/src/ui/views/Notebook/cell-kinds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Shared per-kind presentation for the notebook view, so cells, the graph
* explorer and the kind filter all give a kind the same icon and label.
*/

import {
DifferentialEquationIcon,
ParameterIcon,
PlaceFilledIcon,
TokenTypeIcon,
TransitionFilledIcon,
} from "../../constants/entity-icons";

import type { NotebookCellKind } from "./notebook-model";
import type { ComponentType } from "react";

export const CELL_KIND_ICONS: Record<
NotebookCellKind,
ComponentType<{ size: number }>
> = {
place: PlaceFilledIcon,
transition: TransitionFilledIcon,
type: TokenTypeIcon,
differentialEquation: DifferentialEquationIcon,
parameter: ParameterIcon,
};

/** Keyword shown before a cell's name, as a declaration would read. */
export const CELL_KIND_LABELS: Record<NotebookCellKind, string> = {
place: "Place",
transition: "Transition",
type: "Type",
differentialEquation: "Equation",
parameter: "Parameter",
};

/** Kinds in the order the filter row lists them. */
export const CELL_KINDS: NotebookCellKind[] = [
"place",
"transition",
"type",
"differentialEquation",
"parameter",
];

export const CELL_KIND_PLURAL_LABELS: Record<NotebookCellKind, string> = {
place: "Places",
transition: "Transitions",
type: "Types",
differentialEquation: "Equations",
parameter: "Parameters",
};
Loading
Loading