Skip to main content

jellyflow_egui/
samples.rs

1use std::collections::BTreeMap;
2
3use jellyflow::core::{
4    CanvasPoint, CanvasSize, EdgeId, EdgeLabelAnchor, EdgeRouteKind, EdgeViewDescriptor, Graph,
5    GraphId, GraphOp, GraphTransaction, NodeId, NodeKindKey, PortCapacity, PortDirection, PortId,
6    PortKind,
7};
8use jellyflow::runtime::io::{NodeGraphEditorConfig, NodeGraphViewState};
9use jellyflow::runtime::runtime::connection::ConnectEdgeRequest;
10use jellyflow::runtime::runtime::create_node::CreateNodeRequest;
11use jellyflow::runtime::schema::{
12    NodeKitRegistry, NodeRegistry, NodeSchema, PortDecl, PortViewDescriptor,
13};
14use jellyflow::runtime::{DispatchOutcome, NodeGraphStore};
15use serde_json::json;
16use thiserror::Error;
17
18use crate::state::LayoutPresetChoice;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub enum SampleGraphKind {
22    #[default]
23    Workflow,
24    AutomationBuilder,
25    MindMap,
26    Tree,
27    OrgChart,
28    KnowledgeBoard,
29    Erd,
30    ShaderGraph,
31}
32
33impl SampleGraphKind {
34    pub const ALL: [Self; 8] = [
35        Self::Workflow,
36        Self::AutomationBuilder,
37        Self::MindMap,
38        Self::Tree,
39        Self::OrgChart,
40        Self::KnowledgeBoard,
41        Self::Erd,
42        Self::ShaderGraph,
43    ];
44
45    pub fn label(self) -> &'static str {
46        match self {
47            Self::Workflow => "Workflow",
48            Self::AutomationBuilder => "Automation builder",
49            Self::MindMap => "Mind map",
50            Self::Tree => "Tree",
51            Self::OrgChart => "Org chart",
52            Self::KnowledgeBoard => "Knowledge board",
53            Self::Erd => "ERD",
54            Self::ShaderGraph => "Shader graph",
55        }
56    }
57
58    pub fn default_layout(self) -> LayoutPresetChoice {
59        match self {
60            Self::Workflow | Self::AutomationBuilder => LayoutPresetChoice::Workflow,
61            Self::MindMap => LayoutPresetChoice::MindMap,
62            Self::Tree | Self::OrgChart => LayoutPresetChoice::Tree,
63            Self::KnowledgeBoard | Self::Erd | Self::ShaderGraph => LayoutPresetChoice::Freeform,
64        }
65    }
66}
67
68#[derive(Debug, Error)]
69pub enum SampleGraphError {
70    #[error("sample graph create node failed: {0}")]
71    Create(String),
72    #[error("sample graph connect failed: {0}")]
73    Connect(String),
74    #[error("sample graph missing node alias: {0}")]
75    MissingNode(String),
76    #[error("sample graph missing port for `{node}` direction {direction:?}")]
77    MissingPort {
78        node: String,
79        direction: PortDirection,
80    },
81}
82
83pub(crate) struct SampleGraph {
84    pub store: NodeGraphStore,
85    pub registry: NodeRegistry,
86    pub default_layout: LayoutPresetChoice,
87}
88
89pub(crate) fn sample_graph(kind: SampleGraphKind) -> Result<SampleGraph, SampleGraphError> {
90    let registry = sample_node_registry();
91    let mut builder = SampleGraphBuilder::new(registry.clone());
92    match kind {
93        SampleGraphKind::Workflow => populate_workflow(&mut builder)?,
94        SampleGraphKind::AutomationBuilder => populate_automation_builder(&mut builder)?,
95        SampleGraphKind::MindMap => populate_mind_map(&mut builder)?,
96        SampleGraphKind::Tree => populate_tree(&mut builder)?,
97        SampleGraphKind::OrgChart => populate_org_chart(&mut builder)?,
98        SampleGraphKind::KnowledgeBoard => populate_knowledge_board(&mut builder)?,
99        SampleGraphKind::Erd => populate_erd(&mut builder)?,
100        SampleGraphKind::ShaderGraph => populate_shader_graph(&mut builder)?,
101    }
102    builder.fit_view();
103
104    Ok(SampleGraph {
105        store: builder.store,
106        registry,
107        default_layout: kind.default_layout(),
108    })
109}
110
111fn populate_workflow(builder: &mut SampleGraphBuilder) -> Result<(), SampleGraphError> {
112    builder.node(
113        "brief",
114        "demo.start",
115        "Intake brief",
116        "Collect source notes and constraints",
117        CanvasPoint { x: -420.0, y: 20.0 },
118    )?;
119    builder.node(
120        "research",
121        "demo.task",
122        "Research",
123        "Cluster sources and claims",
124        CanvasPoint {
125            x: -120.0,
126            y: -80.0,
127        },
128    )?;
129    builder.node(
130        "decide",
131        "demo.decision",
132        "Decision",
133        "Enough signal to publish?",
134        CanvasPoint { x: 210.0, y: -80.0 },
135    )?;
136    builder.node(
137        "draft",
138        "demo.task",
139        "Draft",
140        "Write sections and citations",
141        CanvasPoint {
142            x: 520.0,
143            y: -150.0,
144        },
145    )?;
146    builder.node(
147        "review",
148        "demo.task",
149        "Review",
150        "Resolve comments",
151        CanvasPoint { x: 520.0, y: 20.0 },
152    )?;
153    builder.node(
154        "publish",
155        "demo.output",
156        "Publish",
157        "Export article and graph",
158        CanvasPoint { x: 840.0, y: -60.0 },
159    )?;
160
161    builder.connect("brief", "research")?;
162    builder.connect("research", "decide")?;
163    builder.connect("decide", "draft")?;
164    builder.connect("decide", "review")?;
165    builder.connect("draft", "publish")?;
166    builder.connect("review", "publish")?;
167    builder.apply_default_layout(SampleGraphKind::Workflow.default_layout());
168    Ok(())
169}
170
171fn populate_automation_builder(builder: &mut SampleGraphBuilder) -> Result<(), SampleGraphError> {
172    for (alias, kind, title, summary, pos) in [
173        (
174            "trigger",
175            "demo.trigger",
176            "Webhook trigger",
177            "Receives customer intake events",
178            CanvasPoint {
179                x: -620.0,
180                y: -40.0,
181            },
182        ),
183        (
184            "normalize",
185            "demo.tool",
186            "Normalize JSON",
187            "Maps request fields into variables",
188            CanvasPoint {
189                x: -300.0,
190                y: -120.0,
191            },
192        ),
193        (
194            "classify",
195            "demo.llm",
196            "Classify request",
197            "LLM chooses priority and route",
198            CanvasPoint { x: 20.0, y: -120.0 },
199        ),
200        (
201            "condition",
202            "demo.switch",
203            "Needs human review?",
204            "Branch on confidence and policy",
205            CanvasPoint {
206                x: 350.0,
207                y: -120.0,
208            },
209        ),
210        (
211            "notify",
212            "demo.tool",
213            "Notify assignee",
214            "Posts a Slack task",
215            CanvasPoint {
216                x: 680.0,
217                y: -210.0,
218            },
219        ),
220        (
221            "error",
222            "demo.error",
223            "Error path",
224            "Capture failed tool calls",
225            CanvasPoint { x: 680.0, y: 10.0 },
226        ),
227        (
228            "output",
229            "demo.workflow_output",
230            "Workflow output",
231            "Return ticket id and route",
232            CanvasPoint {
233                x: 1010.0,
234                y: -100.0,
235            },
236        ),
237    ] {
238        builder.node(alias, kind, title, summary, pos)?;
239    }
240
241    builder.connect("trigger", "normalize")?;
242    builder.connect("normalize", "classify")?;
243    builder.connect("classify", "condition")?;
244    builder.connect_ports("condition", "yes", "notify", "in")?;
245    builder.connect_ports("condition", "no", "error", "error")?;
246    builder.connect("notify", "output")?;
247    builder.connect("error", "output")?;
248    builder.apply_default_layout(SampleGraphKind::AutomationBuilder.default_layout());
249    Ok(())
250}
251
252fn populate_mind_map(builder: &mut SampleGraphBuilder) -> Result<(), SampleGraphError> {
253    builder.node(
254        "center",
255        "demo.topic",
256        "Product strategy",
257        "MindNode-style radial map",
258        CanvasPoint::default(),
259    )?;
260    for (alias, title, summary) in [
261        ("users", "Users", "Researchers, builders, editors"),
262        ("jobs", "Jobs", "Capture, connect, explain"),
263        ("channels", "Channels", "Desktop, native, embedded"),
264        ("risks", "Risks", "Trust, scale, migration"),
265        ("metrics", "Metrics", "Retention and graph reuse"),
266    ] {
267        builder.node(alias, "demo.idea", title, summary, CanvasPoint::default())?;
268        builder.connect("center", alias)?;
269    }
270    for (parent, alias, title, summary) in [
271        ("users", "researchers", "Researchers", "Long-form synthesis"),
272        ("users", "operators", "Operators", "Repeatable workflows"),
273        ("jobs", "clip", "Clip", "Save source fragments"),
274        ("jobs", "connect", "Connect", "Show evidence paths"),
275        ("channels", "egui", "egui", "Native embedded demo"),
276        ("channels", "web", "Web", "Future adapter target"),
277        ("risks", "perf", "Performance", "Large visible graphs"),
278        (
279            "risks",
280            "sync",
281            "Collaboration",
282            "CRDT-safe mutation boundary",
283        ),
284        ("metrics", "reuse", "Reuse", "Subgraphs imported again"),
285        ("metrics", "share", "Share", "Readable exported maps"),
286    ] {
287        builder.node(alias, "demo.idea", title, summary, CanvasPoint::default())?;
288        builder.connect(parent, alias)?;
289    }
290    builder.apply_default_layout(SampleGraphKind::MindMap.default_layout());
291    Ok(())
292}
293
294fn populate_tree(builder: &mut SampleGraphBuilder) -> Result<(), SampleGraphError> {
295    builder.node(
296        "root",
297        "demo.topic",
298        "Research outline",
299        "Tidy tree for hierarchy",
300        CanvasPoint::default(),
301    )?;
302    for (alias, title, summary) in [
303        ("intro", "1. Context", "Why this topic matters"),
304        ("method", "2. Method", "How evidence was collected"),
305        ("findings", "3. Findings", "Key claims and support"),
306        ("next", "4. Next steps", "Open decisions"),
307    ] {
308        builder.node(
309            alias,
310            "demo.section",
311            title,
312            summary,
313            CanvasPoint::default(),
314        )?;
315        builder.connect("root", alias)?;
316    }
317    for (parent, alias, title, summary) in [
318        ("intro", "problem", "Problem", "Fragmented knowledge work"),
319        ("intro", "audience", "Audience", "People building maps"),
320        ("method", "sources", "Sources", "Notes, PDFs, web clips"),
321        ("method", "criteria", "Criteria", "Trust and recency"),
322        (
323            "findings",
324            "finding-a",
325            "Finding A",
326            "Graphs need semantics",
327        ),
328        (
329            "findings",
330            "finding-b",
331            "Finding B",
332            "Adapters own rendering",
333        ),
334        ("findings", "finding-c", "Finding C", "Layouts are presets"),
335        ("next", "ship", "Ship", "Polish runnable demos"),
336        ("next", "measure", "Measure", "Benchmark large graphs"),
337    ] {
338        builder.node(
339            alias,
340            "demo.section",
341            title,
342            summary,
343            CanvasPoint::default(),
344        )?;
345        builder.connect(parent, alias)?;
346    }
347    builder.apply_default_layout(SampleGraphKind::Tree.default_layout());
348    Ok(())
349}
350
351fn populate_org_chart(builder: &mut SampleGraphBuilder) -> Result<(), SampleGraphError> {
352    for (alias, kind, title, summary, pos) in [
353        (
354            "ceo",
355            "demo.person",
356            "Avery Chen",
357            "CEO · strategy and capital",
358            CanvasPoint::default(),
359        ),
360        (
361            "product",
362            "demo.department",
363            "Product",
364            "Roadmap, research, UX",
365            CanvasPoint::default(),
366        ),
367        (
368            "engineering",
369            "demo.department",
370            "Engineering",
371            "Runtime, adapters, infra",
372            CanvasPoint::default(),
373        ),
374        (
375            "gtm",
376            "demo.department",
377            "Go to market",
378            "Sales, success, community",
379            CanvasPoint::default(),
380        ),
381        (
382            "pm",
383            "demo.person",
384            "Mina Rao",
385            "Head of Product",
386            CanvasPoint::default(),
387        ),
388        (
389            "design",
390            "demo.person",
391            "Noah Park",
392            "Design systems",
393            CanvasPoint::default(),
394        ),
395        (
396            "platform",
397            "demo.person",
398            "Iris Lin",
399            "Platform lead",
400            CanvasPoint::default(),
401        ),
402        (
403            "adapter",
404            "demo.person",
405            "Sam Patel",
406            "Adapter lead",
407            CanvasPoint::default(),
408        ),
409        (
410            "success",
411            "demo.person",
412            "Leah Gomez",
413            "Customer success",
414            CanvasPoint::default(),
415        ),
416    ] {
417        builder.node(alias, kind, title, summary, pos)?;
418    }
419
420    for (from, to) in [
421        ("ceo", "product"),
422        ("ceo", "engineering"),
423        ("ceo", "gtm"),
424        ("product", "pm"),
425        ("product", "design"),
426        ("engineering", "platform"),
427        ("engineering", "adapter"),
428        ("gtm", "success"),
429    ] {
430        builder.connect(from, to)?;
431    }
432    builder.apply_default_layout(SampleGraphKind::OrgChart.default_layout());
433    Ok(())
434}
435
436fn populate_knowledge_board(builder: &mut SampleGraphBuilder) -> Result<(), SampleGraphError> {
437    for (alias, kind, title, summary, pos) in [
438        (
439            "paper",
440            "demo.source",
441            "Paper excerpt",
442            "Evidence card imported from PDF",
443            CanvasPoint {
444                x: -520.0,
445                y: -160.0,
446            },
447        ),
448        (
449            "clip",
450            "demo.source",
451            "Web clip",
452            "Counterexample from a case study",
453            CanvasPoint { x: -520.0, y: 80.0 },
454        ),
455        (
456            "claim",
457            "demo.topic",
458            "Main claim",
459            "Graph apps need semantic nodes",
460            CanvasPoint {
461                x: -120.0,
462                y: -40.0,
463            },
464        ),
465        (
466            "question",
467            "demo.decision",
468            "Open question",
469            "What should stay headless?",
470            CanvasPoint {
471                x: 260.0,
472                y: -160.0,
473            },
474        ),
475        (
476            "action",
477            "demo.task",
478            "Action item",
479            "Build adapter examples",
480            CanvasPoint { x: 280.0, y: 80.0 },
481        ),
482        (
483            "output",
484            "demo.output",
485            "Export",
486            "Readable board snapshot",
487            CanvasPoint { x: 660.0, y: -40.0 },
488        ),
489    ] {
490        builder.node(alias, kind, title, summary, pos)?;
491    }
492    builder.connect("paper", "claim")?;
493    builder.connect("clip", "claim")?;
494    builder.connect("claim", "question")?;
495    builder.connect("claim", "action")?;
496    builder.connect("question", "output")?;
497    builder.connect("action", "output")?;
498    builder.apply_default_layout(SampleGraphKind::KnowledgeBoard.default_layout());
499    Ok(())
500}
501
502fn populate_erd(builder: &mut SampleGraphBuilder) -> Result<(), SampleGraphError> {
503    for (alias, title, fields, pos) in [
504        (
505            "customers",
506            "customers",
507            ["id", "email", "plan_id"].as_slice(),
508            CanvasPoint {
509                x: -260.0,
510                y: -100.0,
511            },
512        ),
513        (
514            "orders",
515            "orders",
516            ["id", "customer_id", "total"].as_slice(),
517            CanvasPoint {
518                x: -60.0,
519                y: -120.0,
520            },
521        ),
522        (
523            "order_items",
524            "order_items",
525            ["id", "order_id", "sku_id", "qty"].as_slice(),
526            CanvasPoint {
527                x: 140.0,
528                y: -120.0,
529            },
530        ),
531        (
532            "skus",
533            "skus",
534            ["id", "title", "price"].as_slice(),
535            CanvasPoint {
536                x: 340.0,
537                y: -120.0,
538            },
539        ),
540        (
541            "plans",
542            "plans",
543            ["id", "name", "limits"].as_slice(),
544            CanvasPoint {
545                x: -260.0,
546                y: 130.0,
547            },
548        ),
549    ] {
550        builder.table_node(alias, title, fields, pos)?;
551    }
552
553    builder.connect_ports("customers", "pk", "orders", "fk")?;
554    builder.connect_ports("orders", "pk", "order_items", "fk")?;
555    builder.connect_ports("skus", "pk", "order_items", "fk")?;
556    builder.connect_ports("plans", "pk", "customers", "fk")?;
557    builder.apply_default_layout(SampleGraphKind::Erd.default_layout());
558    Ok(())
559}
560
561fn populate_shader_graph(builder: &mut SampleGraphBuilder) -> Result<(), SampleGraphError> {
562    builder.shader_node(
563        "texture",
564        "demo.shader.texture_sample",
565        json!({
566            "title": "Texture Sample",
567            "summary": "Sample base color from UV",
568            "ports": {
569                "inputs": ["vec2 uv"],
570                "outputs": ["vec4 color"]
571            },
572            "preview": {
573                "texture": "checker"
574            }
575        }),
576        CanvasPoint {
577            x: -320.0,
578            y: -80.0,
579        },
580    )?;
581    builder.shader_node(
582        "tint",
583        "demo.shader.texture_sample",
584        json!({
585            "title": "Tint Ramp",
586            "summary": "Procedural color ramp",
587            "ports": {
588                "inputs": ["vec2 uv"],
589                "outputs": ["vec4 color"]
590            },
591            "preview": {
592                "texture": "warm ramp"
593            }
594        }),
595        CanvasPoint {
596            x: -320.0,
597            y: 120.0,
598        },
599    )?;
600    builder.shader_node(
601        "mix",
602        "demo.shader.mix",
603        json!({
604            "title": "Mix",
605            "summary": "Blend albedo with tint",
606            "ports": {
607                "inputs": ["vec4 albedo", "vec4 tint", "float factor"],
608                "outputs": ["vec4 result"]
609            },
610            "config": {
611                "factor": {
612                    "type": "float",
613                    "default": 0.5
614                }
615            },
616            "preview": {
617                "result": "gradient"
618            },
619            "dynamic_inputs": [
620                { "id": "a", "name": "Albedo", "ty": "vec4", "port": "a" },
621                { "id": "b", "name": "Tint", "ty": "vec4", "port": "b" },
622                { "id": "factor", "name": "Factor", "ty": "float", "port": "factor" }
623            ]
624        }),
625        CanvasPoint { x: 40.0, y: 20.0 },
626    )?;
627
628    builder.connect_ports("texture", "color", "mix", "a")?;
629    builder.connect_ports("tint", "color", "mix", "b")?;
630    builder.apply_default_layout(SampleGraphKind::ShaderGraph.default_layout());
631    Ok(())
632}
633
634struct SampleGraphBuilder {
635    store: NodeGraphStore,
636    registry: NodeRegistry,
637    aliases: BTreeMap<String, NodeId>,
638}
639
640impl SampleGraphBuilder {
641    fn new(registry: NodeRegistry) -> Self {
642        Self {
643            store: NodeGraphStore::new(
644                Graph::new(GraphId::new()),
645                NodeGraphViewState::default(),
646                NodeGraphEditorConfig::default().with_spatial_index_enabled(true),
647            ),
648            registry,
649            aliases: BTreeMap::new(),
650        }
651    }
652
653    fn node(
654        &mut self,
655        alias: &str,
656        kind: &str,
657        title: &str,
658        summary: &str,
659        pos: CanvasPoint,
660    ) -> Result<NodeId, SampleGraphError> {
661        let outcome = self
662            .store
663            .apply_create_node_from_schema(
664                &self.registry,
665                CreateNodeRequest::new(NodeKindKey::from(kind), pos),
666            )
667            .map_err(|err| SampleGraphError::Create(err.to_string()))?;
668        let node = outcome.node_id();
669        self.set_node_payload(node, title, summary)
670            .map_err(SampleGraphError::Create)?;
671        self.aliases.insert(alias.to_owned(), node);
672        Ok(node)
673    }
674
675    fn table_node(
676        &mut self,
677        alias: &str,
678        title: &str,
679        fields: &[&str],
680        pos: CanvasPoint,
681    ) -> Result<NodeId, SampleGraphError> {
682        let summary = fields.join(" · ");
683        let node = self.node(alias, "demo.table", title, &summary, pos)?;
684        self.set_table_fields(node, fields)
685            .map_err(SampleGraphError::Create)?;
686        Ok(node)
687    }
688
689    fn shader_node(
690        &mut self,
691        alias: &str,
692        kind: &str,
693        data: serde_json::Value,
694        pos: CanvasPoint,
695    ) -> Result<NodeId, SampleGraphError> {
696        let outcome = self
697            .store
698            .apply_create_node_from_schema(
699                &self.registry,
700                CreateNodeRequest::new(NodeKindKey::from(kind), pos),
701            )
702            .map_err(|err| SampleGraphError::Create(err.to_string()))?;
703        let node = outcome.node_id();
704        self.set_node_data(node, data)
705            .map_err(SampleGraphError::Create)?;
706        self.aliases.insert(alias.to_owned(), node);
707        Ok(node)
708    }
709
710    fn connect(&mut self, from_alias: &str, to_alias: &str) -> Result<(), SampleGraphError> {
711        self.connect_by(from_alias, None, to_alias, None)
712    }
713
714    fn connect_ports(
715        &mut self,
716        from_alias: &str,
717        from_port_key: &str,
718        to_alias: &str,
719        to_port_key: &str,
720    ) -> Result<(), SampleGraphError> {
721        self.connect_by(from_alias, Some(from_port_key), to_alias, Some(to_port_key))
722    }
723
724    fn connect_by(
725        &mut self,
726        from_alias: &str,
727        from_port_key: Option<&str>,
728        to_alias: &str,
729        to_port_key: Option<&str>,
730    ) -> Result<(), SampleGraphError> {
731        let from = self.node_id(from_alias)?;
732        let to = self.node_id(to_alias)?;
733        let source = self.port(from, from_alias, PortDirection::Out, from_port_key)?;
734        let target = self.port(to, to_alias, PortDirection::In, to_port_key)?;
735        let mode = self.store.resolved_interaction_state().connection_mode;
736        let outcome = self
737            .store
738            .apply_connect_edge(ConnectEdgeRequest::new(source, target, mode))
739            .map_err(|err| {
740                SampleGraphError::Connect(format!(
741                    "{from_alias}({from_port_key:?}) -> {to_alias}({to_port_key:?}): {err}"
742                ))
743            })?;
744        if let Some(edge) = outcome.as_ref().and_then(edge_from_outcome) {
745            self.decorate_edge(edge, from_alias, to_alias)?;
746        }
747        Ok(())
748    }
749
750    fn apply_default_layout(&mut self, choice: LayoutPresetChoice) {
751        let request = choice.builder().all().build();
752        let _ = self.store.apply_layout(
753            &request,
754            jellyflow::layout::builtin_layout_engine_registry(),
755        );
756    }
757
758    fn fit_view(&mut self) {
759        let nodes = self
760            .store
761            .graph()
762            .nodes()
763            .values()
764            .filter_map(|node| {
765                let size = node.size?;
766                Some(jellyflow::runtime::runtime::fit_view::FitViewNodeInfo {
767                    pos: node.pos,
768                    origin: node.origin,
769                    size_px: (size.width, size.height),
770                })
771            })
772            .collect::<Vec<_>>();
773        let Some((pan, zoom)) = jellyflow::runtime::runtime::fit_view::compute_fit_view_target(
774            &nodes,
775            jellyflow::runtime::runtime::fit_view::FitViewComputeOptions {
776                viewport_width_px: 1100.0,
777                viewport_height_px: 700.0,
778                node_origin: (0.0, 0.0),
779                padding: 0.14,
780                margin_px_fallback: 56.0,
781                min_zoom: 0.2,
782                max_zoom: 2.5,
783            },
784        ) else {
785            return;
786        };
787        self.store.set_viewport(pan, zoom);
788    }
789
790    fn node_id(&self, alias: &str) -> Result<NodeId, SampleGraphError> {
791        self.aliases
792            .get(alias)
793            .copied()
794            .ok_or_else(|| SampleGraphError::MissingNode(alias.to_owned()))
795    }
796
797    fn port(
798        &self,
799        node: NodeId,
800        alias: &str,
801        direction: PortDirection,
802        key: Option<&str>,
803    ) -> Result<PortId, SampleGraphError> {
804        self.store
805            .graph()
806            .nodes()
807            .get(&node)
808            .and_then(|record| {
809                record.ports.iter().copied().find(|port| {
810                    self.store.graph().ports().get(port).is_some_and(|record| {
811                        record.dir == direction
812                            && key.is_none_or(|expected| record.key.0 == expected)
813                    })
814                })
815            })
816            .ok_or_else(|| SampleGraphError::MissingPort {
817                node: alias.to_owned(),
818                direction,
819            })
820    }
821
822    fn set_node_payload(
823        &mut self,
824        node: NodeId,
825        title: &str,
826        summary: &str,
827    ) -> Result<DispatchOutcome, String> {
828        let from = self
829            .store
830            .graph()
831            .nodes()
832            .get(&node)
833            .map(|node| node.data.clone())
834            .ok_or_else(|| format!("missing node `{node:?}`"))?;
835        let mut to = from.clone();
836        if !to.is_object() {
837            to = json!({});
838        }
839        if let Some(object) = to.as_object_mut() {
840            object.insert("title".to_owned(), json!(title));
841            object.insert("summary".to_owned(), json!(summary));
842        }
843        self.store
844            .dispatch_transaction(
845                &jellyflow::core::GraphTransaction::from_ops([
846                    jellyflow::core::GraphOp::SetNodeData { id: node, from, to },
847                ])
848                .with_label("Set sample node data"),
849            )
850            .map_err(|err| err.to_string())
851    }
852
853    fn set_node_data(
854        &mut self,
855        node: NodeId,
856        to: serde_json::Value,
857    ) -> Result<DispatchOutcome, String> {
858        let from = self
859            .store
860            .graph()
861            .nodes()
862            .get(&node)
863            .map(|node| node.data.clone())
864            .ok_or_else(|| format!("missing node `{node:?}`"))?;
865        self.store
866            .dispatch_transaction(
867                &jellyflow::core::GraphTransaction::from_ops([
868                    jellyflow::core::GraphOp::SetNodeData { id: node, from, to },
869                ])
870                .with_label("Set sample node data"),
871            )
872            .map_err(|err| err.to_string())
873    }
874
875    fn set_table_fields(
876        &mut self,
877        node: NodeId,
878        fields: &[&str],
879    ) -> Result<DispatchOutcome, String> {
880        let from = self
881            .store
882            .graph()
883            .nodes()
884            .get(&node)
885            .map(|node| node.data.clone())
886            .ok_or_else(|| format!("missing node `{node:?}`"))?;
887        let mut to = from.clone();
888        if !to.is_object() {
889            to = json!({});
890        }
891        let (field_order, field_map) = table_field_payload(fields);
892        if let Some(object) = to.as_object_mut() {
893            object.insert("field_order".to_owned(), json!(field_order));
894            object.insert("fields".to_owned(), json!(field_map));
895        }
896        self.store
897            .dispatch_transaction(
898                &jellyflow::core::GraphTransaction::from_ops([
899                    jellyflow::core::GraphOp::SetNodeData { id: node, from, to },
900                ])
901                .with_label("Set table sample fields"),
902            )
903            .map_err(|err| err.to_string())
904    }
905
906    fn decorate_edge(
907        &mut self,
908        edge: EdgeId,
909        from_alias: &str,
910        to_alias: &str,
911    ) -> Result<(), SampleGraphError> {
912        let label = edge_label_for_aliases(from_alias, to_alias);
913        let data = json!({ "label": label, "from": from_alias, "to": to_alias });
914        let view = EdgeViewDescriptor::new()
915            .with_renderer_key("sample-edge")
916            .with_label(label)
917            .with_label_anchor(EdgeLabelAnchor::Center)
918            .with_target_marker_key("arrow")
919            .with_style_token("default")
920            .with_route_kind(EdgeRouteKind::Orthogonal)
921            .with_hit_target_width(24.0);
922        self.store
923            .dispatch_transaction(
924                &GraphTransaction::from_ops([
925                    GraphOp::SetEdgeData {
926                        id: edge,
927                        from: serde_json::Value::Null,
928                        to: data,
929                    },
930                    GraphOp::SetEdgeView {
931                        id: edge,
932                        from: EdgeViewDescriptor::default(),
933                        to: view,
934                    },
935                ])
936                .with_label("Set sample edge metadata"),
937            )
938            .map(|_| ())
939            .map_err(|err| SampleGraphError::Connect(err.to_string()))
940    }
941}
942
943fn table_field_payload(fields: &[&str]) -> (Vec<String>, BTreeMap<String, String>) {
944    let mut order = Vec::new();
945    let mut data = BTreeMap::new();
946    let mut has_foreign_key = false;
947    for field in fields {
948        let key = if *field == "id" {
949            "primary_key".to_owned()
950        } else if field.ends_with("_id") && !has_foreign_key {
951            has_foreign_key = true;
952            "foreign_key".to_owned()
953        } else {
954            (*field).to_owned()
955        };
956        order.push(key.clone());
957        data.insert(key, (*field).to_owned());
958    }
959    (order, data)
960}
961
962fn edge_from_outcome(outcome: &DispatchOutcome) -> Option<EdgeId> {
963    outcome.committed().ops().iter().find_map(|op| match op {
964        GraphOp::AddEdge { id, .. } => Some(*id),
965        _ => None,
966    })
967}
968
969fn edge_label_for_aliases(from_alias: &str, to_alias: &str) -> &'static str {
970    match (from_alias, to_alias) {
971        ("decide", "draft") => "yes",
972        ("decide", "review") => "no",
973        ("trigger", "normalize") => "event",
974        ("normalize", "classify") => "variables",
975        ("classify", "condition") => "classification",
976        ("condition", "notify") => "yes",
977        ("condition", "error") => "error",
978        ("notify", "output") => "success",
979        ("error", "output") => "recovered",
980        ("customers", "orders") => "1:N",
981        ("orders", "order_items") => "1:N",
982        ("skus", "order_items") => "1:N",
983        ("plans", "customers") => "1:N",
984        ("ceo", "product") | ("ceo", "engineering") | ("ceo", "gtm") => "reports",
985        ("product", "pm")
986        | ("product", "design")
987        | ("engineering", "platform")
988        | ("engineering", "adapter")
989        | ("gtm", "success") => "member",
990        ("question", "output") => "answer",
991        ("action", "output") => "deliver",
992        _ => "flow",
993    }
994}
995
996fn sample_node_registry() -> NodeRegistry {
997    let mut registry = NodeKitRegistry::builtin().node_registry();
998    registry.register(
999        NodeSchema::builder("demo.section", "Section")
1000            .category(["Tree"])
1001            .keywords(["outline", "heading", "chapter"])
1002            .renderer_key("section-card")
1003            .default_size(CanvasSize {
1004                width: 190.0,
1005                height: 78.0,
1006            })
1007            .port(input_port("parent"))
1008            .port(output_port("child"))
1009            .default_data(json!({ "title": "Section", "summary": "Outline item" }))
1010            .build(),
1011    );
1012    registry.register(
1013        NodeSchema::builder("demo.department", "Department")
1014            .category(["Org chart"])
1015            .keywords(["team", "department", "hierarchy"])
1016            .renderer_key("section-card")
1017            .default_size(CanvasSize {
1018                width: 206.0,
1019                height: 84.0,
1020            })
1021            .port(input_port("manager"))
1022            .port(output_port("reports"))
1023            .default_data(json!({ "title": "Department", "summary": "Team branch" }))
1024            .build(),
1025    );
1026    registry.register(
1027        NodeSchema::builder("demo.person", "Person")
1028            .category(["Org chart"])
1029            .keywords(["employee", "role", "reports"])
1030            .renderer_key("idea-card")
1031            .default_size(CanvasSize {
1032                width: 196.0,
1033                height: 78.0,
1034            })
1035            .port(input_port("manager"))
1036            .port(output_port("reports"))
1037            .default_data(json!({ "title": "Person", "summary": "Role and ownership" }))
1038            .build(),
1039    );
1040    registry.register(
1041        NodeSchema::builder("demo.source", "Source")
1042            .category(["Knowledge"])
1043            .keywords(["paper", "quote", "annotation", "marginnote"])
1044            .renderer_key("source-card")
1045            .default_size(CanvasSize {
1046                width: 210.0,
1047                height: 92.0,
1048            })
1049            .port(output_port("out"))
1050            .default_data(json!({ "title": "Source", "summary": "Evidence card" }))
1051            .build(),
1052    );
1053    registry
1054}
1055
1056fn input_port(key: &str) -> PortDecl {
1057    PortDecl::new(key, PortDirection::In, PortKind::Data, PortCapacity::Multi)
1058        .with_label(key)
1059        .on_left()
1060}
1061
1062fn output_port(key: &str) -> PortDecl {
1063    PortDecl::new(key, PortDirection::Out, PortKind::Data, PortCapacity::Multi)
1064        .with_label(key)
1065        .with_view(PortViewDescriptor::right())
1066}