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) => ( -
-
+ ))} + + + + ); +} diff --git a/src/components/HotspotModal.jsx b/src/components/HotspotModal.jsx index 4265e19..0ce96f6 100644 --- a/src/components/HotspotModal.jsx +++ b/src/components/HotspotModal.jsx @@ -1,9 +1,10 @@ import { useState, useEffect } from "react"; import { COLORS, FONTS, styles } from "../styles/theme"; import { generateId } from "../utils/generateId"; +import { DataFlowEditor } from "./DataFlowEditor"; function FollowUpSection({ title, titleColor, action, setAction, targetId, setTargetId, - customDesc, setCustomDesc, otherScreens }) { + customDesc, setCustomDesc, otherScreens, dataFlow, onDataFlowChange }) { return (
{(action === "navigate" || action === "modal") && ( - + <> + + {dataFlow && onDataFlowChange && ( +
+ +
+ )} + )} {action === "custom" && ( @@ -105,6 +113,11 @@ export function HotspotModal({ screen, hotspot, connection, screens, documents = const [onErrorTargetId, setOnErrorTargetId] = useState(hotspot?.onErrorTargetId || ""); const [onErrorCustomDesc, setOnErrorCustomDesc] = useState(hotspot?.onErrorCustomDesc || ""); + // Data flow fields + const [dataFlow, setDataFlow] = useState(hotspot?.dataFlow || []); + const [onSuccessDataFlow, setOnSuccessDataFlow] = useState(hotspot?.onSuccessDataFlow || []); + const [onErrorDataFlow, setOnErrorDataFlow] = useState(hotspot?.onErrorDataFlow || []); + // Conditional branching fields const [conditions, setConditions] = useState( hotspot?.conditions?.length > 0 @@ -276,6 +289,9 @@ export function HotspotModal({ screen, hotspot, connection, screens, documents = onErrorAction, onErrorTargetId: onErrorTargetId || null, onErrorCustomDesc, + dataFlow: (action === "navigate" || action === "modal") ? dataFlow : [], + onSuccessDataFlow: action === "api" ? onSuccessDataFlow : [], + onErrorDataFlow: action === "api" ? onErrorDataFlow : [], conditions: action === "conditional" ? conditions : [], x, y, w, h, transitionType, @@ -365,65 +381,76 @@ export function HotspotModal({ screen, hotspot, connection, screens, documents =
{conditions.map((cond, i) => ( -
-