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}