diff --git a/.gitignore b/.gitignore index c65ba81..c762171 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ coverage/ .DS_Store .vscode/ .idea/ -/temp/ \ No newline at end of file +/temp/ +.gstack/ diff --git a/README.md b/README.md index 423e865..ec93813 100644 --- a/README.md +++ b/README.md @@ -1,153 +1,275 @@ # @pipelex/mthds-ui -Shared graph rendering logic for MTHDS method visualization. Core graph logic (builders, layout, analysis, configuration) is pure TypeScript with no React dependency. An optional React layer provides a ready-to-use `GraphViewer` component powered by ReactFlow. +Interactive graph visualization for [MTHDS](https://mthds.sh) method pipelines. Renders execution graphs from [GraphSpec](https://docs.pipelex.com/latest/under-the-hood/execution-graph-tracing/#graphspec) data — the canonical format produced by [Pipelex](https://pipelex.com) when tracing pipeline execution. + +Core graph logic (builders, layout, analysis) is pure TypeScript with no React dependency. The React layer provides a drop-in `GraphViewer` component powered by [ReactFlow](https://reactflow.dev). ## Install ```bash -npm install github:pipelex/mthds-ui +npm install @pipelex/mthds-ui ``` ### Peer dependencies -| Dependency | Required | Used by | -| -------------------- | ------------ | ----------------------------- | -| `dagre` | **yes** | Graph layout engine | -| `@types/dagre` | no (TS only) | Type definitions for dagre | -| `react`, `react-dom` | no | React layer (`graph/react`) | -| `@xyflow/react` | no | React layer (`graph/react`) | -| `shiki` | no | Syntax highlighting (`shiki`) | +| Dependency | Required | Used by | +| -------------------- | ------------ | --------------------------- | +| `dagre` | **yes** | Graph layout engine | +| `@types/dagre` | no (TS only) | Type definitions for dagre | +| `react`, `react-dom` | no | React layer (`graph/react`) | +| `@xyflow/react` | no | React layer (`graph/react`) | -## Entry points +## Quick start (React) -| Import path | Content | -| ---------------------------------------------- | ---------------------------------------------------------------------- | -| `@pipelex/mthds-ui` (or `./graph`) | Pure-TS graph logic — types, builders, layout, controllers, config | -| `@pipelex/mthds-ui/graph/react` | React components — `GraphViewer`, `ControllerGroupNode`, label helpers | -| `@pipelex/mthds-ui/graph/react/graph-core.css` | Base ReactFlow node/edge styles | -| `@pipelex/mthds-ui/shiki` | MTHDS syntax highlighting with shiki | +```tsx +import { GraphViewer } from "@pipelex/mthds-ui/graph/react"; -## Quick start (pure TypeScript) +function MethodGraph({ graphspec }) { + return ; +} +``` -```ts -import { - buildGraph, - getLayoutedElements, - applyControllers, - DEFAULT_GRAPH_CONFIG, -} from "@pipelex/mthds-ui"; +That's it. `GraphViewer` handles layout, styling, CSS variables, and all ReactFlow internals. All props except `graphspec` are optional with sensible defaults. -// Build graph data from a GraphSpec -const { graphData, analysis } = buildGraph(graphspec, "bezier"); +### Next.js (App Router) -// Apply dagre layout -const { nodes, edges } = getLayoutedElements(graphData.nodes, graphData.edges, "TB"); +ReactFlow accesses browser globals at module-evaluation time, so it cannot be server-side rendered. Use `next/dynamic` with `ssr: false`: -// Optionally wrap nodes in controller groups -const final = applyControllers(nodes, edges, graphspec, analysis, true); +```tsx +"use client"; + +import dynamic from "next/dynamic"; +import type { GraphViewerProps } from "@pipelex/mthds-ui/graph/react"; +import React from "react"; + +const GraphViewer = dynamic( + () => + import("@pipelex/mthds-ui/graph/react").then((mod) => ({ + default: mod.GraphViewer, + })), + { ssr: false }, +) as React.ComponentType; + +export function MyGraph({ graphspec }) { + return ; +} ``` -## Quick start (React) +### GraphViewer props -```tsx -import { ReactFlowProvider } from "@xyflow/react"; -import { DEFAULT_GRAPH_CONFIG } from "@pipelex/mthds-ui"; -import { GraphViewer } from "@pipelex/mthds-ui/graph/react"; -import "@xyflow/react/dist/style.css"; -import "@pipelex/mthds-ui/graph/react/graph-core.css"; +| Prop | Type | Default | Description | +| ------------------- | ----------------------------------- | ---------------------------- | ------------------------------------------ | +| `graphspec` | `GraphSpec \| null` | — | Graph data (nodes + edges) | +| `config` | `GraphConfig` | `DEFAULT_GRAPH_CONFIG` | Layout and visual configuration | +| `direction` | `GraphDirection` | `"TB"` | Layout direction: `TB`, `LR`, `RL`, `BT` | +| `showControllers` | `boolean` | `false` | Show controller group outlines | +| `onNavigateToPipe` | `(pipeCode: string) => void` | — | Callback when a pipe node is clicked | +| `onReactFlowInit` | `(instance: AppRFInstance) => void` | — | Access the underlying ReactFlow instance | -function MethodGraph({ graphspec }) { - return ( - - console.log("navigate:", pipeCode)} - /> - - ); -} +### Container sizing + +The graph container uses `position: absolute; inset: 0` to fill its parent. Make sure the parent element has `position: relative` and a defined height (e.g. `h-full`, `flex-1`, or an explicit height). + +## GraphSpec + +GraphSpec is the data format that describes a pipeline execution graph. It's generated by Pipelex when running or validating MTHDS method bundles. + +### Generating a GraphSpec + +```bash +# From a .mthds bundle file +pipelex-agent validate bundle my-method.mthds --view + +# The output JSON contains a graphspec field ``` -### GraphViewer props +Or programmatically via the Pipelex Python SDK: + +```python +from pipelex import Pipelex + +px = Pipelex() +result = px.validate_bundle("my-method.mthds", view=True) +graphspec = result.graphspec # dict ready for JSON serialization +``` + +### Structure + +```typescript +interface GraphSpec { + nodes: GraphSpecNode[]; + edges: GraphSpecEdge[]; +} + +interface GraphSpecNode { + id: string; + pipe_code?: string; // e.g. "analyze_match" + pipe_type?: string; // e.g. "PipeLLM", "PipeSequence", "PipeExtract" + status?: string; // "SUCCEEDED", "FAILED", "RUNNING", etc. + io?: { + inputs?: IOItem[]; + outputs?: IOItem[]; + }; +} -| Prop | Type | Description | -| ------------------- | ----------------------------------- | ------------------------------------------- | -| `graphspec` | `GraphSpec \| null` | Graph spec (nodes, edges, IO, containment) | -| `config` | `GraphConfig` | Layout and visual configuration | -| `direction` | `GraphDirection` | `"TB"`, `"LR"`, `"RL"`, or `"BT"` | -| `showControllers` | `boolean` | Show controller group outlines | -| `onNavigateToPipe?` | `(pipeCode: string) => void` | Callback when a pipe node is clicked | -| `onReactFlowInit?` | `(instance: AppRFInstance) => void` | Access the ReactFlow instance | - -## CSS theming - -`graph-core.css` reads these CSS custom properties. Override them in your app to theme the graph: - -| Property | Purpose | -| ------------------------- | -------------------------------- | -| `--color-bg` | Graph background | -| `--color-bg-dots` | Background dot pattern | -| `--color-text-muted` | Edge label text | -| `--color-accent` | Selected node highlight | -| `--color-controller-text` | Controller group label/type text | -| `--font-sans` | Node font family | -| `--font-mono` | Controller label font family | -| `--shadow-lg` | Selected node box shadow | - -## GraphConfig - -The `GraphConfig` object controls layout and visual behavior. Use `DEFAULT_GRAPH_CONFIG` as a starting point. - -```ts -interface GraphConfig { - direction?: "TB" | "LR" | "RL" | "BT"; // Dagre layout direction - showControllers?: boolean; // Show controller group boxes - nodesep?: number; // Horizontal spacing (default: 50) - ranksep?: number; // Vertical spacing (default: 30) - edgeType?: "bezier" | "step" | "straight" | "smoothstep"; - initialZoom?: number | null; // Override fit-view zoom (null = auto) - panToTop?: boolean; // Pan viewport to top after layout - paletteColors?: Record; // Color overrides for node/edge styles +interface GraphSpecEdge { + id?: string; + source: string; // Source node ID + target: string; // Target node ID + kind: EdgeKind; // "data", "contains", "batch_item", etc. + label?: string; } ``` -### Palette colors +### Node types + +Nodes represent pipes (operations) and stuffs (data artifacts) in the pipeline: + +- **Pipe nodes** — operations like `PipeLLM`, `PipeSequence`, `PipeExtract`, `PipeSearch` +- **Stuff nodes** — data flowing between pipes, typed by concepts (e.g. `CandidateProfile`, `Document`) + +### Edge kinds + +| Kind | Description | +| -------------------- | -------------------------------------------------- | +| `data` | Data flow — stuff produced by one pipe, consumed by another | +| `contains` | Containment — a controller pipe wraps child pipes | +| `batch_item` | Batch processing — items fanned out from a collection | +| `batch_aggregate` | Batch aggregation — items collected back | +| `parallel_combine` | Parallel results combined | + +### Example GraphSpec (minimal) + +```json +{ + "nodes": [ + { "id": "input_doc", "pipe_type": "Input" }, + { "id": "extract_text", "pipe_code": "extract_text", "pipe_type": "PipeExtract" }, + { "id": "pages", "pipe_type": "Stuff" }, + { "id": "summarize", "pipe_code": "summarize", "pipe_type": "PipeLLM" }, + { "id": "summary", "pipe_type": "Stuff" } + ], + "edges": [ + { "source": "input_doc", "target": "extract_text", "kind": "data" }, + { "source": "extract_text", "target": "pages", "kind": "data" }, + { "source": "pages", "target": "summarize", "kind": "data" }, + { "source": "summarize", "target": "summary", "kind": "data" } + ] +} +``` -`paletteColors` controls the colors used when building nodes and edges. Keys are CSS custom property names (e.g. `--color-pipe`, `--color-stuff`, `--color-edge`). See `graphConfig.ts` for the full default palette. +Full specification: [docs.pipelex.com — Execution Graph Tracing](https://docs.pipelex.com/latest/under-the-hood/execution-graph-tracing/#graphspec) -## Type boundary: domain types vs ReactFlow types +## Configuration -The graph module separates two type worlds: +### GraphConfig -- **Domain types** (`graph/types.ts`): `GraphNode`, `GraphEdge`, `GraphNodeData` — used by all pure graph logic (builders, layout, controllers, analysis). No React dependency. -- **ReactFlow types** (`graph/react/rfTypes.ts`): `AppNode`, `AppEdge`, `AppRFInstance` — ReactFlow generics parameterized with domain data. Used only in the React layer. +Controls layout and visual behavior. All fields are optional — `DEFAULT_GRAPH_CONFIG` provides sensible defaults. -If you bypass `GraphViewer` and drive ReactFlow directly, convert at the boundary: +```typescript +import { DEFAULT_GRAPH_CONFIG } from "@pipelex/mthds-ui"; -```ts -import { toAppNodes, toAppEdges } from "@pipelex/mthds-ui/graph/react"; +// Override specific settings +const myConfig = { + ...DEFAULT_GRAPH_CONFIG, + direction: "LR", + nodesep: 80, + ranksep: 50, +}; +``` -setNodes(toAppNodes(domainNodes)); -setEdges(toAppEdges(domainEdges)); +| Field | Type | Default | Description | +| ------------------ | --------------------- | ---------- | ---------------------------------------- | +| `direction` | `GraphDirection` | `"TB"` | Dagre layout direction | +| `showControllers` | `boolean` | `false` | Show controller group boxes | +| `nodesep` | `number` | `50` | Horizontal spacing between nodes | +| `ranksep` | `number` | `30` | Vertical spacing between ranks | +| `edgeType` | `EdgeType` | `"bezier"` | Edge curve style | +| `initialZoom` | `number \| null` | `null` | Override fit-view zoom (`null` = auto) | +| `panToTop` | `boolean` | `true` | Pan viewport to top after layout | +| `paletteColors` | `Record` | *(see below)* | CSS variable overrides for theming | + +### Theming with palette colors + +`GraphViewer` applies CSS custom properties from `paletteColors` on mount. Override colors by passing a custom config: + +```tsx + ``` -Never use `as any` to pass `GraphNode[]` to `setNodes` — use the mapping functions. +Default palette colors include: + +| Variable | Purpose | +| --------------------------- | -------------------------- | +| `--color-pipe` | Pipe node border/accent | +| `--color-pipe-bg` | Pipe node background | +| `--color-stuff` | Stuff node border/accent | +| `--color-stuff-bg` | Stuff node background | +| `--color-edge` | Edge line color | +| `--color-batch-item` | Batch item edge color | +| `--color-batch-aggregate` | Batch aggregate edge color | +| `--color-bg` | Graph background | +| `--color-bg-dots` | Background dot pattern | +| `--font-sans` | Node font family | +| `--font-mono` | Code/controller font | + +See `graphConfig.ts` for the full default palette. + +## Entry points + +| Import path | Content | +| ------------------------------- | ------------------------------------------------------------------ | +| `@pipelex/mthds-ui` | Pure-TS graph logic — types, builders, layout, controllers, config | +| `@pipelex/mthds-ui/graph/react` | React components — `GraphViewer`, label helpers, type converters | +| `@pipelex/mthds-ui/shiki` | MTHDS syntax highlighting with shiki | + +## Pure TypeScript usage + +Use the graph logic without React — build nodes/edges, run layout, and feed the result to your own renderer: + +```typescript +import { + buildGraph, + getLayoutedElements, + applyControllers, + DEFAULT_GRAPH_CONFIG, +} from "@pipelex/mthds-ui"; + +// Build graph data from a GraphSpec +const { graphData, analysis } = buildGraph(graphspec, "bezier"); + +// Apply dagre layout +const { nodes, edges } = getLayoutedElements( + graphData.nodes, + graphData.edges, + "TB", +); + +// Optionally wrap nodes in controller groups +const final = applyControllers(nodes, edges, graphspec, analysis, true); +``` ## Shiki integration Syntax-highlight MTHDS code with the bundled grammar and themes: -```ts +```typescript import { highlightMthds, getAvailableThemes } from "@pipelex/mthds-ui/shiki"; const html = await highlightMthds(mthdsSource, "pipelex-dark"); ``` -For custom shiki setups, use `getMthdsGrammar()` and `getMthdsTheme()` to register the MTHDS language and theme with your own highlighter instance. - ## Development ```bash diff --git a/package-lock.json b/package-lock.json index cba5385..f7da54f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@pipelex/mthds-ui", - "version": "0.1.3", + "version": "0.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pipelex/mthds-ui", - "version": "0.1.3", + "version": "0.1.4", "license": "MIT", "devDependencies": { "@eslint/js": "^9.39.4", diff --git a/src/graph/graphConfig.ts b/src/graph/graphConfig.ts index 2c27034..a110bd4 100644 --- a/src/graph/graphConfig.ts +++ b/src/graph/graphConfig.ts @@ -1,7 +1,7 @@ import type { GraphConfig } from "./types"; export const DEFAULT_GRAPH_CONFIG: GraphConfig = { - direction: "TB", + direction: "LR", showControllers: false, nodesep: 50, ranksep: 30, @@ -9,6 +9,7 @@ export const DEFAULT_GRAPH_CONFIG: GraphConfig = { initialZoom: null, panToTop: true, paletteColors: { + // Graph node/edge colors "--color-pipe": "#ff6b6b", "--color-pipe-bg": "rgba(224,108,117,0.18)", "--color-pipe-text": "#ffffff", @@ -27,6 +28,14 @@ export const DEFAULT_GRAPH_CONFIG: GraphConfig = { "--color-error-bg": "rgba(255,85,85,0.15)", "--color-accent": "#8BE9FD", "--color-warning": "#FFB86C", + // Base theme vars used by graph-core.css + "--color-bg": "#1e1e1e", + "--color-bg-dots": "#334155", + "--color-text-muted": "#94a3b8", + "--color-controller-text": "#94a3b8", + "--font-sans": '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', + "--font-mono": '"JetBrains Mono", "Monaco", "Menlo", monospace', + "--shadow-lg": "0 8px 24px rgba(0, 0, 0, 0.5)", }, }; diff --git a/src/graph/react/GraphViewer.tsx b/src/graph/react/GraphViewer.tsx index 06cee87..b62c3af 100644 --- a/src/graph/react/GraphViewer.tsx +++ b/src/graph/react/GraphViewer.tsx @@ -20,14 +20,15 @@ import { toAppNodes, toAppEdges } from "./rfTypes"; import { buildGraph } from "../graphBuilders"; import { getLayoutedElements, ensureControllerSpacing } from "../graphLayout"; import { applyControllers } from "../graphControllers"; +import { DEFAULT_GRAPH_CONFIG } from "../graphConfig"; import { hydrateLabels } from "./renderLabel"; import { controllerNodeTypes } from "./ControllerGroupNode"; export interface GraphViewerProps { graphspec: GraphSpec | null; - config: GraphConfig; - direction: GraphDirection; - showControllers: boolean; + config?: GraphConfig; + direction?: GraphDirection; + showControllers?: boolean; onNavigateToPipe?: (pipeCode: string) => void; onReactFlowInit?: (instance: AppRFInstance) => void; } @@ -42,8 +43,24 @@ function cloneCachedNodes(nodes: GraphNode[]): GraphNode[] { } export function GraphViewer(props: GraphViewerProps) { - const { graphspec, config, direction, showControllers, onNavigateToPipe, onReactFlowInit } = - props; + const { + graphspec, + config = DEFAULT_GRAPH_CONFIG, + direction = config.direction ?? DEFAULT_GRAPH_CONFIG.direction ?? "TB", + showControllers = config.showControllers ?? DEFAULT_GRAPH_CONFIG.showControllers ?? false, + onNavigateToPipe, + onReactFlowInit, + } = props; + + // Apply palette CSS vars on mount (so consumers don't have to) + React.useEffect(() => { + const palette = config.paletteColors ?? DEFAULT_GRAPH_CONFIG.paletteColors; + if (palette) { + for (const [cssVar, value] of Object.entries(palette)) { + document.documentElement.style.setProperty(cssVar, value); + } + } + }, [config.paletteColors]); const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); diff --git a/src/graph/react/graph-core.css b/src/graph/react/graph-core.css index f9a8d58..cf23a78 100644 --- a/src/graph/react/graph-core.css +++ b/src/graph/react/graph-core.css @@ -1,6 +1,6 @@ .react-flow-container { - width: 100%; - height: 100%; + position: absolute; + inset: 0; background: var(--color-bg); } .react-flow__node { diff --git a/src/graph/react/index.ts b/src/graph/react/index.ts index 8de9b25..71aaecd 100644 --- a/src/graph/react/index.ts +++ b/src/graph/react/index.ts @@ -1,3 +1,8 @@ +"use client"; + +import "@xyflow/react/dist/style.css"; +import "./graph-core.css"; + export { GraphViewer } from "./GraphViewer"; export type { GraphViewerProps } from "./GraphViewer"; export type { AppNode, AppEdge, AppRFInstance } from "./rfTypes"; diff --git a/tsup.config.ts b/tsup.config.ts index 4ed9898..206b250 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -15,7 +15,10 @@ export default defineConfig({ "react", "react-dom", "@xyflow/react", + /graph-core\.css$/, ], + // Keep graph-core.css as a standalone export for consumers that need it separately. + // GraphViewer.tsx also imports it directly, so it gets bundled into the React entry's CSS. onSuccess: async () => { mkdirSync("dist/graph/react", { recursive: true }); cpSync("src/graph/react/graph-core.css", "dist/graph/react/graph-core.css");