This is an automated email from the ASF dual-hosted git repository.

xqhu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git


The following commit(s) were added to refs/heads/master by this push:
     new d9ddcd4bf51 Add YAML Editor and Visualization Panel (#35947)
d9ddcd4bf51 is described below

commit d9ddcd4bf5199b28829fa69454bb5c34aad5e6fe
Author: Chenzo <[email protected]>
AuthorDate: Sat Sep 6 20:57:51 2025 +0800

    Add YAML Editor and Visualization Panel (#35947)
    
    * Yaml Panel
    
    * Update CHANGES.md
    
    * Update
    
    * Update 
sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/apache_beam_jupyterlab_sidepanel/yaml_parse_utils.py
    
    Co-authored-by: gemini-code-assist[bot] 
<176961590+gemini-code-assist[bot]@users.noreply.github.com>
    
    * Update CHANGES.md
    
    * Fix CI/CD fails
    
    * Update CHANGES.md
    
    * Update yaml_parse_utils.py
    
    * Update CHANGES.md
    
    * Update CHANGES.md
    
    ---------
    
    Co-authored-by: gemini-code-assist[bot] 
<176961590+gemini-code-assist[bot]@users.noreply.github.com>
---
 CHANGES.md                                         |   3 +-
 .../yaml_parse_utils.py                            | 176 +++++++
 .../apache-beam-jupyterlab-sidepanel/package.json  |  17 +-
 .../src/SidePanel.ts                               |  11 +-
 .../apache-beam-jupyterlab-sidepanel/src/index.ts  |  35 +-
 .../src/yaml/CustomStyle.tsx                       | 179 +++++++
 .../src/yaml/DataType.ts                           |  37 ++
 .../src/yaml/EditablePanel.tsx                     | 408 ++++++++++++++++
 .../src/yaml/EmojiMap.ts                           |  75 +++
 .../src/yaml/Yaml.tsx                              | 322 +++++++++++++
 .../src/yaml/YamlEditor.tsx                        | 338 +++++++++++++
 .../src/yaml/YamlFlow.tsx                          | 227 +++++++++
 .../src/yaml/YamlWidget.tsx                        |  34 ++
 .../style/index.css                                |   3 +
 .../style/mdc-theme.css                            |   4 +-
 .../style/{index.css => yaml/Yaml.css}             |  30 +-
 .../style/{index.css => yaml/YamlEditor.css}       |  26 +-
 .../style/yaml/YamlFlow.css                        | 168 +++++++
 .../apache-beam-jupyterlab-sidepanel/yarn.lock     | 535 ++++++++++++++++++++-
 19 files changed, 2602 insertions(+), 26 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 0394882d8a7..af1ab8c6e3c 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -65,6 +65,7 @@
 
 * New highly anticipated feature X added to Python SDK 
([#X](https://github.com/apache/beam/issues/X)).
 * New highly anticipated feature Y added to Java SDK 
([#Y](https://github.com/apache/beam/issues/Y)).
+* (Python) Add YAML Editor and Visualization Panel 
([#35772](https://github.com/apache/beam/issues/35772)).
 
 ## I/Os
 
@@ -96,7 +97,7 @@
 
 * New highly anticipated feature X added to Python SDK 
([#X](https://github.com/apache/beam/issues/X)).
 * New highly anticipated feature Y added to Java SDK 
([#Y](https://github.com/apache/beam/issues/Y)).
-* (Python) Prism runner now enabled by default for most Python pipelines using 
the direct runner ([#34612](https://github.com/apache/beam/pull/34612)). This 
may break some tests, see https://github.com/apache/beam/pull/34612 for details 
on how to handle issues.
+* [Python] Prism runner now enabled by default for most Python pipelines using 
the direct runner ([#34612](https://github.com/apache/beam/pull/34612)). This 
may break some tests, see https://github.com/apache/beam/pull/34612 for details 
on how to handle issues.
 
 ## I/Os
 
diff --git 
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/apache_beam_jupyterlab_sidepanel/yaml_parse_utils.py
 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/apache_beam_jupyterlab_sidepanel/yaml_parse_utils.py
new file mode 100644
index 00000000000..aebca7b85d6
--- /dev/null
+++ 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/apache_beam_jupyterlab_sidepanel/yaml_parse_utils.py
@@ -0,0 +1,176 @@
+#  Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+#  use this file except in compliance with the License. You may obtain a copy 
of
+#  the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+#  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#  License for the specific language governing permissions and limitations 
under
+#  the License.
+
+import dataclasses
+import json
+from dataclasses import dataclass
+from typing import Any
+from typing import Dict
+from typing import List
+from typing import TypedDict
+
+import yaml
+
+import apache_beam as beam
+from apache_beam.yaml.main import build_pipeline_components_from_yaml
+
+# ======================== Type Definitions ========================
+
+
+@dataclass
+class NodeData:
+  id: str
+  label: str
+  type: str = ""
+
+  def __post_init__(self):
+    # Ensure ID is not empty
+    if not self.id:
+      raise ValueError("Node ID cannot be empty")
+
+
+@dataclass
+class EdgeData:
+  source: str
+  target: str
+  label: str = ""
+
+  def __post_init__(self):
+    if not self.source or not self.target:
+      raise ValueError("Edge source and target cannot be empty")
+
+
+class FlowGraph(TypedDict):
+  nodes: List[Dict[str, Any]]
+  edges: List[Dict[str, Any]]
+
+
+# ======================== Main Function ========================
+
+
+def parse_beam_yaml(yaml_str: str, isDryRunMode: bool = False) -> str:
+  """
+    Parse Beam YAML and convert to flow graph data structure
+    
+    Args:
+        yaml_str: Input YAML string
+        
+    Returns:
+        Standardized response format:
+        - Success: {'status': 'success', 'data': {...}, 'error': None}
+        - Failure: {'status': 'error', 'data': None, 'error': 'message'}
+    """
+  # Phase 1: YAML Parsing
+  try:
+    parsed_yaml = yaml.safe_load(yaml_str)
+    if not parsed_yaml or 'pipeline' not in parsed_yaml:
+      return build_error_response(
+          "Invalid YAML structure: missing 'pipeline' section")
+  except yaml.YAMLError as e:
+    return build_error_response(f"YAML parsing error: {str(e)}")
+
+  # Phase 2: Pipeline Validation
+  try:
+    options, constructor = build_pipeline_components_from_yaml(
+        yaml_str,
+        [],
+        validate_schema='per_transform'
+    )
+    if isDryRunMode:
+      with beam.Pipeline(options=options) as p:
+        constructor(p)
+  except Exception as e:
+    return build_error_response(f"Pipeline validation failed: {str(e)}")
+
+  # Phase 3: Graph Construction
+  try:
+    pipeline = parsed_yaml['pipeline']
+    transforms = pipeline.get('transforms', [])
+
+    nodes: List[NodeData] = []
+    edges: List[EdgeData] = []
+
+    nodes.append(NodeData(id='0', label='Input', type='input'))
+    nodes.append(NodeData(id='1', label='Output', type='output'))
+
+    # Process transform nodes
+    for idx, transform in enumerate(transforms):
+      if not isinstance(transform, dict):
+        continue
+
+      payload = {k: v for k, v in transform.items() if k not in {"type"}}
+
+      node_id = f"t{idx}"
+      node_data = NodeData(
+          id=node_id,
+          label=transform.get('type', 'unnamed'),
+          type='default',
+          **payload)
+      nodes.append(node_data)
+
+      # Create connections between nodes
+      if idx > 0:
+        edges.append(
+            EdgeData(source=f"t{idx-1}", target=node_id, label='chain'))
+
+    if transforms:
+      edges.append(EdgeData(source='0', target='t0', label='start'))
+      edges.append(EdgeData(source=node_id, target='1', label='stop'))
+
+    def to_dict(node):
+      if hasattr(node, '__dataclass_fields__'):
+        return dataclasses.asdict(node)
+      return node
+
+    nodes_serializable = [to_dict(n) for n in nodes]
+
+    return build_success_response(
+        nodes=nodes_serializable, edges=[dataclasses.asdict(e) for e in edges])
+
+  except Exception as e:
+    return build_error_response(f"Graph construction failed: {str(e)}")
+
+
+# ======================== Utility Functions ========================
+
+
+def build_success_response(
+    nodes: List[Dict[str, Any]], edges: List[Dict[str, Any]]) -> str:
+  """Build success response"""
+  return json.dumps({'data': {'nodes': nodes, 'edges': edges}, 'error': None})
+
+
+def build_error_response(error_msg: str) -> str:
+  """Build error response"""
+  return json.dumps({'data': None, 'error': error_msg})
+
+
+if __name__ == "__main__":
+  # Example usage
+  example_yaml = """
+pipeline:
+  transforms:
+    - type: ReadFromCsv
+      name: A
+      config:
+        path: /path/to/input*.csv
+    - type: WriteToJson
+      name: B
+      config:
+        path: /path/to/output.json
+      input: ReadFromCsv
+    - type: Join
+      input: [A, B]
+    """
+
+  response = parse_beam_yaml(example_yaml, isDryRunMode=False)
+  print(response)
diff --git 
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/package.json
 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/package.json
index 8b51461f6cd..eef3fcaa80f 100644
--- 
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/package.json
+++ 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/package.json
@@ -47,27 +47,37 @@
     "@jupyterlab/launcher": "^4.3.6",
     "@jupyterlab/mainmenu": "^4.3.6",
     "@lumino/widgets": "^2.2.1",
+    "@monaco-editor/react": "^4.7.0",
     "@rmwc/base": "^14.0.0",
     "@rmwc/button": "^8.0.6",
+    "@rmwc/card": "^14.3.5",
     "@rmwc/data-table": "^8.0.6",
     "@rmwc/dialog": "^8.0.6",
     "@rmwc/drawer": "^8.0.6",
     "@rmwc/fab": "^8.0.6",
+    "@rmwc/grid": "^14.3.5",
     "@rmwc/list": "^8.0.6",
     "@rmwc/ripple": "^14.0.0",
     "@rmwc/textfield": "^8.0.6",
     "@rmwc/tooltip": "^8.0.6",
     "@rmwc/top-app-bar": "^8.0.6",
+    "@rmwc/touch-target": "^14.3.5",
+    "@xyflow/react": "^12.8.2",
+    "dagre": "^0.8.5",
+    "lodash": "^4.17.21",
     "material-design-icons": "^3.0.1",
     "react": "^18.2.0",
-    "react-dom": "^18.2.0"
+    "react-dom": "^18.2.0",
+    "react-split": "^2.0.14"
   },
   "devDependencies": {
     "@jupyterlab/builder": "^4.3.6",
     "@testing-library/dom": "^9.3.0",
     "@testing-library/jest-dom": "^6.1.4",
     "@testing-library/react": "^14.0.0",
+    "@types/dagre": "^0.7.53",
     "@types/jest": "^29.5.14",
+    "@types/lodash": "^4.17.20",
     "@types/react": "^18.2.0",
     "@types/react-dom": "^18.2.0",
     "@typescript-eslint/eslint-plugin": "^7.3.1",
@@ -97,5 +107,6 @@
   "test": "jest",
   "resolutions": {
     "@types/react": "^18.2.0"
-  }
-}
+  },
+  "packageManager": 
"[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
+}
\ No newline at end of file
diff --git 
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/SidePanel.ts
 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/SidePanel.ts
index fb86b0a53fd..d8f19c27884 100644
--- 
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/SidePanel.ts
+++ 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/SidePanel.ts
@@ -58,14 +58,17 @@ export class SidePanel extends BoxPanel {
     const sessionModelItr = manager.sessions.running();
     const firstModel = sessionModelItr.next();
     let onlyOneUniqueKernelExists = true;
-    if (firstModel === undefined) {
-      // There is zero unique running kernel.
+
+    if (firstModel.done) {
+      // No Running kernel
       onlyOneUniqueKernelExists = false;
     } else {
+      // firstModel.value is the first session
       let sessionModel = sessionModelItr.next();
-      while (sessionModel !== undefined) {
+
+      while (!sessionModel.done) {
+        // Check if there is more than one unique kernel
         if (sessionModel.value.kernel.id !== firstModel.value.kernel.id) {
-          // There is more than one unique running kernel.
           onlyOneUniqueKernelExists = false;
           break;
         }
diff --git 
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/index.ts
 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/index.ts
index 3f2b02d11b5..92a1ea3cdbb 100644
--- 
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/index.ts
+++ 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/index.ts
@@ -28,12 +28,15 @@ import { SidePanel } from './SidePanel';
 import {
   InteractiveInspectorWidget
 } from './inspector/InteractiveInspectorWidget';
+import { YamlWidget } from './yaml/YamlWidget';
 
 namespace CommandIDs {
   export const open_inspector =
     'apache-beam-jupyterlab-sidepanel:open_inspector';
   export const open_clusters_panel =
     'apache-beam-jupyterlab-sidepanel:open_clusters_panel';
+  export const open_yaml_editor =
+    'apache-beam-jupyterlab-sidepanel:open_yaml_editor';
 }
 
 /**
@@ -67,6 +70,7 @@ function activate(
   const category = 'Interactive Beam';
   const inspectorCommandLabel = 'Open Inspector';
   const clustersCommandLabel = 'Manage Clusters';
+  const yamlCommandLabel = 'Edit YAML Pipeline';
   const { commands, shell, serviceManager } = app;
 
   async function createInspectorPanel(): Promise<SidePanel> {
@@ -105,6 +109,24 @@ function activate(
     return panel;
   }
 
+  async function createYamlPanel(): Promise<SidePanel> {
+    const sessionContext = new SessionContext({
+      sessionManager: serviceManager.sessions,
+      specsManager: serviceManager.kernelspecs,
+      name: 'Interactive Beam YAML Session'
+    });
+    const yamlEditor = new YamlWidget(sessionContext);
+    const panel = new SidePanel(
+      serviceManager,
+      rendermime,
+      sessionContext,
+      'Interactive Beam YAML Editor',
+      yamlEditor
+    );
+    activatePanel(panel);
+    return panel;
+  }
+
   function activatePanel(panel: SidePanel): void {
     shell.add(panel, 'main');
     shell.activateById(panel.id);
@@ -122,6 +144,12 @@ function activate(
     execute: createClustersPanel
   });
 
+  // The open_yaml_editor command is also used by the below entry points.
+  commands.addCommand(CommandIDs.open_yaml_editor, {
+    label: yamlCommandLabel,
+    execute: createYamlPanel
+  });
+
   // Entry point in launcher.
   if (launcher) {
     launcher.add({
@@ -132,6 +160,10 @@ function activate(
       command: CommandIDs.open_clusters_panel,
       category: category
     });
+    launcher.add({
+      command: CommandIDs.open_yaml_editor,
+      category: category
+    });
   }
 
   // Entry point in top menu.
@@ -140,10 +172,11 @@ function activate(
   mainMenu.addMenu(menu);
   menu.addItem({ command: CommandIDs.open_inspector });
   menu.addItem({ command: CommandIDs.open_clusters_panel });
+  menu.addItem({ command: CommandIDs.open_yaml_editor });
 
   // Entry point in commands palette.
   palette.addItem({ command: CommandIDs.open_inspector, category });
   palette.addItem({ command: CommandIDs.open_clusters_panel, category });
+  palette.addItem({ command: CommandIDs.open_yaml_editor, category });
 }
-
 export default extension;
diff --git 
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/CustomStyle.tsx
 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/CustomStyle.tsx
new file mode 100644
index 00000000000..87d93de0b60
--- /dev/null
+++ 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/CustomStyle.tsx
@@ -0,0 +1,179 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy 
of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations 
under
+// the License.
+
+import React, { memo } from 'react';
+import { Handle, Position } from '@xyflow/react';
+import { EdgeProps, BaseEdge, getSmoothStepPath } from '@xyflow/react';
+import { INodeData } from './DataType';
+import { transformEmojiMap } from './EmojiMap';
+
+export function DefaultNode({ data }: { data: INodeData }) {
+  const emoji = data.label
+    ? transformEmojiMap[data.label] || 'πŸ“¦'
+    : data.emoji || 'πŸ“¦';
+  const typeClass = data.type ? `custom-node-${data.type}` : '';
+
+  return (
+    <div className={`custom-node ${typeClass}`}>
+      <div className="custom-node-header">
+        <div className="custom-node-icon">{emoji}</div>
+        <div className="custom-node-title">{data.label}</div>
+      </div>
+
+      <Handle type="target" position={Position.Top} className="custom-handle" 
/>
+      <Handle
+        type="source"
+        position={Position.Bottom}
+        className="custom-handle"
+      />
+    </div>
+  );
+}
+
+// ===== Input Node =====
+export function InputNode({ data }: { data: INodeData }) {
+  return (
+    <div className="custom-node custom-node-input">
+      <div className="custom-node-header">
+        <div className="custom-node-icon">{data.emoji || '🟒'}</div>
+        <div className="custom-node-title">{data.label}</div>
+      </div>
+
+      <Handle
+        type="source"
+        position={Position.Bottom}
+        id="output"
+        className="custom-handle"
+      />
+    </div>
+  );
+}
+
+// ===== Output Node =====
+export function OutputNode({ data }: { data: INodeData }) {
+  return (
+    <div className="custom-node custom-node-output">
+      <div className="custom-node-header">
+        <div className="custom-node-icon">{data.emoji || 'πŸ”΄'}</div>
+        <div className="custom-node-title">{data.label}</div>
+      </div>
+
+      <Handle
+        type="target"
+        position={Position.Top}
+        id="input"
+        className="custom-handle"
+      />
+    </div>
+  );
+}
+
+export default memo(DefaultNode);
+
+export function AnimatedSVGEdge({
+  id,
+  sourceX,
+  sourceY,
+  targetX,
+  targetY,
+  sourcePosition,
+  targetPosition
+}: EdgeProps) {
+  const [initialEdgePath] = getSmoothStepPath({
+    sourceX,
+    sourceY,
+    targetX,
+    targetY,
+    sourcePosition,
+    targetPosition
+  });
+
+  let edgePath = initialEdgePath;
+
+  // If the edge is almost vertical or horizontal, use a straight line
+  const dx = Math.abs(targetX - sourceX);
+  const dy = Math.abs(targetY - sourceY);
+  if (dx < 1) {
+    edgePath = `M${sourceX},${sourceY} L${sourceX + 1},${targetY}`;
+  } else if (dy < 1) {
+    edgePath = `M${sourceX},${sourceY} L${targetX},${sourceY + 1}`;
+  }
+
+  const dotCount = 4;
+  const dotDur = 3.5;
+
+  const dots = Array.from({ length: dotCount }, (_, i) => (
+    <circle key={i} r="5" fill="url(#dotGradient)" opacity="0.8">
+      <animateMotion
+        dur={`${dotDur}s`}
+        repeatCount="indefinite"
+        begin={`${(i * dotDur) / dotCount}s`}
+        path={edgePath}
+      />
+      <animate
+        attributeName="r"
+        values="5;7;5"
+        dur={`${dotDur}s`}
+        repeatCount="indefinite"
+        begin={`${(i * dotDur) / dotCount}s`}
+      />
+    </circle>
+  ));
+
+  return (
+    <>
+      {/* Gradient Base Edge */}
+      <BaseEdge
+        id={id}
+        path={edgePath}
+        style={{
+          stroke: 'url(#gradientEdge)',
+          strokeWidth: 12
+        }}
+      />
+
+      {/* Dots */}
+      {dots}
+
+      {/* Flow shader line */}
+      <path
+        d={edgePath}
+        fill="none"
+        stroke="rgba(255,255,255,0.2)"
+        strokeWidth={5}
+        strokeDasharray="10 10"
+      >
+        <animate
+          attributeName="stroke-dashoffset"
+          from="20"
+          to="0"
+          dur="0.5s"
+          repeatCount="indefinite"
+        />
+      </path>
+
+      {/* Gradient Color */}
+      <defs>
+        <linearGradient id="gradientEdge" gradientTransform="rotate(90)">
+          <stop offset="0%" stopColor="#4facfe" />
+          <stop offset="100%" stopColor="#00f2fe" />
+        </linearGradient>
+
+        <radialGradient id="dotGradient">
+          <stop offset="0%" stopColor="#fff" stopOpacity="1" />
+          <stop offset="50%" stopColor="#4facfe" stopOpacity="0.8" />
+          <stop offset="100%" stopColor="#00f2fe" stopOpacity="0.5" />
+        </radialGradient>
+      </defs>
+    </>
+  );
+}
diff --git 
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/DataType.ts
 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/DataType.ts
new file mode 100644
index 00000000000..0ea535d5fc6
--- /dev/null
+++ 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/DataType.ts
@@ -0,0 +1,37 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy 
of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations 
under
+// the License.
+
+export const nodeWidth = 320;
+export const nodeHeight = 100;
+
+export interface INodeData {
+  id: string;
+  label: string;
+  type?: string;
+  [key: string]: any;
+}
+
+export interface IEdgeData {
+  source: string;
+  target: string;
+  label?: string;
+}
+
+export interface IFlowGraph {
+  nodes: INodeData[];
+  edges: IEdgeData[];
+}
+
+export interface IApiResponse {
+  data: IFlowGraph | null;
+  error: string | null;
+}
diff --git 
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/EditablePanel.tsx
 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/EditablePanel.tsx
new file mode 100644
index 00000000000..d2b19d4371f
--- /dev/null
+++ 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/EditablePanel.tsx
@@ -0,0 +1,408 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy 
of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations 
under
+// the License.
+
+import React from 'react';
+import { Node } from '@xyflow/react';
+import '../../style/yaml/YamlEditor.css';
+import { transformEmojiMap } from './EmojiMap';
+
+type EditableKeyValuePanelProps = {
+  node: Node;
+  onChange: (newData: Record<string, any>) => void;
+  depth?: number;
+};
+
+type EditableKeyValuePanelState = {
+  localData: Record<string, any>;
+  collapsedKeys: Set<string>;
+};
+
+/**
+ * An editable key-value panel component for displaying
+ * and modifying node properties.
+ *
+ * Features:
+ * - Nested object support with collapsible sections
+ * - Real-time key-value editing with validation
+ * - Dynamic field addition and deletion
+ * - Support for multi-line text values
+ * - Object conversion for nested structures
+ * - Reference documentation integration
+ * - Visual hierarchy with depth-based indentation
+ * - Interactive UI with hover effects and transitions
+ *
+ * State Management:
+ * - localData: Local copy of the node data being edited
+ * - collapsedKeys: Set of keys that are currently collapsed
+ *
+ * Props:
+ * @param {Node} node - The node is data to be edited
+ * @param {(data: Record<string, any>) => void} onChange -
+ *  Callback for data changes
+ * @param {number} [depth=0] - Current nesting depth for recursive rendering
+ *
+ * Methods:
+ * - toggleCollapse: Toggles collapse state of nested objects
+ * - handleKeyChange: Updates keys with validation
+ * - handleValueChange: Updates values in the local data
+ * - handleDelete: Removes key-value pairs
+ * - handleAddPair: Adds new key-value pairs
+ * - convertToObject: Converts primitive values to objects
+ * - renderValueEditor: Renders appropriate input based on value type
+ *
+ * UI Features:
+ * - Collapsible nested object sections
+ * - Multi-line text support for complex values
+ * - Add/Delete buttons for field management
+ * - Reference documentation links
+ * - Visual feedback for user interactions
+ * - Responsive design with proper spacing
+ */
+export class EditableKeyValuePanel extends React.Component<
+  EditableKeyValuePanelProps,
+  EditableKeyValuePanelState
+> {
+  static defaultProps = {
+    depth: 0
+  };
+
+  constructor(props: EditableKeyValuePanelProps) {
+    super(props);
+    this.state = {
+      localData: { ...(props.node ? props.node.data : {}) },
+      collapsedKeys: new Set()
+    };
+  }
+
+  componentDidUpdate(prevProps: EditableKeyValuePanelProps) {
+    if (prevProps.node !== this.props.node && this.props.node) {
+      this.setState({ localData: { ...(this.props.node.data ?? {}) } });
+    }
+  }
+
+  toggleCollapse = (key: string) => {
+    this.setState(({ collapsedKeys }) => {
+      const newSet = new Set(collapsedKeys);
+      newSet.has(key) ? newSet.delete(key) : newSet.add(key);
+      return { collapsedKeys: newSet };
+    });
+  };
+
+  handleKeyChange = (oldKey: string, newKey: string) => {
+    newKey = newKey.trim();
+    if (newKey === oldKey || newKey === '') {
+      return alert('Invalid Key!');
+    }
+    if (newKey in this.state.localData) {
+      return alert('Duplicated Key!');
+    }
+
+    const newData: Record<string, any> = {};
+    for (const [k, v] of Object.entries(this.state.localData)) {
+      newData[k === oldKey ? newKey : k] = v;
+    }
+
+    this.setState({ localData: newData }, () => this.props.onChange(newData));
+  };
+
+  handleValueChange = (key: string, newValue: any) => {
+    const newData = { ...this.state.localData, [key]: newValue };
+    this.setState({ localData: newData }, () => this.props.onChange(newData));
+  };
+
+  handleDelete = (key: string) => {
+    const { [key]: _, ...rest } = this.state.localData;
+    void _;
+    this.setState({ localData: rest }, () => this.props.onChange(rest));
+  };
+
+  handleAddPair = () => {
+    let i = 1;
+    const baseKey = 'newKey';
+    while (`${baseKey}${i}` in this.state.localData) {
+      i++;
+    }
+    const newKey = `${baseKey}${i}`;
+    const newData = { ...this.state.localData, [newKey]: '' };
+    this.setState({ localData: newData }, () => this.props.onChange(newData));
+  };
+
+  convertToObject = (key: string) => {
+    if (
+      typeof this.state.localData[key] === 'object' &&
+      this.state.localData[key] !== null
+    ) {
+      return;
+    }
+    const newData = { ...this.state.localData, [key]: {} };
+    this.setState({ localData: newData }, () => this.props.onChange(newData));
+    this.setState(({ collapsedKeys }) => {
+      const newSet = new Set(collapsedKeys);
+      newSet.delete(key);
+      return { collapsedKeys: newSet };
+    });
+  };
+
+  renderValueEditor = (key: string, value: any) => {
+    const isMultiline =
+      key === 'callable' || (typeof value === 'string' && 
value.includes('\n'));
+
+    return isMultiline ? (
+      <textarea
+        value={value}
+        onChange={e => this.handleValueChange(key, e.target.value)}
+        className="editor-input"
+        style={{ minHeight: 100 }}
+      />
+    ) : (
+      <input
+        type="text"
+        value={value}
+        onChange={e => this.handleValueChange(key, e.target.value)}
+        className="editor-input"
+      />
+    );
+  };
+
+  render() {
+    const { localData, collapsedKeys } = this.state;
+    const depth = this.props.depth ?? 0;
+
+    return (
+      <div style={{ fontFamily: 'monospace', fontSize: 14 }}>
+        {Object.entries(localData).map(([key, value]) => {
+          const isObject =
+            typeof value === 'object' &&
+            value !== null &&
+            !Array.isArray(value);
+          const isCollapsed = collapsedKeys.has(key);
+
+          return (
+            <div key={key} style={{ marginBottom: 4 }}>
+              <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
+                {/* Toggle Button or Spacer */}
+                {isObject ? (
+                  <button
+                    onClick={() => this.toggleCollapse(key)}
+                    style={{
+                      width: 24,
+                      height: 32,
+                      cursor: 'pointer',
+                      border: 'none',
+                      background: 'none',
+                      fontWeight: 'bold',
+                      userSelect: 'none',
+                      flexShrink: 0
+                    }}
+                  >
+                    {isCollapsed ? 'β–Ά' : 'β–Ό'}
+                  </button>
+                ) : (
+                  <div style={{ width: 24, height: 32 }} />
+                )}
+
+                {/* Key input */}
+                <input
+                  type="text"
+                  value={key}
+                  onChange={e => this.handleKeyChange(key, e.target.value)}
+                  className="editor-input"
+                  style={{
+                    width: 120,
+                    height: 32,
+                    boxSizing: 'border-box',
+                    padding: '4px 6px',
+                    borderRadius: 4,
+                    border: '1px solid #ccc',
+                    fontFamily: 'inherit',
+                    fontSize: 'inherit'
+                  }}
+                />
+
+                {/* Value input or collapsed preview */}
+                <div style={{ flexGrow: 1 }}>
+                  {isObject ? (
+                    isCollapsed ? (
+                      <span style={{ color: '#888' }}>{'{...}'}</span>
+                    ) : (
+                      <span style={{ color: '#444', fontStyle: 'italic' }}>
+                        {'{...}'}
+                      </span>
+                    )
+                  ) : (
+                    this.renderValueEditor(key, value)
+                  )}
+                </div>
+
+                {/* Action buttons */}
+                <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
+                  {!isObject && (
+                    <button
+                      onClick={() => this.convertToObject(key)}
+                      style={{
+                        width: 70,
+                        height: 32,
+                        padding: '4px 8px',
+                        borderRadius: 4,
+                        border: '1px solid #4caf50',
+                        backgroundColor: '#e8f5e9',
+                        color: '#2e7d32',
+                        cursor: 'pointer'
+                      }}
+                    >
+                      + Sub
+                    </button>
+                  )}
+                  <button
+                    onClick={() => this.handleDelete(key)}
+                    style={{
+                      height: 32,
+                      padding: '4px 8px',
+                      borderRadius: 4,
+                      border: '1px solid #f44336',
+                      backgroundColor: '#ffebee',
+                      color: '#b71c1c',
+                      cursor: 'pointer'
+                    }}
+                  >
+                    Γ—
+                  </button>
+                </div>
+              </div>
+
+              {isObject && !isCollapsed && (
+                <div
+                  style={{
+                    marginLeft: 20,
+                    marginTop: 4,
+                    borderLeft: '1px solid #ccc',
+                    paddingLeft: 8
+                  }}
+                >
+                  <EditableKeyValuePanel
+                    node={{ id: key, data: value } as Node}
+                    onChange={newVal => this.handleValueChange(key, newVal)}
+                    depth={depth + 1}
+                  />
+                </div>
+              )}
+            </div>
+          );
+        })}
+
+        <button
+          onClick={this.handleAddPair}
+          style={{
+            marginTop: 8,
+            padding: '6px 12px',
+            borderRadius: 4,
+            border: '1px solid #2196f3',
+            backgroundColor: '#e3f2fd',
+            color: '#0d47a1',
+            cursor: 'pointer'
+          }}
+        >
+          + Add {depth > 0 ? 'Nested ' : ''}Field
+        </button>
+
+        {/* Reference Doc */}
+        {depth === 0 && (
+          <div
+            style={{
+              marginTop: 14,
+              padding: '14px 20px',
+              borderRadius: 12,
+              background: 'linear-gradient(135deg, #f7f9fc, #e3f0ff)',
+              border: '1px solid #4facfe',
+              boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)',
+              display: 'flex',
+              justifyContent: 'space-between',
+              alignItems: 'center',
+              fontFamily: 'sans-serif',
+              fontSize: 14,
+              color: '#0d47a1',
+              transition: 'transform 0.15s ease, box-shadow 0.15s ease'
+            }}
+            onMouseEnter={e => {
+              e.currentTarget.style.transform = 'translateY(-2px)';
+              e.currentTarget.style.boxShadow =
+                '0 6px 16px rgba(0, 0, 0, 0.12)';
+            }}
+            onMouseLeave={e => {
+              e.currentTarget.style.transform = 'translateY(0)';
+              e.currentTarget.style.boxShadow =
+                '0 4px 12px rgba(0, 0, 0, 0.08)';
+            }}
+          >
+            {/* Emoji + label */}
+            <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
+              <div
+                style={{
+                  fontSize: 22,
+                  width: 28,
+                  height: 28,
+                  display: 'flex',
+                  justifyContent: 'center',
+                  alignItems: 'center'
+                }}
+              >
+                {transformEmojiMap[localData.label || ''] || 'πŸ“„'}
+              </div>
+              <div style={{ display: 'flex', flexDirection: 'column', gap: 2 
}}>
+                <span
+                  style={{ fontWeight: 600, fontSize: 14, color: '#0d47a1' }}
+                >
+                  {localData.label}
+                </span>
+                <span style={{ fontSize: 12, color: '#555' }}>
+                  Reference for Beam YAML transform
+                </span>
+              </div>
+            </div>
+
+            {/* Button */}
+            <a
+              
href={`https://beam.apache.org/releases/yamldoc/current/#${encodeURIComponent(
+                localData.label?.toLowerCase() || ''
+              )}`}
+              target="_blank"
+              rel="noopener noreferrer"
+              style={{
+                padding: '6px 14px',
+                borderRadius: 6,
+                backgroundColor: '#2196f3',
+                color: 'white',
+                fontWeight: 500,
+                fontSize: 13,
+                textDecoration: 'none',
+                boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
+                transition: 'all 0.2s ease'
+              }}
+              onMouseEnter={e => {
+                e.currentTarget.style.backgroundColor = '#1976d2';
+                e.currentTarget.style.transform = 'translateY(-1px)';
+                e.currentTarget.style.boxShadow = '0 4px 8px rgba(0,0,0,0.15)';
+              }}
+              onMouseLeave={e => {
+                e.currentTarget.style.backgroundColor = '#2196f3';
+                e.currentTarget.style.transform = 'translateY(0)';
+                e.currentTarget.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
+              }}
+            >
+              Open Doc
+            </a>
+          </div>
+        )}
+      </div>
+    );
+  }
+}
diff --git 
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/EmojiMap.ts
 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/EmojiMap.ts
new file mode 100644
index 00000000000..ed6a9f2285c
--- /dev/null
+++ 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/EmojiMap.ts
@@ -0,0 +1,75 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy 
of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations 
under
+// the License.
+
+// Emoji mapping for various data processing and I/O operations
+// Generate with AI
+export const transformEmojiMap: Record<string, string> = {
+  // Data processing operations
+  AnomalyDetection: 'πŸ”',
+  AssertEqual: 'βš–οΈ',
+  AssignTimestamps: '⏰',
+  Combine: 'πŸ”„',
+  Create: '✨',
+  Enrichment: 'πŸ’Ž',
+  Explode: 'πŸ’₯',
+  ExtractWindowingInfo: 'πŸͺŸ',
+  Filter: 'πŸ”Ž',
+  Flatten: 'πŸ“‰',
+  Join: '🀝',
+  LogForTesting: 'πŸ“',
+  MLTransform: 'πŸ€–',
+  MapToFields: 'πŸ—ΊοΈ',
+  Partition: 'πŸ“‚',
+  PyTransform: '🐍',
+  RunInference: '🧠',
+  Sql: 'πŸ—ƒοΈ',
+  StripErrorMetadata: '🧹',
+  ValidateWithSchema: 'βœ…',
+  WindowInto: 'πŸͺŸ',
+
+  // I/O operations
+  ReadFromAvro: 'β¬‡οΈπŸ“',
+  WriteToAvro: 'β¬†οΈπŸ“',
+  ReadFromBigQuery: 'β¬‡οΈπŸ“Š',
+  WriteToBigQuery: 'β¬†οΈπŸ“Š',
+  WriteToBigTable: 'β¬†οΈπŸ“‹',
+  ReadFromCsv: 'β¬‡οΈπŸ“',
+  WriteToCsv: 'β¬†οΈπŸ“',
+  ReadFromIceberg: 'β¬‡οΈπŸ§Š',
+  WriteToIceberg: 'β¬†οΈπŸ§Š',
+  ReadFromJdbc: 'β¬‡οΈπŸ”Œ',
+  WriteToJdbc: 'β¬†οΈπŸ”Œ',
+  ReadFromJson: 'β¬‡οΈπŸ“‹',
+  WriteToJson: 'β¬†οΈπŸ“‹',
+  ReadFromKafka: 'β¬‡οΈπŸ“¬',
+  WriteToKafka: 'β¬†οΈπŸ“¬',
+  ReadFromMySql: 'β¬‡οΈπŸ¬',
+  WriteToMySql: 'β¬†οΈπŸ¬',
+  ReadFromOracle: 'β¬‡οΈπŸ›οΈ',
+  WriteToOracle: 'β¬†οΈπŸ›οΈ',
+  ReadFromParquet: 'β¬‡οΈπŸ“¦',
+  WriteToParquet: 'β¬†οΈπŸ“¦',
+  ReadFromPostgres: 'β¬‡οΈπŸ˜',
+  WriteToPostgres: 'β¬†οΈπŸ˜',
+  ReadFromPubSub: 'β¬‡οΈπŸ“’',
+  WriteToPubSub: 'β¬†οΈπŸ“’',
+  ReadFromPubSubLite: 'β¬‡οΈπŸ“£',
+  WriteToPubSubLite: 'β¬†οΈπŸ“£',
+  ReadFromSpanner: 'β¬‡οΈπŸ“',
+  WriteToSpanner: 'β¬†οΈπŸ“',
+  ReadFromSqlServer: 'β¬‡οΈπŸ—„οΈ',
+  WriteToSqlServer: 'β¬†οΈπŸ—„οΈ',
+  ReadFromTFRecord: 'β¬‡οΈπŸ“Ό',
+  WriteToTFRecord: 'β¬†οΈπŸ“Ό',
+  ReadFromText: 'β¬‡οΈπŸ“„',
+  WriteToText: 'β¬†οΈπŸ“„'
+};
diff --git 
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/Yaml.tsx
 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/Yaml.tsx
new file mode 100644
index 00000000000..a004f86bdf5
--- /dev/null
+++ 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/Yaml.tsx
@@ -0,0 +1,322 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy 
of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations 
under
+// the License.
+
+import React from 'react';
+import { ISessionContext } from '@jupyterlab/apputils';
+import { Card } from '@rmwc/card';
+import '@rmwc/textfield/styles';
+import '@rmwc/grid/styles';
+import '@rmwc/card/styles';
+import Split from 'react-split';
+import { debounce } from 'lodash';
+
+import { Node, Edge } from '@xyflow/react';
+import '@xyflow/react/dist/style.css';
+import '../../style/yaml/Yaml.css';
+
+import { YamlEditor } from './YamlEditor';
+import { EditableKeyValuePanel } from './EditablePanel';
+import { FlowEditor } from './YamlFlow';
+import { IApiResponse } from './DataType';
+import { nodeWidth, nodeHeight } from './DataType';
+
+interface IYamlProps {
+  sessionContext: ISessionContext;
+}
+
+interface IYamlState {
+  yamlContent: string;
+  elements: any;
+  selectedNode: Node | null;
+  errors: string[];
+  isDryRunMode: boolean;
+  Nodes: Node[];
+  Edges: Edge[];
+}
+
+const initialNodes: Node[] = [
+  {
+    id: '0',
+    width: nodeWidth,
+    position: { x: 0, y: 0 },
+    type: 'input',
+    data: { label: 'Input' }
+  },
+  {
+    id: '1',
+    width: nodeWidth,
+    position: { x: 0, y: 100 },
+    type: 'default',
+    data: { label: '1' }
+  },
+  {
+    id: '2',
+    width: nodeWidth,
+    position: { x: 0, y: 200 },
+    type: 'default',
+    data: { label: '2' }
+  },
+  {
+    id: '3',
+    width: nodeWidth,
+    position: { x: 0, y: 300 },
+    type: 'output',
+    data: { label: 'Output' }
+  }
+];
+
+const initialEdges: Edge[] = [
+  { id: 'e0-1', source: '0', target: '1' },
+  { id: 'e1-2', source: '1', target: '2' },
+  { id: 'e2-3', source: '2', target: '3' }
+];
+
+/**
+ * A YAML pipeline editor component with integrated flow visualization.
+ *
+ * Features:
+ * - Three-panel layout with YAML editor, flow diagram, and node properties
+ * - Real-time YAML validation and error display
+ * - Automatic flow diagram generation from YAML content
+ * - Interactive node selection and editing
+ * - Dry run mode support for pipeline testing
+ * - Kernel-based YAML parsing using Apache Beam utilities
+ * - Debounced updates to optimize performance
+ * - Split-pane resizable interface
+ *
+ * State Management:
+ * - yamlContent: Current YAML text content
+ * - elements: Combined nodes and edges for the flow diagram
+ * - selectedNode: Currently selected node in the flow diagram
+ * - errors: Array of validation errors
+ * - Nodes: Array of flow nodes
+ * - Edges: Array of flow edges
+ * - isDryRunMode: Flag for dry run mode state
+ *
+ * Props:
+ * @param {IYamlProps} props - Component props including
+ * sessionContext for kernel communication
+ *
+ * Methods:
+ * - handleNodeClick: Handles node selection in the flow diagram
+ * - handleYamlChange: Debounced handler for YAML content changes
+ * - validateAndRenderYaml: Validates YAML and updates the flow diagram
+ */
+export class Yaml extends React.Component<IYamlProps, IYamlState> {
+  constructor(props: IYamlProps) {
+    super(props);
+    this.state = {
+      yamlContent: '',
+      elements: [],
+      selectedNode: null,
+      errors: [],
+      Nodes: initialNodes,
+      Edges: initialEdges,
+      isDryRunMode: false
+    };
+  }
+
+  componentDidMount(): void {
+    this.props.sessionContext.ready.then(() => {
+      const kernel = this.props.sessionContext.session?.kernel;
+      if (!kernel) {
+        console.error('Kernel is not available even after ready');
+        return;
+      }
+
+      console.log('Kernel is ready:', kernel.name);
+    });
+  }
+
+  handleNodeClick = (node: Node) => {
+    this.setState({
+      selectedNode: node
+    });
+  };
+
+  //debounce methods to prevent excessive rendering
+  private handleYamlChange = debounce((value?: string) => {
+    const yamlText = value || '';
+    this.setState({ yamlContent: yamlText });
+    this.validateAndRenderYaml(yamlText);
+  }, 2000);
+
+  validateAndRenderYaml(yamlText: string) {
+    const escapedYaml = yamlText.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
+    const code = `
+from apache_beam_jupyterlab_sidepanel.yaml_parse_utils import parse_beam_yaml
+print(parse_beam_yaml("""${escapedYaml}""",
+  isDryRunMode=${this.state.isDryRunMode ? 'True' : 'False'}))
+`.trim();
+    const session = this.props.sessionContext.session;
+    if (!session?.kernel) {
+      console.error('No kernel available');
+      return;
+    }
+
+    // Clear previous state immediately
+    this.setState({
+      Nodes: [],
+      Edges: [],
+      selectedNode: null,
+      errors: []
+    });
+
+    const future = session.kernel.requestExecute({ code });
+
+    // Handle kernel execution results
+    future.onIOPub = msg => {
+      if (msg.header.msg_type === 'stream') {
+        const content = msg.content as { name: string; text: string };
+        const output = content.text.trim();
+
+        try {
+          const result: IApiResponse = JSON.parse(output);
+
+          if (result.error) {
+            this.setState({
+              elements: [],
+              selectedNode: null,
+              errors: [result.error]
+            });
+          } else {
+            const flowNodes: Node[] = result.data.nodes.map(node => ({
+              id: node.id,
+              type: node.type,
+              width: nodeWidth,
+              height: nodeHeight,
+              position: { x: 0, y: 0 }, // Will be auto-layouted
+              data: {
+                label: node.label,
+                ...node // include all original properties
+              }
+            }));
+
+            // Transform edges for React Flow
+            const flowEdges: Edge[] = result.data.edges.map(edge => ({
+              id: `${edge.source}-${edge.target}`,
+              source: edge.source,
+              target: edge.target,
+              animated: edge.label === 'pipeline_entry',
+              label: edge.label,
+              type: 'default' // or your custom edge type
+            }));
+
+            this.setState({
+              Nodes: flowNodes,
+              Edges: flowEdges,
+              errors: []
+            });
+          }
+        } catch (err) {
+          this.setState({
+            elements: [],
+            selectedNode: null,
+            errors: [output]
+          });
+        }
+      }
+    };
+  }
+
+  render(): React.ReactNode {
+    return (
+      <div
+        style={{
+          height: '100vh',
+          width: '100vw'
+        }}
+      >
+        <Split
+          direction="horizontal"
+          sizes={[25, 35, 20]} // L/C/R
+          minSize={100}
+          gutterSize={6}
+          className="split-pane"
+        >
+          {/* Left */}
+          <div
+            style={{
+              height: '100%',
+              overflow: 'auto',
+              minWidth: '100px'
+            }}
+          >
+            <YamlEditor
+              value={this.state.yamlContent}
+              onChange={value => {
+                // Clear old errors & Handle new errors
+                this.setState({ errors: [] });
+                this.handleYamlChange(value || '');
+              }}
+              errors={this.state.errors}
+              showConsole={true}
+              onDryRunModeChange={newValue =>
+                this.setState({ isDryRunMode: newValue })
+              }
+            />
+          </div>
+
+          <div
+            style={{
+              padding: '1rem',
+              height: '100%'
+            }}
+          >
+            <Card
+              style={{
+                height: '100%',
+                display: 'flex',
+                flexDirection: 'column'
+              }}
+            >
+              <div
+                className="w-full h-full 
+                bg-gray-50 dark:bg-zinc-900 
+                text-sm font-sans"
+                style={{ flex: 1, minHeight: 0 }}
+              >
+                <FlowEditor
+                  Nodes={this.state.Nodes}
+                  Edges={this.state.Edges}
+                  onNodesUpdate={(nodes: Node[]) =>
+                    this.setState({ Nodes: nodes })
+                  }
+                  onEdgesUpdate={(edges: Edge[]) =>
+                    this.setState({ Edges: edges })
+                  }
+                  onNodeClick={this.handleNodeClick}
+                />
+              </div>
+            </Card>
+          </div>
+
+          {/* Right */}
+          <div
+            style={{
+              height: '100%',
+              overflow: 'auto',
+              padding: '12px',
+              boxSizing: 'border-box'
+            }}
+          >
+            <EditableKeyValuePanel
+              node={this.state.selectedNode}
+              // TODO: implement onChange to update node data from Panel
+              onChange={() => {}}
+            />
+          </div>
+        </Split>
+      </div>
+    );
+  }
+}
diff --git 
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/YamlEditor.tsx
 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/YamlEditor.tsx
new file mode 100644
index 00000000000..8bbedc4b547
--- /dev/null
+++ 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/YamlEditor.tsx
@@ -0,0 +1,338 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy 
of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations 
under
+// the License.
+
+import React, { useState, useRef } from 'react';
+import Editor from '@monaco-editor/react';
+interface IYamlEditorProps {
+  value: string;
+  onChange: (value: string | undefined) => void;
+  onDryRunModeChange: (value: boolean) => void;
+  errors?: string[];
+  showConsole?: boolean;
+  consoleHeight?: number;
+}
+
+/**
+ * A YAML editor component built with React and Monaco Editor.
+ *
+ * Features:
+ * - Full YAML syntax highlighting and validation
+ * - Adjustable font size (10px-24px) with zoom controls
+ * - Word wrap toggle
+ * - File download and upload capabilities
+ * - Dry run mode toggle for testing
+ * - Error console with detailed validation messages
+ *
+ * Props:
+ * @param {string} value - Current YAML content
+ * @param {(isDryRun: boolean) => void} onDryRunModeChange -
+ *  Callback when dry run mode changes
+ * @param {(value: string) => void} onChange - Callback when content changes
+ * @param {string[]} [errors=[]] - Array of error messages to display
+ * @param {boolean} [showConsole=true] - Whether to show the error console
+ * @param {number} [consoleHeight=200] - Height of the error console in pixels
+ */
+export const YamlEditor: React.FC<IYamlEditorProps> = ({
+  value,
+  onDryRunModeChange,
+  onChange,
+  errors = [],
+  showConsole = true,
+  consoleHeight = 200
+}) => {
+  const [fontSize, setFontSize] = useState(14);
+  const [wordWrap, setWordWrap] = useState(true);
+  const [isDryRunMode, setDryRunMode] = useState(false);
+  const [isFontMenuOpen, setFontMenuOpen] = useState<boolean>(false);
+  const fileInputRef = useRef<HTMLInputElement>(null);
+
+  const zoomIn = () => setFontSize(prev => Math.min(prev + 1, 24));
+  const zoomOut = () => setFontSize(prev => Math.max(prev - 1, 10));
+  const handleFontSizeChange = (e: React.ChangeEvent<HTMLInputElement>) =>
+    setFontSize(Number(e.target.value));
+
+  // Download YAML File
+  const handleDownload = () => {
+    const blob = new Blob([value], { type: 'text/yaml;charset=utf-8' });
+    const url = URL.createObjectURL(blob);
+    const link = document.createElement('a');
+    link.href = url;
+    link.download = 'pipeline.yaml';
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+    URL.revokeObjectURL(url);
+  };
+
+  // Open YAML File
+  const handleOpenFile = (e: React.ChangeEvent<HTMLInputElement>) => {
+    const file = e.target.files?.[0];
+    if (!file) {
+      return;
+    }
+    const reader = new FileReader();
+    reader.onload = ev => {
+      const content = ev.target?.result;
+      if (typeof content === 'string') {
+        onChange(content);
+      }
+    };
+    reader.readAsText(file);
+    e.target.value = '';
+  };
+
+  return (
+    <div
+      style={{
+        height: '100%',
+        display: 'flex',
+        flexDirection: 'column',
+        background: '#1e1e1e'
+      }}
+    >
+      {/* ================ Toolbar ================ */}
+      <div
+        style={{
+          padding: '8px',
+          display: 'flex',
+          justifyContent: 'space-between',
+          alignItems: 'center',
+          borderBottom: '1px solid #333',
+          gap: 8
+        }}
+      >
+        {/* Fonts */}
+        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+          <button
+            onClick={() => setFontMenuOpen(!isFontMenuOpen)}
+            style={{
+              background: 'transparent',
+              border: '1px solid #555',
+              color: '#ddd',
+              padding: '4px 8px',
+              borderRadius: '4px',
+              cursor: 'pointer'
+            }}
+          >
+            Font {fontSize}px
+          </button>
+          {isFontMenuOpen && (
+            <div
+              style={{
+                position: 'absolute',
+                top: '40px',
+                left: '8px',
+                background: '#252526',
+                padding: '8px',
+                borderRadius: '4px',
+                boxShadow: '0 2px 8px rgba(0,0,0,0.3)',
+                zIndex: 100
+              }}
+            >
+              <div
+                style={{
+                  display: 'flex',
+                  alignItems: 'center',
+                  marginBottom: 8
+                }}
+              >
+                <button
+                  onClick={zoomOut}
+                  style={{
+                    background: '#333',
+                    border: 'none',
+                    color: '#fff',
+                    width: 24,
+                    height: 24,
+                    borderRadius: 4,
+                    cursor: 'pointer'
+                  }}
+                >
+                  -
+                </button>
+                <input
+                  type="range"
+                  min="10"
+                  max="24"
+                  value={fontSize}
+                  onChange={handleFontSizeChange}
+                  style={{ margin: '0 8px', width: 100 }}
+                />
+                <button
+                  onClick={zoomIn}
+                  style={{
+                    background: '#333',
+                    border: 'none',
+                    color: '#fff',
+                    width: 24,
+                    height: 24,
+                    borderRadius: 4,
+                    cursor: 'pointer'
+                  }}
+                >
+                  +
+                </button>
+              </div>
+              <div style={{ color: '#999', fontSize: 12 }}>
+                Current: {fontSize}px
+              </div>
+            </div>
+          )}
+        </div>
+
+        {/* Autowrap & DryRun */}
+        <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
+          <label
+            style={{
+              display: 'flex',
+              alignItems: 'center',
+              color: '#ddd',
+              cursor: 'pointer'
+            }}
+          >
+            <input
+              type="checkbox"
+              checked={wordWrap}
+              onChange={() => setWordWrap(!wordWrap)}
+              style={{ marginRight: 6 }}
+            />{' '}
+            Autowrap
+          </label>
+          <label
+            style={{
+              display: 'flex',
+              alignItems: 'center',
+              color: '#ddd',
+              cursor: 'pointer'
+            }}
+          >
+            <input
+              type="checkbox"
+              checked={isDryRunMode}
+              onChange={() => {
+                onDryRunModeChange(!isDryRunMode);
+                setDryRunMode(!isDryRunMode);
+              }}
+              style={{ marginRight: 6 }}
+            />{' '}
+            Dry Run Mode
+          </label>
+        </div>
+
+        {/* Download / Open */}
+        <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
+          <button
+            onClick={handleDownload}
+            style={{
+              padding: '4px 10px',
+              borderRadius: 4,
+              border: '1px solid #2196f3',
+              background: '#2196f3',
+              color: '#fff',
+              cursor: 'pointer'
+            }}
+          >
+            Download
+          </button>
+          <button
+            onClick={() => fileInputRef.current?.click()}
+            style={{
+              padding: '4px 10px',
+              borderRadius: 4,
+              border: '1px solid #4caf50',
+              background: '#4caf50',
+              color: '#fff',
+              cursor: 'pointer'
+            }}
+          >
+            Open
+          </button>
+          <input
+            ref={fileInputRef}
+            type="file"
+            accept=".yaml,.yml"
+            style={{ display: 'none' }}
+            onChange={handleOpenFile}
+          />
+        </div>
+      </div>
+      {/* ================ ~Toolbar ================ */}
+
+      {/* ================ Code Editor ================ */}
+      <div style={{ flex: 1, minHeight: 0 }}>
+        <Editor
+          height="98%"
+          language="yaml"
+          value={value}
+          theme="vs-dark"
+          onChange={onChange}
+          options={{
+            fontSize,
+            fontFamily: 'monospace',
+            wordWrap: wordWrap ? 'on' : 'off',
+            minimap: { enabled: false },
+            scrollBeyondLastLine: false,
+            automaticLayout: true,
+            lineNumbers: 'on',
+            tabSize: 2
+          }}
+        />
+      </div>
+      {/* ================ ~Code Editor ================ */}
+
+      {/* ================ Console ================ */}
+      {showConsole && (
+        <div
+          style={{
+            height: consoleHeight,
+            background: '#1e1e1e',
+            borderTop: '1px solid #ff6b6b',
+            overflow: 'auto',
+            padding: 12,
+            fontFamily: 'monospace',
+            fontSize: 13,
+            whiteSpace: 'pre'
+          }}
+        >
+          <div
+            style={{
+              color: errors.length ? '#f48771' : '#888',
+              marginBottom: errors.length ? 12 : 0,
+              fontWeight: 500
+            }}
+          >
+            {errors.length ? '❌' : 'βœ… YAML Format Correct'}
+          </div>
+          {errors.map((error, i) => (
+            <pre
+              key={i}
+              style={{
+                color: '#f48771',
+                margin: '8px 0',
+                padding: 0,
+                backgroundColor: 'transparent',
+                border: 'none',
+                overflow: 'visible',
+                whiteSpace: 'pre-wrap',
+                wordBreak: 'break-all',
+                fontFamily: 'inherit'
+              }}
+            >
+              {error}
+            </pre>
+          ))}
+        </div>
+      )}
+      {/* ================ ~Console ================ */}
+    </div>
+  );
+};
diff --git 
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/YamlFlow.tsx
 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/YamlFlow.tsx
new file mode 100644
index 00000000000..0db9dbad14f
--- /dev/null
+++ 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/YamlFlow.tsx
@@ -0,0 +1,227 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy 
of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations 
under
+// the License.
+
+import React, { useEffect, useCallback, useMemo } from 'react';
+import {
+  ReactFlow,
+  useNodesState,
+  useEdgesState,
+  addEdge,
+  MiniMap,
+  Controls,
+  Background,
+  Panel,
+  Node,
+  Edge,
+  applyNodeChanges,
+  NodeChange,
+  Connection
+} from '@xyflow/react';
+import { debounce } from 'lodash';
+import dagre from 'dagre';
+import {
+  DefaultNode,
+  InputNode,
+  OutputNode,
+  AnimatedSVGEdge
+} from './CustomStyle';
+import { nodeWidth, nodeHeight } from './DataType';
+
+import '@xyflow/react/dist/style.css';
+
+interface IFlowEditorProps {
+  Nodes: Node[];
+  Edges: Edge[];
+  onNodesUpdate?: (nodes: Node[]) => void;
+  onEdgesUpdate?: (edges: Edge[]) => void;
+  onNodeClick?: (node: Node) => void;
+  debounceWait?: number;
+}
+
+/**
+ * A flow diagram editor component built with React Flow.
+ *
+ * Features:
+ * - Interactive node-based flow diagram editor
+ * - Auto-layout functionality using Dagre graph layout
+ * - Support for different node types (default, input, output)
+ * - Animated edge connections
+ * - Mini-map and controls for navigation
+ * - Debounced updates to optimize performance
+ * - Real-time node and edge manipulation
+ *
+ * Props:
+ * @param {Node[]} Nodes - Initial array of nodes
+ * @param {Edge[]} Edges - Initial array of edges
+ * @param {(nodes: Node[]) => void} onNodesUpdate -
+ * Callback when nodes are updated
+ * @param {(edges: Edge[]) => void} onEdgesUpdate -
+ * Callback when edges are updated
+ * @param {(event: React.MouseEvent, node: Node) => void} onNodeClick -
+ * Callback when a node is clicked
+ * @param {number} [debounceWait=500] -
+ * Debounce wait time in milliseconds for updates
+ */
+export const FlowEditor: React.FC<IFlowEditorProps> = ({
+  Nodes,
+  Edges,
+  onNodesUpdate,
+  onEdgesUpdate,
+  onNodeClick,
+  debounceWait = 500 //Default debounce wait time
+}: IFlowEditorProps) => {
+  const [nodes, setNodes, _] = useNodesState(Nodes);
+  const [edges, setEdges, onEdgesChange] = useEdgesState(Edges);
+
+  void _;
+  // Debounce callback
+  const debouncedNodesUpdate = useMemo(
+    () =>
+      debounce((nodes: Node[]) => {
+        onNodesUpdate?.(nodes);
+      }, debounceWait),
+    [onNodesUpdate, debounceWait]
+  );
+
+  const debouncedEdgesUpdate = useMemo(
+    () =>
+      debounce((edges: Edge[]) => {
+        onEdgesUpdate?.(edges);
+      }, debounceWait),
+    [onEdgesUpdate, debounceWait]
+  );
+
+  const onConnect = useCallback(
+    (params: Connection) => setEdges(eds => addEdge(params, eds)),
+    [setEdges]
+  );
+
+  // Listen initialNodes/initialEdges changes and update state accordingly
+  useEffect(() => {
+    setNodes(Nodes);
+  }, [Nodes]);
+
+  useEffect(() => {
+    setEdges(Edges);
+  }, [Edges]);
+
+  const handleNodeClick = useCallback(
+    (event: React.MouseEvent, node: Node) => {
+      onNodeClick?.(node);
+    },
+    [onNodeClick]
+  );
+
+  const applyAutoLayout = useCallback(
+    (nodes: Node[], edges: Edge[]): Node[] => {
+      const g = new dagre.graphlib.Graph();
+      g.setDefaultEdgeLabel(() => ({}));
+      g.setGraph({
+        rankdir: 'TB',
+        ranksep: 100
+      }); // TB = Top to Bottom. Use LR for Left to Right
+
+      nodes.forEach(node => {
+        g.setNode(node.id, { width: nodeWidth, height: nodeHeight });
+      });
+
+      edges.forEach(edge => {
+        g.setEdge(edge.source, edge.target);
+      });
+
+      dagre.layout(g);
+
+      return nodes.map(node => {
+        const pos = g.node(node.id);
+        return {
+          ...node,
+          position: {
+            x: pos.x - nodeWidth / 2,
+            y: pos.y - nodeHeight / 2
+          }
+        };
+      });
+    },
+    []
+  );
+
+  const handleNodesChange = useCallback(
+    (changes: NodeChange[]) => {
+      const updatedNodes = applyNodeChanges(changes, nodes);
+
+      // Judge whether the node data is changed or not
+      // except position / dragging / selectedοΌ‰
+      const structuralChanges = changes.filter(
+        c => !['position', 'dragging', 'selected', 'select'].includes(c.type)
+      );
+
+      if (structuralChanges.length > 0) {
+        // Only when there are structural changes, we apply auto-layout
+        const autoLayouted = applyAutoLayout(updatedNodes, edges);
+        setNodes(autoLayouted);
+        onNodesUpdate?.(autoLayouted);
+      } else {
+        // Dragging or selection changes, just update normally
+        setNodes(updatedNodes);
+      }
+    },
+    [nodes, edges, onNodesUpdate, applyAutoLayout]
+  );
+
+  const NodeTypes = {
+    default: DefaultNode,
+    input: InputNode,
+    output: OutputNode
+  };
+
+  // notify parent whenever nodes/edges change
+  useEffect(() => {
+    debouncedNodesUpdate(nodes);
+    return () => debouncedNodesUpdate.cancel();
+  }, [nodes, debouncedNodesUpdate]);
+
+  useEffect(() => {
+    debouncedEdgesUpdate(edges);
+    return () => debouncedEdgesUpdate.cancel();
+  }, [edges, debouncedEdgesUpdate]);
+
+  return (
+    <ReactFlow
+      nodes={nodes}
+      edges={edges}
+      nodeTypes={NodeTypes}
+      edgeTypes={{ default: AnimatedSVGEdge }}
+      onNodesChange={handleNodesChange}
+      onEdgesChange={onEdgesChange}
+      onConnect={onConnect}
+      onNodeClick={handleNodeClick}
+      fitView
+    >
+      <Panel position="top-right">
+        <button
+          onClick={() => {
+            const newNodes = applyAutoLayout(nodes, edges);
+            setNodes(newNodes);
+          }}
+          className="px-3 py-1 
+          bg-blue-600 text-white rounded-md shadow 
+          hover:bg-blue-700 transition-all duration-200"
+        >
+          Auto Layout
+        </button>
+      </Panel>
+      <MiniMap />
+      <Controls />
+      <Background />
+    </ReactFlow>
+  );
+};
diff --git 
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/YamlWidget.tsx
 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/YamlWidget.tsx
new file mode 100644
index 00000000000..191e4b38016
--- /dev/null
+++ 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/yaml/YamlWidget.tsx
@@ -0,0 +1,34 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy 
of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations 
under
+// the License.
+
+import * as React from 'react';
+
+import { ISessionContext, ReactWidget } from '@jupyterlab/apputils';
+
+import { Yaml } from './Yaml';
+
+/**
+ * Converts the React component Clusters into a lumino widget used
+ * in Jupyter labextensions.
+ */
+export class YamlWidget extends ReactWidget {
+  constructor(sessionContext: ISessionContext) {
+    super();
+    this._sessionContext = sessionContext;
+  }
+
+  protected render(): React.ReactElement<any> {
+    return <Yaml sessionContext={this._sessionContext} />;
+  }
+
+  private _sessionContext: ISessionContext;
+}
diff --git 
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/index.css
 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/index.css
index 1b2227845b6..1a158f9bfe4 100644
--- 
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/index.css
+++ 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/index.css
@@ -18,3 +18,6 @@
 @import './inspector/Inspectables.css';
 @import './inspector/InspectableView.css';
 @import './inspector/InteractiveInspector.css';
+@import './yaml/Yaml.css';
+@import './yaml/YamlEditor.css';
+@import './yaml/YamlFlow.css';
\ No newline at end of file
diff --git 
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/mdc-theme.css
 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/mdc-theme.css
index b6383f96512..be39963782a 100644
--- 
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/mdc-theme.css
+++ 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/mdc-theme.css
@@ -49,11 +49,11 @@
   color: var(--jp-ui-font-color1);
 }
 
-.mdc-form-field > label {
+.mdc-form-field>label {
   margin-bottom: 0;
 }
 
 .mdc-text-field {
   margin-left: 4px;
   margin-right: 4px;
-}
+}
\ No newline at end of file
diff --git 
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/index.css
 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/yaml/Yaml.css
similarity index 58%
copy from 
sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/index.css
copy to 
sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/yaml/Yaml.css
index 1b2227845b6..3947022fc7f 100644
--- 
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/index.css
+++ 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/yaml/Yaml.css
@@ -11,10 +11,30 @@
  * License for the specific language governing permissions and limitations 
under
  * the License.
  */
-@import '~material-design-icons/iconfont/material-icons.css';
 
-@import './mdc-theme.css';
+.split-pane {
+    height: 90%;
+    display: flex;
+}
 
-@import './inspector/Inspectables.css';
-@import './inspector/InspectableView.css';
-@import './inspector/InteractiveInspector.css';
+.gutter {
+    background-color: #ccc;
+    background-clip: padding-box;
+    box-sizing: border-box;
+    z-index: 1;
+    transition: background-color 0.2s;
+}
+
+.gutter:hover {
+    background-color: #aaa;
+}
+
+.gutter.gutter-horizontal {
+    cursor: col-resize;
+    width: 6px;
+}
+
+.gutter.gutter-vertical {
+    cursor: row-resize;
+    height: 6px;
+}
\ No newline at end of file
diff --git 
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/index.css
 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/yaml/YamlEditor.css
similarity index 61%
copy from 
sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/index.css
copy to 
sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/yaml/YamlEditor.css
index 1b2227845b6..b1b45a7b641 100644
--- 
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/index.css
+++ 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/yaml/YamlEditor.css
@@ -11,10 +11,26 @@
  * License for the specific language governing permissions and limitations 
under
  * the License.
  */
-@import '~material-design-icons/iconfont/material-icons.css';
 
-@import './mdc-theme.css';
+input {
+    border: 1px solid #ccc;
+    padding: 4px 6px;
+    border-radius: 4px;
+}
 
-@import './inspector/Inspectables.css';
-@import './inspector/InspectableView.css';
-@import './inspector/InteractiveInspector.css';
+button {
+    cursor: pointer;
+    background: none;
+    border: none;
+    font-size: 1.1rem;
+}
+
+.editor-input {
+    width: 100%;
+    height: 20px;
+    padding: 4px 6px;
+    border-radius: 4px;
+    border: 1px solid #ccc;
+    font-family: inherit;
+    font-size: inherit;
+}
\ No newline at end of file
diff --git 
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/yaml/YamlFlow.css
 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/yaml/YamlFlow.css
new file mode 100644
index 00000000000..091e3e211d7
--- /dev/null
+++ 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/style/yaml/YamlFlow.css
@@ -0,0 +1,168 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+ * use this file except in compliance with the License. You may obtain a copy 
of
+ * the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations 
under
+ * the License.
+ */
+
+.custom-node {
+    background: linear-gradient(145deg, #ffffff, #e5e5e5);
+    border: 2px solid #ccc;
+    border-radius: 12px;
+    padding: 12px 16px;
+    width: 250px;
+    display: inline-block;
+    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.12);
+    transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s 
ease;
+    cursor: pointer;
+    user-select: none;
+    word-wrap: break-word;
+    overflow-wrap: break-word;
+}
+
+.custom-node:hover {
+    transform: translateY(-3px) scale(1.02);
+    box-shadow: 0 6px 14px rgba(0, 0, 0, 0.18);
+    border-color: #4cafef;
+}
+
+.custom-node:active {
+    transform: scale(0.96);
+    box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2);
+    border-color: #2196f3;
+}
+
+/* Header */
+.custom-node-header {
+    display: flex;
+    align-items: center;
+}
+
+.custom-node-icon {
+    width: 44px;
+    height: 44px;
+    border-radius: 50%;
+    flex-shrink: 0;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    font-size: 22px;
+    margin-right: 10px;
+    box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.1);
+}
+
+.custom-node-title {
+    font-size: 16px;
+    font-weight: bold;
+    color: #333;
+    letter-spacing: 0.3px;
+    line-height: 1.3;
+    flex-grow: 1;
+    white-space: normal;
+}
+
+/* Handle */
+.custom-handle {
+    width: 40px !important;
+    height: 8px !important;
+    border-radius: 4px;
+    background: #00bcd4 !important;
+    border: none !important;
+    transition: background 0.2s ease;
+}
+
+.custom-handle:hover {
+    background: #008ba3 !important;
+}
+
+/* ====== Colors ====== */
+.custom-node-input {
+    border-color: #4caf50;
+}
+
+.custom-node-input .custom-node-icon {
+    background: linear-gradient(135deg, #e8f5e9, #a5d6a7);
+}
+
+.custom-node-default {
+    border-color: #2196f3;
+}
+
+.custom-node-default .custom-node-icon {
+    background: linear-gradient(135deg, #e3f2fd, #90caf9);
+}
+
+.custom-node-output {
+    border-color: #ff9800;
+}
+
+.custom-node-output .custom-node-icon {
+    background: linear-gradient(135deg, #fff3e0, #ffcc80);
+}
+
+/* ====== Input ====== */
+.custom-node-input {
+    border-color: #4caf50;
+    background: linear-gradient(145deg, #f1fbf3, #dcedc8);
+}
+
+.custom-node-input .custom-node-icon {
+    background: linear-gradient(135deg, #c8e6c9, #81c784);
+    color: #ffffff;
+    box-shadow: 0 0 8px rgba(76, 175, 80, 0.5) inset;
+    transition: box-shadow 0.3s ease, transform 0.2s ease;
+}
+
+.custom-node-input:hover .custom-node-icon {
+    box-shadow: 0 0 12px rgba(76, 175, 80, 0.7) inset;
+    transform: scale(1.05);
+}
+
+.custom-node-input:active .custom-node-icon {
+    transform: scale(0.95);
+}
+
+.custom-node-input .custom-handle {
+    background: #43a047 !important;
+}
+
+.custom-node-input .custom-handle:hover {
+    background: #2e7d32 !important;
+}
+
+/* ====== Output ====== */
+.custom-node-output {
+    border-color: #ff9800;
+    background: linear-gradient(145deg, #fff8f0, #ffe0b2);
+}
+
+.custom-node-output .custom-node-icon {
+    background: linear-gradient(135deg, #ffcc80, #ffb74d);
+    color: #ffffff;
+    box-shadow: 0 0 8px rgba(255, 152, 0, 0.5) inset;
+    transition: box-shadow 0.3s ease, transform 0.2s ease;
+}
+
+.custom-node-output:hover .custom-node-icon {
+    box-shadow: 0 0 12px rgba(255, 152, 0, 0.7) inset;
+    transform: scale(1.05);
+}
+
+.custom-node-output:active .custom-node-icon {
+    transform: scale(0.95);
+}
+
+.custom-node-output .custom-handle {
+    background: #fb8c00 !important;
+}
+
+.custom-node-output .custom-handle:hover {
+    background: #ef6c00 !important;
+}
\ No newline at end of file
diff --git 
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/yarn.lock
 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/yarn.lock
index 3a1e6924b85..58bedbf7192 100644
--- 
a/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/yarn.lock
+++ 
b/sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/yarn.lock
@@ -1400,6 +1400,27 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@material/button@npm:^14.0.0":
+  version: 14.0.0
+  resolution: "@material/button@npm:14.0.0"
+  dependencies:
+    "@material/density": ^14.0.0
+    "@material/dom": ^14.0.0
+    "@material/elevation": ^14.0.0
+    "@material/feature-targeting": ^14.0.0
+    "@material/focus-ring": ^14.0.0
+    "@material/ripple": ^14.0.0
+    "@material/rtl": ^14.0.0
+    "@material/shape": ^14.0.0
+    "@material/theme": ^14.0.0
+    "@material/tokens": ^14.0.0
+    "@material/touch-target": ^14.0.0
+    "@material/typography": ^14.0.0
+    tslib: ^2.1.0
+  checksum: 
83b55dd1038b3fa98e685d31f4a20c3530bd152f4c2f2319ae553c9496955f3ab9a86bfd50f523018a28bbeaf49410cd689c2b25339eb8b718b1677f6fe7103b
+  languageName: node
+  linkType: hard
+
 "@material/button@npm:^8.0.0":
   version: 8.0.0
   resolution: "@material/button@npm:8.0.0"
@@ -1417,6 +1438,22 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@material/card@npm:^14.0.0":
+  version: 14.0.0
+  resolution: "@material/card@npm:14.0.0"
+  dependencies:
+    "@material/dom": ^14.0.0
+    "@material/elevation": ^14.0.0
+    "@material/feature-targeting": ^14.0.0
+    "@material/ripple": ^14.0.0
+    "@material/rtl": ^14.0.0
+    "@material/shape": ^14.0.0
+    "@material/theme": ^14.0.0
+    tslib: ^2.1.0
+  checksum: 
e058ff89fadd4ef4cc28d7d8c9f6ac8152ba94430707a272d5cd2ebe4ebd4e481d00b42f97addf25cd49439b892d5979f0133bfb7bb090a322f99036cdb4d87f
+  languageName: node
+  linkType: hard
+
 "@material/checkbox@npm:^8.0.0":
   version: 8.0.0
   resolution: "@material/checkbox@npm:8.0.0"
@@ -1460,6 +1497,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@material/density@npm:^14.0.0":
+  version: 14.0.0
+  resolution: "@material/density@npm:14.0.0"
+  dependencies:
+    tslib: ^2.1.0
+  checksum: 
94279ca2d7b75bbb998026da5fb7fbd68c830f986d25c4dcd5988a9f180bb600d8c1135c7828f844f1d74fa8919d817f44d10be03a18a4ccf81afe0f0d3810a9
+  languageName: node
+  linkType: hard
+
 "@material/density@npm:^8.0.0":
   version: 8.0.0
   resolution: "@material/density@npm:8.0.0"
@@ -1528,6 +1574,20 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@material/elevation@npm:^14.0.0":
+  version: 14.0.0
+  resolution: "@material/elevation@npm:14.0.0"
+  dependencies:
+    "@material/animation": ^14.0.0
+    "@material/base": ^14.0.0
+    "@material/feature-targeting": ^14.0.0
+    "@material/rtl": ^14.0.0
+    "@material/theme": ^14.0.0
+    tslib: ^2.1.0
+  checksum: 
1cdc33f86b47d40dbc3f425f36dae20ebdfc5b8ce1b0307d4bbd30bc9680d17aff1af13399c733aeba7425cd97ff12197e14d21fa9e7fa6e3582f6f63ed0cb1a
+  languageName: node
+  linkType: hard
+
 "@material/elevation@npm:^8.0.0":
   version: 8.0.0
   resolution: "@material/elevation@npm:8.0.0"
@@ -1590,6 +1650,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@material/focus-ring@npm:^14.0.0":
+  version: 14.0.0
+  resolution: "@material/focus-ring@npm:14.0.0"
+  dependencies:
+    "@material/dom": ^14.0.0
+    "@material/feature-targeting": ^14.0.0
+    "@material/rtl": ^14.0.0
+  checksum: 
61cbd9d2c449b7e743198c7dcae3181ee7488fdcefcd7666aff016f03b9a128b918f74cdf97862c189f6629e4cc654ea1325ca51f56751a20924fc7cb86914cb
+  languageName: node
+  linkType: hard
+
 "@material/form-field@npm:^8.0.0":
   version: 8.0.0
   resolution: "@material/form-field@npm:8.0.0"
@@ -1605,6 +1676,25 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@material/icon-button@npm:^14.0.0":
+  version: 14.0.0
+  resolution: "@material/icon-button@npm:14.0.0"
+  dependencies:
+    "@material/base": ^14.0.0
+    "@material/density": ^14.0.0
+    "@material/dom": ^14.0.0
+    "@material/elevation": ^14.0.0
+    "@material/feature-targeting": ^14.0.0
+    "@material/focus-ring": ^14.0.0
+    "@material/ripple": ^14.0.0
+    "@material/rtl": ^14.0.0
+    "@material/theme": ^14.0.0
+    "@material/touch-target": ^14.0.0
+    tslib: ^2.1.0
+  checksum: 
99a7b5fd1882e45eb904fd6076975c6c6faeb50cabd4d17771232cd4b4f8696d1593accb102791265f602943a75eba99d8a89299afe0c0bc56b9abacd7dd4fb7
+  languageName: node
+  linkType: hard
+
 "@material/icon-button@npm:^8.0.0":
   version: 8.0.0
   resolution: "@material/icon-button@npm:8.0.0"
@@ -1620,6 +1710,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@material/layout-grid@npm:^14.0.0":
+  version: 14.0.0
+  resolution: "@material/layout-grid@npm:14.0.0"
+  dependencies:
+    tslib: ^2.1.0
+  checksum: 
96a0748754fb034ab5a87cc7ed3642c9cb9b7812db034811599e12053ac39f06406f45d1f676bf3dbad597217b171313c0a314d20cad2c34fc93f08affc7307f
+  languageName: node
+  linkType: hard
+
 "@material/line-ripple@npm:^8.0.0":
   version: 8.0.0
   resolution: "@material/line-ripple@npm:8.0.0"
@@ -1796,6 +1895,18 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@material/shape@npm:^14.0.0":
+  version: 14.0.0
+  resolution: "@material/shape@npm:14.0.0"
+  dependencies:
+    "@material/feature-targeting": ^14.0.0
+    "@material/rtl": ^14.0.0
+    "@material/theme": ^14.0.0
+    tslib: ^2.1.0
+  checksum: 
bcab9e26a10ff26880c99f86b31485f0b416330cd83e81bceeb9c4bc13fddc3a108eb3bbb011998da6ae1bf1f42707bb6095a804915728b10c746724e0b21607
+  languageName: node
+  linkType: hard
+
 "@material/shape@npm:^8.0.0":
   version: 8.0.0
   resolution: "@material/shape@npm:8.0.0"
@@ -1866,6 +1977,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@material/tokens@npm:^14.0.0":
+  version: 14.0.0
+  resolution: "@material/tokens@npm:14.0.0"
+  dependencies:
+    "@material/elevation": ^14.0.0
+  checksum: 
666320dd0bde170e337ab8172153fa975381144cf46765f1fa898bfe7dad216a7437ee0b7f9c32ab4d8317a4096064596fe89fe5d9a90f047b14081392beb9d6
+  languageName: node
+  linkType: hard
+
 "@material/top-app-bar@npm:^8.0.0":
   version: 8.0.0
   resolution: "@material/top-app-bar@npm:8.0.0"
@@ -1883,6 +2003,18 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@material/touch-target@npm:^14.0.0":
+  version: 14.0.0
+  resolution: "@material/touch-target@npm:14.0.0"
+  dependencies:
+    "@material/base": ^14.0.0
+    "@material/feature-targeting": ^14.0.0
+    "@material/rtl": ^14.0.0
+    tslib: ^2.1.0
+  checksum: 
2ad21fccf992d15b049ca5cf5ee954685cdcd64157f3bd36b44488af1f5363075c24a962274532f87377f3b5e0cc3ca929797bb213126467c1db415f7896bf86
+  languageName: node
+  linkType: hard
+
 "@material/touch-target@npm:^8.0.0":
   version: 8.0.0
   resolution: "@material/touch-target@npm:8.0.0"
@@ -1893,6 +2025,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@material/typography@npm:^14.0.0":
+  version: 14.0.0
+  resolution: "@material/typography@npm:14.0.0"
+  dependencies:
+    "@material/feature-targeting": ^14.0.0
+    "@material/theme": ^14.0.0
+    tslib: ^2.1.0
+  checksum: 
24e52daaf1f94a32689b585e8e9cb9f09894cb0d9b3cbe4f8c19112fa59a1586b8b250f7dfaa7aa27b00158b0cd1184c89bcace40906557fbe59a94c65a45417
+  languageName: node
+  linkType: hard
+
 "@material/typography@npm:^8.0.0":
   version: 8.0.0
   resolution: "@material/typography@npm:8.0.0"
@@ -1938,6 +2081,28 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@monaco-editor/loader@npm:^1.5.0":
+  version: 1.5.0
+  resolution: "@monaco-editor/loader@npm:1.5.0"
+  dependencies:
+    state-local: ^1.0.6
+  checksum: 
45e5f56ea9b1e5c16e3d40b05f8c365af830627d2aa8215c86cfac57384419c1b896927408c1261a12dc182a08419d4f20a0d0949d3e76ca42ccc68f4ffec508
+  languageName: node
+  linkType: hard
+
+"@monaco-editor/react@npm:^4.7.0":
+  version: 4.7.0
+  resolution: "@monaco-editor/react@npm:4.7.0"
+  dependencies:
+    "@monaco-editor/loader": ^1.5.0
+  peerDependencies:
+    monaco-editor: ">= 0.25.0 < 1"
+    react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+    react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+  checksum: 
8b3bd8adfcd6af70dc5f965e986932269e1e2c2a0f6beb5a3c632c8c7942c1341f6086d9664f9a949983bdf4a04a706e529a93bfec3b5884642915dfcc0354c3
+  languageName: node
+  linkType: hard
+
 "@nodelib/fs.scandir@npm:2.1.5":
   version: 2.1.5
   resolution: "@nodelib/fs.scandir@npm:2.1.5"
@@ -2067,6 +2232,23 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rmwc/button@npm:14.3.5":
+  version: 14.3.5
+  resolution: "@rmwc/button@npm:14.3.5"
+  dependencies:
+    "@material/button": ^14.0.0
+    "@rmwc/base": 14.3.5
+    "@rmwc/icon": 14.3.5
+    "@rmwc/provider": 14.3.5
+    "@rmwc/ripple": 14.3.5
+    "@rmwc/types": 14.3.5
+  peerDependencies:
+    react: ">=16.8.x"
+    react-dom: ">=16.8.x"
+  checksum: 
0bbd2e78e3c89ca660dfd2b114e987d5afa8ac242999ad3e0d5a19943efa6f02a728f9922285a8e6bc13ce640c706bdf182bf00a57f79e2bd395831c56546209
+  languageName: node
+  linkType: hard
+
 "@rmwc/button@npm:^8.0.6, @rmwc/button@npm:^8.0.8":
   version: 8.0.8
   resolution: "@rmwc/button@npm:8.0.8"
@@ -2084,6 +2266,23 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rmwc/card@npm:^14.3.5":
+  version: 14.3.5
+  resolution: "@rmwc/card@npm:14.3.5"
+  dependencies:
+    "@material/card": ^14.0.0
+    "@rmwc/base": 14.3.5
+    "@rmwc/button": 14.3.5
+    "@rmwc/icon-button": 14.3.5
+    "@rmwc/ripple": 14.3.5
+    "@rmwc/types": 14.3.5
+  peerDependencies:
+    react: ">=16.8.x"
+    react-dom: ">=16.8.x"
+  checksum: 
2177e30d444a367045deba77f4dc1e287950ea6bc407ec69964ba362baad1069d1138dd0ad3ef996ae982125449b52ee865746895ab3ae2b1cc536c383928497
+  languageName: node
+  linkType: hard
+
 "@rmwc/checkbox@npm:^8.0.8":
   version: 8.0.8
   resolution: "@rmwc/checkbox@npm:8.0.8"
@@ -2195,6 +2394,36 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rmwc/grid@npm:^14.3.5":
+  version: 14.3.5
+  resolution: "@rmwc/grid@npm:14.3.5"
+  dependencies:
+    "@material/layout-grid": ^14.0.0
+    "@rmwc/base": 14.3.5
+    "@rmwc/types": 14.3.5
+  peerDependencies:
+    react: ">=16.8.x"
+    react-dom: ">=16.8.x"
+  checksum: 
6466a9bb4d8b533ae09df4b320f7bc6be7c6e9274a11a47a08174b9e7cdbe6e9669b513d292b55391fe387609b9839a5bae36d9659a660f9be989461c677e65a
+  languageName: node
+  linkType: hard
+
+"@rmwc/icon-button@npm:14.3.5":
+  version: 14.3.5
+  resolution: "@rmwc/icon-button@npm:14.3.5"
+  dependencies:
+    "@material/icon-button": ^14.0.0
+    "@rmwc/base": 14.3.5
+    "@rmwc/icon": 14.3.5
+    "@rmwc/ripple": 14.3.5
+    "@rmwc/types": 14.3.5
+  peerDependencies:
+    react: ">=16.8.x"
+    react-dom: ">=16.8.x"
+  checksum: 
ae8e32bf5fb9fb6ba2abec6b34deea6a3d09868766e5ff092d0ba44fa199492c2353f1e8a6a715c4a8a9c3abfa76b7854e2003d41767a5822cd00ccf6492bc01
+  languageName: node
+  linkType: hard
+
 "@rmwc/icon-button@npm:^8.0.8":
   version: 8.0.8
   resolution: "@rmwc/icon-button@npm:8.0.8"
@@ -2211,6 +2440,20 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rmwc/icon@npm:14.3.5":
+  version: 14.3.5
+  resolution: "@rmwc/icon@npm:14.3.5"
+  dependencies:
+    "@rmwc/base": 14.3.5
+    "@rmwc/provider": 14.3.5
+    "@rmwc/types": 14.3.5
+  peerDependencies:
+    react: ">=16.8.x"
+    react-dom: ">=16.8.x"
+  checksum: 
335afea39b5421863bd4bc460005c4d0f3d83a5febc4bfac256dcef5e2c255e5594853bec876adc81e94c3693ad5396cd63d1e14af86e11ba1d2c4c3e7d65fc8
+  languageName: node
+  linkType: hard
+
 "@rmwc/icon@npm:^8.0.8":
   version: 8.0.8
   resolution: "@rmwc/icon@npm:8.0.8"
@@ -2312,7 +2555,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@rmwc/ripple@npm:^14.0.0":
+"@rmwc/ripple@npm:14.3.5, @rmwc/ripple@npm:^14.0.0":
   version: 14.3.5
   resolution: "@rmwc/ripple@npm:14.3.5"
   dependencies:
@@ -2443,6 +2686,19 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rmwc/touch-target@npm:^14.3.5":
+  version: 14.3.5
+  resolution: "@rmwc/touch-target@npm:14.3.5"
+  dependencies:
+    "@material/touch-target": ^14.0.0
+    "@rmwc/base": 14.3.5
+  peerDependencies:
+    react: ">=16.8.x"
+    react-dom: ">=16.8.x"
+  checksum: 
c9f82f3c685240be0df5346aeed0abdb7bdd68e2640e6bee930579a359309424965d38c55324cac94235a890ac73a408450dd129a9eb369727e96a5710679003
+  languageName: node
+  linkType: hard
+
 "@rmwc/types@npm:14.3.5":
   version: 14.3.5
   resolution: "@rmwc/types@npm:14.3.5"
@@ -2591,6 +2847,64 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@types/d3-color@npm:*":
+  version: 3.1.3
+  resolution: "@types/d3-color@npm:3.1.3"
+  checksum: 
8a0e79a709929502ec4effcee2c786465b9aec51b653ba0b5d05dbfec3e84f418270dd603002d94021885061ff592f614979193bd7a02ad76317f5608560e357
+  languageName: node
+  linkType: hard
+
+"@types/d3-drag@npm:^3.0.7":
+  version: 3.0.7
+  resolution: "@types/d3-drag@npm:3.0.7"
+  dependencies:
+    "@types/d3-selection": "*"
+  checksum: 
1107cb1667ead79073741c06ea4a9e8e4551698f6c9c60821e327a6aa30ca2ba0b31a6fe767af85a2e38a22d2305f6c45b714df15c2bba68adf58978223a5fc5
+  languageName: node
+  linkType: hard
+
+"@types/d3-interpolate@npm:*, @types/d3-interpolate@npm:^3.0.4":
+  version: 3.0.4
+  resolution: "@types/d3-interpolate@npm:3.0.4"
+  dependencies:
+    "@types/d3-color": "*"
+  checksum: 
efd2770e174e84fc7316fdafe03cf3688451f767dde1fa6211610137f495be7f3923db7e1723a6961a0e0e9ae0ed969f4f47c038189fa0beb1d556b447922622
+  languageName: node
+  linkType: hard
+
+"@types/d3-selection@npm:*, @types/d3-selection@npm:^3.0.10":
+  version: 3.0.11
+  resolution: "@types/d3-selection@npm:3.0.11"
+  checksum: 
4b76630f76dffdafc73cdc786d73e7b4c96f40546483074b3da0e7fe83fd7f5ed9bc6c50f79bcef83595f943dcc9ed6986953350f39371047af644cc39c41b43
+  languageName: node
+  linkType: hard
+
+"@types/d3-transition@npm:^3.0.8":
+  version: 3.0.9
+  resolution: "@types/d3-transition@npm:3.0.9"
+  dependencies:
+    "@types/d3-selection": "*"
+  checksum: 
c8608b1ac7cf09acfe387f3d41074631adcdfd7f2c8ca2efb378309adf0e9fc8469dbcf0d7a8c40fd1f03f2d2bf05fcda0cde7aa356ae8533a141dcab4dff221
+  languageName: node
+  linkType: hard
+
+"@types/d3-zoom@npm:^3.0.8":
+  version: 3.0.8
+  resolution: "@types/d3-zoom@npm:3.0.8"
+  dependencies:
+    "@types/d3-interpolate": "*"
+    "@types/d3-selection": "*"
+  checksum: 
a1685728949ed39faf8ce162cc13338639c57bc2fd4d55fc7902b2632cad2bc2a808941263e57ce6685647e8a6a0a556e173386a52d6bb74c9ed6195b68be3de
+  languageName: node
+  linkType: hard
+
+"@types/dagre@npm:^0.7.53":
+  version: 0.7.53
+  resolution: "@types/dagre@npm:0.7.53"
+  checksum: 
e5b8e52cabd62849479aa11842112ea10e2b0911a8e0c23f7832e639d08dbd0a30d83a4e21223dfec20493e4ba5406bcdfc4c51084bc9c1b14edf16d6d215f24
+  languageName: node
+  linkType: hard
+
 "@types/eslint-scope@npm:^3.7.7":
   version: 3.7.7
   resolution: "@types/eslint-scope@npm:3.7.7"
@@ -2680,6 +2994,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@types/lodash@npm:^4.17.20":
+  version: 4.17.20
+  resolution: "@types/lodash@npm:4.17.20"
+  checksum: 
dc7bb4653514dd91117a4c4cec2c37e2b5a163d7643445e4757d76a360fabe064422ec7a42dde7450c5e7e0e7e678d5e6eae6d2a919abcddf581d81e63e63839
+  languageName: node
+  linkType: hard
+
 "@types/node@npm:*":
   version: 24.0.3
   resolution: "@types/node@npm:24.0.3"
@@ -3086,6 +3407,37 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@xyflow/react@npm:^12.8.2":
+  version: 12.8.2
+  resolution: "@xyflow/react@npm:12.8.2"
+  dependencies:
+    "@xyflow/system": 0.0.66
+    classcat: ^5.0.3
+    zustand: ^4.4.0
+  peerDependencies:
+    react: ">=17"
+    react-dom: ">=17"
+  checksum: 
53af765d263a01541f8815bef5c17cb3d62238446ef8e956413d2b73268cfe45b9815eeb3899129a00f5295dfbcdcba82029b9b9566268cec59d775a7757e676
+  languageName: node
+  linkType: hard
+
+"@xyflow/system@npm:0.0.66":
+  version: 0.0.66
+  resolution: "@xyflow/system@npm:0.0.66"
+  dependencies:
+    "@types/d3-drag": ^3.0.7
+    "@types/d3-interpolate": ^3.0.4
+    "@types/d3-selection": ^3.0.10
+    "@types/d3-transition": ^3.0.8
+    "@types/d3-zoom": ^3.0.8
+    d3-drag: ^3.0.0
+    d3-interpolate: ^3.0.1
+    d3-selection: ^3.0.0
+    d3-zoom: ^3.0.0
+  checksum: 
224e94f43495980cfcf437ad9632474926cd06cf7b6c57a315605cc5e2449cb9cff2b2b43d602a163d43ead9d7458f3f83631cd020f89ce53eccfde62b55c3a5
+  languageName: node
+  linkType: hard
+
 "abab@npm:^2.0.3, abab@npm:^2.0.6":
   version: 2.0.6
   resolution: "abab@npm:2.0.6"
@@ -3294,25 +3646,33 @@ __metadata:
     "@jupyterlab/launcher": ^4.3.6
     "@jupyterlab/mainmenu": ^4.3.6
     "@lumino/widgets": ^2.2.1
+    "@monaco-editor/react": ^4.7.0
     "@rmwc/base": ^14.0.0
     "@rmwc/button": ^8.0.6
+    "@rmwc/card": ^14.3.5
     "@rmwc/data-table": ^8.0.6
     "@rmwc/dialog": ^8.0.6
     "@rmwc/drawer": ^8.0.6
     "@rmwc/fab": ^8.0.6
+    "@rmwc/grid": ^14.3.5
     "@rmwc/list": ^8.0.6
     "@rmwc/ripple": ^14.0.0
     "@rmwc/textfield": ^8.0.6
     "@rmwc/tooltip": ^8.0.6
     "@rmwc/top-app-bar": ^8.0.6
+    "@rmwc/touch-target": ^14.3.5
     "@testing-library/dom": ^9.3.0
     "@testing-library/jest-dom": ^6.1.4
     "@testing-library/react": ^14.0.0
+    "@types/dagre": ^0.7.53
     "@types/jest": ^29.5.14
+    "@types/lodash": ^4.17.20
     "@types/react": ^18.2.0
     "@types/react-dom": ^18.2.0
     "@typescript-eslint/eslint-plugin": ^7.3.1
     "@typescript-eslint/parser": ^7.3.1
+    "@xyflow/react": ^12.8.2
+    dagre: ^0.8.5
     eslint: ^8.56.0
     eslint-config-prettier: ^9.1.0
     eslint-plugin-prettier: ^5.1.3
@@ -3321,11 +3681,13 @@ __metadata:
     jest: ^29.7.0
     jest-environment-jsdom: ^29.0.0
     jest-util: ^29.7.0
+    lodash: ^4.17.21
     material-design-icons: ^3.0.1
     npm-run-all: ^4.1.5
     prettier: ^3.2.4
     react: ^18.2.0
     react-dom: ^18.2.0
+    react-split: ^2.0.14
     rimraf: ^5.0.5
     ts-jest: ^29.1.2
     typescript: ~5.3.3
@@ -3809,6 +4171,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"classcat@npm:^5.0.3":
+  version: 5.0.5
+  resolution: "classcat@npm:5.0.5"
+  checksum: 
19bdeb99b8923b47f9df978b6ef2c5a4cc3bcaa8fb6be16244e31fad619b291b366429747331903ac2ea27560ffd6066d14089a99c95535ce0f1e897525fa63d
+  languageName: node
+  linkType: hard
+
 "classnames@npm:*, classnames@npm:^2.2.6, classnames@npm:^2.3.1":
   version: 2.5.1
   resolution: "classnames@npm:2.5.1"
@@ -4109,6 +4478,98 @@ __metadata:
   languageName: node
   linkType: hard
 
+"d3-color@npm:1 - 3":
+  version: 3.1.0
+  resolution: "d3-color@npm:3.1.0"
+  checksum: 
4931fbfda5d7c4b5cfa283a13c91a954f86e3b69d75ce588d06cde6c3628cebfc3af2069ccf225e982e8987c612aa7948b3932163ce15eb3c11cd7c003f3ee3b
+  languageName: node
+  linkType: hard
+
+"d3-dispatch@npm:1 - 3":
+  version: 3.0.1
+  resolution: "d3-dispatch@npm:3.0.1"
+  checksum: 
fdfd4a230f46463e28e5b22a45dd76d03be9345b605e1b5dc7d18bd7ebf504e6c00ae123fd6d03e23d9e2711e01f0e14ea89cd0632545b9f0c00b924ba4be223
+  languageName: node
+  linkType: hard
+
+"d3-drag@npm:2 - 3, d3-drag@npm:^3.0.0":
+  version: 3.0.0
+  resolution: "d3-drag@npm:3.0.0"
+  dependencies:
+    d3-dispatch: 1 - 3
+    d3-selection: 3
+  checksum: 
d297231e60ecd633b0d076a63b4052b436ddeb48b5a3a11ff68c7e41a6774565473a6b064c5e9256e88eca6439a917ab9cea76032c52d944ddbf4fd289e31111
+  languageName: node
+  linkType: hard
+
+"d3-ease@npm:1 - 3":
+  version: 3.0.1
+  resolution: "d3-ease@npm:3.0.1"
+  checksum: 
06e2ee5326d1e3545eab4e2c0f84046a123dcd3b612e68858219aa034da1160333d9ce3da20a1d3486d98cb5c2a06f7d233eee1bc19ce42d1533458bd85dedcd
+  languageName: node
+  linkType: hard
+
+"d3-interpolate@npm:1 - 3, d3-interpolate@npm:^3.0.1":
+  version: 3.0.1
+  resolution: "d3-interpolate@npm:3.0.1"
+  dependencies:
+    d3-color: 1 - 3
+  checksum: 
a42ba314e295e95e5365eff0f604834e67e4a3b3c7102458781c477bd67e9b24b6bb9d8e41ff5521050a3f2c7c0c4bbbb6e187fd586daa3980943095b267e78b
+  languageName: node
+  linkType: hard
+
+"d3-selection@npm:2 - 3, d3-selection@npm:3, d3-selection@npm:^3.0.0":
+  version: 3.0.0
+  resolution: "d3-selection@npm:3.0.0"
+  checksum: 
f4e60e133309115b99f5b36a79ae0a19d71ee6e2d5e3c7216ef3e75ebd2cb1e778c2ed2fa4c01bef35e0dcbd96c5428f5bd6ca2184fe2957ed582fde6841cbc5
+  languageName: node
+  linkType: hard
+
+"d3-timer@npm:1 - 3":
+  version: 3.0.1
+  resolution: "d3-timer@npm:3.0.1"
+  checksum: 
1cfddf86d7bca22f73f2c427f52dfa35c49f50d64e187eb788dcad6e927625c636aa18ae4edd44d084eb9d1f81d8ca4ec305dae7f733c15846a824575b789d73
+  languageName: node
+  linkType: hard
+
+"d3-transition@npm:2 - 3":
+  version: 3.0.1
+  resolution: "d3-transition@npm:3.0.1"
+  dependencies:
+    d3-color: 1 - 3
+    d3-dispatch: 1 - 3
+    d3-ease: 1 - 3
+    d3-interpolate: 1 - 3
+    d3-timer: 1 - 3
+  peerDependencies:
+    d3-selection: 2 - 3
+  checksum: 
cb1e6e018c3abf0502fe9ff7b631ad058efb197b5e14b973a410d3935aead6e3c07c67d726cfab258e4936ef2667c2c3d1cd2037feb0765f0b4e1d3b8788c0ea
+  languageName: node
+  linkType: hard
+
+"d3-zoom@npm:^3.0.0":
+  version: 3.0.0
+  resolution: "d3-zoom@npm:3.0.0"
+  dependencies:
+    d3-dispatch: 1 - 3
+    d3-drag: 2 - 3
+    d3-interpolate: 1 - 3
+    d3-selection: 2 - 3
+    d3-transition: 2 - 3
+  checksum: 
8056e3527281cfd1ccbcbc458408f86973b0583e9dac00e51204026d1d36803ca437f970b5736f02fafed9f2b78f145f72a5dbc66397e02d4d95d4c594b8ff54
+  languageName: node
+  linkType: hard
+
+"dagre@npm:^0.8.5":
+  version: 0.8.5
+  resolution: "dagre@npm:0.8.5"
+  dependencies:
+    graphlib: ^2.1.8
+    lodash: ^4.17.15
+  checksum: 
b9fabd425466d7b662381c2e457b1adda996bc4169aa60121d4de50250d83a6bb4b77d559e2f887c9c564caea781c2a377fd4de2a76c15f8f04ec3d086ca95f9
+  languageName: node
+  linkType: hard
+
 "data-urls@npm:^2.0.0":
   version: 2.0.0
   resolution: "data-urls@npm:2.0.0"
@@ -5453,6 +5914,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"graphlib@npm:^2.1.8":
+  version: 2.1.8
+  resolution: "graphlib@npm:2.1.8"
+  dependencies:
+    lodash: ^4.17.15
+  checksum: 
1e0db4dea1c8187d59103d5582ecf32008845ebe2103959a51d22cb6dae495e81fb9263e22c922bca3aaecb56064a45cd53424e15a4626cfb5a0c52d0aff61a8
+  languageName: node
+  linkType: hard
+
 "harmony-reflect@npm:^1.4.6":
   version: 1.6.2
   resolution: "harmony-reflect@npm:1.6.2"
@@ -6985,7 +7455,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.17.4, 
lodash@npm:^4.7.0":
+"lodash@npm:^4.17.15, lodash@npm:^4.17.20, lodash@npm:^4.17.21, 
lodash@npm:^4.17.4, lodash@npm:^4.7.0":
   version: 4.17.21
   resolution: "lodash@npm:4.17.21"
   checksum: 
eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7
@@ -7964,7 +8434,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"prop-types@npm:15.x, prop-types@npm:^15.5.10, prop-types@npm:^15.5.8, 
prop-types@npm:^15.8.1":
+"prop-types@npm:15.x, prop-types@npm:^15.5.10, prop-types@npm:^15.5.7, 
prop-types@npm:^15.5.8, prop-types@npm:^15.8.1":
   version: 15.8.1
   resolution: "prop-types@npm:15.8.1"
   dependencies:
@@ -8136,6 +8606,18 @@ __metadata:
   languageName: node
   linkType: hard
 
+"react-split@npm:^2.0.14":
+  version: 2.0.14
+  resolution: "react-split@npm:2.0.14"
+  dependencies:
+    prop-types: ^15.5.7
+    split.js: ^1.6.0
+  peerDependencies:
+    react: "*"
+  checksum: 
8e9e22b8ef48063ab0b55a96bbb3ff66012ddbe899661deac30b10b72aa08c1b669eb644e599ec1ac0ce880017fc2d4b2e6cb48b113789646d6186d61d8f55f2
+  languageName: node
+  linkType: hard
+
 "react@npm:>=17.0.0 <19.0.0, react@npm:^18.2.0":
   version: 18.3.1
   resolution: "react@npm:18.3.1"
@@ -8796,6 +9278,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"split.js@npm:^1.6.0":
+  version: 1.6.5
+  resolution: "split.js@npm:1.6.5"
+  checksum: 
a3e77d8e0628de06c58e2d8e6ab41872132586c417b4f40ac3d3dbf76c2b31f40745dd86c133609dacd5599ada5d4bee157f6290403223b86bd79fe700a77983
+  languageName: node
+  linkType: hard
+
 "sprintf-js@npm:^1.1.3":
   version: 1.1.3
   resolution: "sprintf-js@npm:1.1.3"
@@ -8828,6 +9317,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"state-local@npm:^1.0.6":
+  version: 1.0.7
+  resolution: "state-local@npm:1.0.7"
+  checksum: 
d1afcf1429e7e6eb08685b3a94be8797db847369316d4776fd51f3962b15b984dacc7f8e401ad20968e5798c9565b4b377afedf4e4c4d60fe7495e1cbe14a251
+  languageName: node
+  linkType: hard
+
 "stop-iteration-iterator@npm:^1.0.0, stop-iteration-iterator@npm:^1.1.0":
   version: 1.1.0
   resolution: "stop-iteration-iterator@npm:1.1.0"
@@ -9368,11 +9864,11 @@ __metadata:
 
 "typescript@patch:typescript@~5.3.3#~builtin<compat/typescript>":
   version: 5.3.3
-  resolution: 
"typescript@patch:typescript@npm%3A5.3.3#~builtin<compat/typescript>::version=5.3.3&hash=e012d7"
+  resolution: 
"typescript@patch:typescript@npm%3A5.3.3#~builtin<compat/typescript>::version=5.3.3&hash=85af82"
   bin:
     tsc: bin/tsc
     tsserver: bin/tsserver
-  checksum: 
4e604a9e107ce0c23b16a2f8d79d0531d4d8fe9ebbb7a8c395c66998c39892f0e0a071ef0b0d4e66420a8ec2b8d6cfd9cdb29ba24f25b37cba072e9282376df9
+  checksum: 
f61375590b3162599f0f0d5b8737877ac0a7bc52761dbb585d67e7b8753a3a4c42d9a554c4cc929f591ffcf3a2b0602f65ae3ce74714fd5652623a816862b610
   languageName: node
   linkType: hard
 
@@ -9470,6 +9966,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"use-sync-external-store@npm:^1.2.2":
+  version: 1.5.0
+  resolution: "use-sync-external-store@npm:1.5.0"
+  peerDependencies:
+    react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+  checksum: 
5e639c9273200adb6985b512c96a3a02c458bc8ca1a72e91da9cdc6426144fc6538dca434b0f99b28fb1baabc82e1c383ba7900b25ccdcb43758fb058dc66c34
+  languageName: node
+  linkType: hard
+
 "util-deprecate@npm:^1.0.2":
   version: 1.0.2
   resolution: "util-deprecate@npm:1.0.2"
@@ -9985,3 +10490,23 @@ __metadata:
   checksum: 
f77b3d8d00310def622123df93d4ee654fc6a0096182af8bd60679ddcdfb3474c56c6c7190817c84a2785648cdee9d721c0154eb45698c62176c322fb46fc700
   languageName: node
   linkType: hard
+
+"zustand@npm:^4.4.0":
+  version: 4.5.7
+  resolution: "zustand@npm:4.5.7"
+  dependencies:
+    use-sync-external-store: ^1.2.2
+  peerDependencies:
+    "@types/react": ">=16.8"
+    immer: ">=9.0.6"
+    react: ">=16.8"
+  peerDependenciesMeta:
+    "@types/react":
+      optional: true
+    immer:
+      optional: true
+    react:
+      optional: true
+  checksum: 
103ab43456bbc3be6afe79b18a93c7fa46ffaa1aa35c45b213f13f4cd0868fee78b43c6805c6d80a822297df2e455fd021c28be94b80529ec4806b2724f20219
+  languageName: node
+  linkType: hard

Reply via email to