From 8e1153d829eea0881a46507b47bf1d12e1c5fd1b Mon Sep 17 00:00:00 2001
From: Quang Tran <16215255+trmquang93@users.noreply.github.com>
Date: Mon, 30 Mar 2026 09:39:58 +0700
Subject: [PATCH] feat: add data flow annotations to connections
Connections can now carry typed data items (name, type, description) that
describe what information passes between screens. This closes the biggest
gap in instruction generation -- AI agents now know exactly what parameters
each screen receives.
- Add dataFlow[] to connection data model (FILE_VERSION 10 -> 11)
- New DataFlowEditor component for editing data items in modals
- Integrate into ConnectionEditModal (navigate + conditional modes)
- Integrate into HotspotModal (navigate, modal, API success/error, conditional)
- Render teal pill badge on canvas for connections with data annotations
- Add "Input Parameters" table per screen in generated screens.md
- Add "Data" column to navigation.md connection table
- Annotate wire items in tasks.md with passed parameters
- Add data_flow parameter to MCP server create/update connection tools
- Update user guide with data flow annotations documentation
---
mcp-server/src/state.js | 5 +-
mcp-server/src/tools/connection-tools.js | 45 +++++-
src/components/ConnectionEditModal.jsx | 119 +++++++++-------
src/components/ConnectionLines.jsx | 35 +++++
src/components/DataFlowEditor.jsx | 136 ++++++++++++++++++
src/components/HotspotModal.jsx | 173 ++++++++++++++---------
src/constants.js | 2 +-
src/hooks/useScreenManager.js | 12 +-
src/pages/docs/userGuide.md | 13 ++
src/utils/buildPayload.test.js | 4 +-
src/utils/generateInstructionFiles.js | 36 ++++-
src/utils/importFlow.js | 4 +
src/utils/importFlow.test.js | 4 +-
13 files changed, 450 insertions(+), 138 deletions(-)
create mode 100644 src/components/DataFlowEditor.jsx
diff --git a/mcp-server/src/state.js b/mcp-server/src/state.js
index d71ac7c..abadc65 100644
--- a/mcp-server/src/state.js
+++ b/mcp-server/src/state.js
@@ -330,7 +330,7 @@ export class FlowState {
// ── Connection Operations ───────────────────
addConnection(options) {
- const { fromScreenId, toScreenId, label, action, hotspotId, connectionPath, condition, conditionGroupId, transitionType, transitionLabel } = options;
+ const { fromScreenId, toScreenId, label, action, hotspotId, connectionPath, condition, conditionGroupId, transitionType, transitionLabel, dataFlow } = options;
if (!this.getScreen(fromScreenId)) throw new Error(`Source screen not found: ${fromScreenId}`);
if (!this.getScreen(toScreenId)) throw new Error(`Target screen not found: ${toScreenId}`);
@@ -355,6 +355,7 @@ export class FlowState {
conditionGroupId: conditionGroupId || null,
transitionType: transitionType || null,
transitionLabel: transitionLabel || "",
+ dataFlow: dataFlow || [],
};
this.connections.push(conn);
@@ -368,7 +369,7 @@ export class FlowState {
const allowed = [
"label", "action", "fromScreenId", "toScreenId",
"connectionPath", "condition", "conditionGroupId",
- "transitionType", "transitionLabel",
+ "transitionType", "transitionLabel", "dataFlow",
];
for (const key of allowed) {
if (updates[key] !== undefined) {
diff --git a/mcp-server/src/tools/connection-tools.js b/mcp-server/src/tools/connection-tools.js
index 86b9bcc..d1041fa 100644
--- a/mcp-server/src/tools/connection-tools.js
+++ b/mcp-server/src/tools/connection-tools.js
@@ -12,6 +12,19 @@ export const connectionTools = [
condition: { type: "string", description: "Condition text for conditional connections" },
conditionGroupId: { type: "string", description: "Group ID for conditional branch connections" },
transitionType: { type: "string", description: "Transition animation type" },
+ data_flow: {
+ type: "array",
+ description: "Data items passed along this connection",
+ items: {
+ type: "object",
+ properties: {
+ name: { type: "string", description: "Parameter name (e.g. 'productId')" },
+ type: { type: "string", description: "Data type (String, Int, Bool, Object, Array, Date, ID, or custom)" },
+ description: { type: "string", description: "What this parameter represents" },
+ },
+ required: ["name"],
+ },
+ },
},
required: ["fromScreenId", "toScreenId"],
},
@@ -28,6 +41,19 @@ export const connectionTools = [
condition: { type: "string" },
transitionType: { type: "string" },
transitionLabel: { type: "string" },
+ data_flow: {
+ type: "array",
+ description: "Data items passed along this connection",
+ items: {
+ type: "object",
+ properties: {
+ name: { type: "string", description: "Parameter name" },
+ type: { type: "string", description: "Data type" },
+ description: { type: "string", description: "What this parameter represents" },
+ },
+ required: ["name"],
+ },
+ },
},
required: ["connectionId"],
},
@@ -53,16 +79,29 @@ export const connectionTools = [
},
];
+function convertDataFlow(dataFlowSnake) {
+ if (!Array.isArray(dataFlowSnake)) return undefined;
+ return dataFlowSnake.map(item => ({
+ id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
+ name: item.name || "",
+ type: item.type || "String",
+ description: item.description || "",
+ }));
+}
+
export function handleConnectionTool(name, args, state) {
switch (name) {
case "create_connection": {
- const conn = state.addConnection(args);
+ const { data_flow, ...rest } = args;
+ const dataFlow = convertDataFlow(data_flow);
+ const conn = state.addConnection({ ...rest, ...(dataFlow ? { dataFlow } : {}) });
return { connectionId: conn.id, fromScreenId: conn.fromScreenId, toScreenId: conn.toScreenId };
}
case "update_connection": {
- const { connectionId, ...updates } = args;
- const conn = state.updateConnection(connectionId, updates);
+ const { connectionId, data_flow, ...updates } = args;
+ const dataFlow = convertDataFlow(data_flow);
+ const conn = state.updateConnection(connectionId, { ...updates, ...(dataFlow ? { dataFlow } : {}) });
return { success: true, connectionId: conn.id };
}
diff --git a/src/components/ConnectionEditModal.jsx b/src/components/ConnectionEditModal.jsx
index 84f6f54..984d81a 100644
--- a/src/components/ConnectionEditModal.jsx
+++ b/src/components/ConnectionEditModal.jsx
@@ -1,6 +1,7 @@
import { useState } from "react";
import { COLORS, styles } from "../styles/theme";
import { generateId } from "../utils/generateId";
+import { DataFlowEditor } from "./DataFlowEditor";
export function ConnectionEditModal({ connection, groupConnections, screens, fromScreen, onSave, onDelete, onClose }) {
const isConditional = groupConnections.length > 1 || !!connection.conditionGroupId;
@@ -10,6 +11,7 @@ export function ConnectionEditModal({ connection, groupConnections, screens, fro
const [targetId, setTargetId] = useState(connection.toScreenId || "");
const [transitionType, setTransitionType] = useState(connection.transitionType || "");
const [transitionLabel, setTransitionLabel] = useState(connection.transitionLabel || "");
+ const [dataFlow, setDataFlow] = useState(connection.dataFlow || []);
const [conditions, setConditions] = useState(() => {
if (isConditional) {
@@ -17,9 +19,10 @@ export function ConnectionEditModal({ connection, groupConnections, screens, fro
id: c.id,
label: c.condition || c.label || "",
targetScreenId: c.toScreenId || "",
+ dataFlow: c.dataFlow || [],
}));
}
- return [{ id: generateId(), label: "", targetScreenId: connection.toScreenId || "" }];
+ return [{ id: generateId(), label: "", targetScreenId: connection.toScreenId || "", dataFlow: [] }];
});
const otherScreens = screens.filter((s) => s.id !== fromScreen.id);
@@ -35,6 +38,7 @@ export function ConnectionEditModal({ connection, groupConnections, screens, fro
conditionGroupId: connection.conditionGroupId || null,
transitionType: transitionType || null,
transitionLabel: transitionType === "custom" ? transitionLabel : "",
+ dataFlow: mode === "navigate" ? dataFlow : [],
});
};
@@ -103,6 +107,8 @@ export function ConnectionEditModal({ connection, groupConnections, screens, fro
))}
+
+
>
)}
@@ -126,65 +132,76 @@ export function ConnectionEditModal({ connection, groupConnections, screens, fro
{conditions.map((cond, i) => (
-
-