From 4127ae42fb4d2a5309ae03e39b9d69c967671e9a Mon Sep 17 00:00:00 2001 From: laansdole Date: Sun, 8 Feb 2026 10:43:31 +0700 Subject: [PATCH 01/10] fix: missing system dependencies install in gha --- .github/workflows/validate-yamls.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/validate-yamls.yml b/.github/workflows/validate-yamls.yml index cde6d64b..9c6f634a 100644 --- a/.github/workflows/validate-yamls.yml +++ b/.github/workflows/validate-yamls.yml @@ -33,6 +33,11 @@ jobs: with: python-version: '3.12' + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libcairo2-dev pkg-config + - name: Install uv uses: astral-sh/setup-uv@v4 with: From e0890eec686bdae31b8807c7d00e6ca4667aa414 Mon Sep 17 00:00:00 2001 From: laansdole Date: Sun, 8 Feb 2026 10:46:03 +0700 Subject: [PATCH 02/10] chores: refactor gha name --- .github/workflows/validate-yamls.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/validate-yamls.yml b/.github/workflows/validate-yamls.yml index 9c6f634a..70141d4a 100644 --- a/.github/workflows/validate-yamls.yml +++ b/.github/workflows/validate-yamls.yml @@ -20,8 +20,7 @@ on: workflow_dispatch: jobs: - validate: - name: Validate YAML Configuration Files + validate-yamls: runs-on: ubuntu-latest steps: From e8e12f7b2c3524722e0b320b03c571a572cba971 Mon Sep 17 00:00:00 2001 From: laansdole Date: Sun, 8 Feb 2026 10:53:59 +0700 Subject: [PATCH 03/10] feat: rich tooltip component --- frontend/src/components/RichTooltip.vue | 351 ++++++++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 frontend/src/components/RichTooltip.vue diff --git a/frontend/src/components/RichTooltip.vue b/frontend/src/components/RichTooltip.vue new file mode 100644 index 00000000..ead65efc --- /dev/null +++ b/frontend/src/components/RichTooltip.vue @@ -0,0 +1,351 @@ + + + + + From b3b7376ef2300ea39fcd56e11eab0ebb726b1e7f Mon Sep 17 00:00:00 2001 From: laansdole Date: Sun, 8 Feb 2026 10:56:56 +0700 Subject: [PATCH 04/10] feat: add help content to tooltip --- frontend/src/utils/helpContent.js | 229 ++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 frontend/src/utils/helpContent.js diff --git a/frontend/src/utils/helpContent.js b/frontend/src/utils/helpContent.js new file mode 100644 index 00000000..85e1b403 --- /dev/null +++ b/frontend/src/utils/helpContent.js @@ -0,0 +1,229 @@ +export const helpContent = { + // Start Node Help + startNode: { + title: "Start Node", + description: "The entry point for your workflow. All nodes connected to the Start node will run in parallel when the workflow launches.", + examples: [ + "Connect multiple nodes to start them simultaneously", + "The first nodes to execute receive your initial input" + ], + learnMoreUrl: "/tutorial#2-create-nodes" + }, + + // Workflow Node Types + workflowNode: { + agent: { + title: "Agent Node", + description: "An AI agent that can reason, generate content, and use tools. Agents receive messages and produce responses based on their configuration.", + examples: [ + "Content generation (writing, coding, analysis)", + "Decision making and routing", + "Tool usage (search, file operations, API calls)" + ], + learnMoreUrl: "/tutorial#agent-node" + }, + human: { + title: "Human Node", + description: "Pauses workflow execution and waits for human input. Use this to review content, make decisions, or provide feedback.", + examples: [ + "Review and approve generated content", + "Provide additional instructions or corrections", + "Choose between workflow paths" + ], + learnMoreUrl: "/tutorial#human-node" + }, + python: { + title: "Python Node", + description: "Executes Python code in a sandboxed environment. The code runs in the workspace directory and can access uploaded files.", + examples: [ + "Data processing and analysis", + "Running generated code", + "File manipulation" + ], + learnMoreUrl: "/tutorial#python-node" + }, + passthrough: { + title: "Passthrough Node", + description: "Passes messages to the next node without modification. Useful for workflow organization and filtering outputs in loops.", + examples: [ + "Preserve initial context in loops", + "Filter redundant outputs", + "Organize workflow structure" + ], + learnMoreUrl: "/tutorial#passthrough-node" + }, + literal: { + title: "Literal Node", + description: "Outputs fixed text, ignoring all input. Use this to inject instructions or context at specific points in the workflow.", + examples: [ + "Add fixed instructions before a node", + "Inject context or constraints", + "Provide test data" + ], + learnMoreUrl: "/tutorial#literal-node" + }, + loop_counter: { + title: "Loop Counter Node", + description: "Limits loop iterations. Only produces output when the maximum count is reached, helping control infinite loops.", + examples: [ + "Prevent runaway loops", + "Set maximum revision cycles", + "Control iterative processes" + ], + learnMoreUrl: "/tutorial#loop-counter-node" + }, + subgraph: { + title: "Subgraph Node", + description: "Embeds another workflow as a reusable module. Enables modular design and workflow composition.", + examples: [ + "Reuse common patterns across workflows", + "Break complex workflows into manageable pieces", + "Share workflows between teams" + ], + learnMoreUrl: "/tutorial#subgraph-node" + }, + unknown: { + title: "Workflow Node", + description: "A node in your workflow. Click to view and edit its configuration.", + learnMoreUrl: "/tutorial#2-create-nodes" + } + }, + + // Workflow Edge Help + edge: { + basic: { + title: "Connection", + description: "Connects two nodes to control information flow and execution order. The upstream node's output becomes the downstream node's input.", + examples: [ + "Data flows from source to target", + "Target executes after source completes" + ], + learnMoreUrl: "/tutorial#what-is-an-edge" + }, + trigger: { + enabled: { + description: "This connection triggers the downstream node to execute.", + }, + disabled: { + description: "This connection passes data but does NOT trigger execution. The downstream node only runs if triggered by another edge.", + } + }, + condition: { + hasCondition: { + description: "This connection has a condition. It only activates when the condition evaluates to true.", + learnMoreUrl: "/tutorial#edge-condition" + } + } + }, + + // Context Menu Actions + contextMenu: { + createNode: { + description: "Create a new node in your workflow. Choose from Agent, Human, Python, and other node types.", + }, + copyNode: { + description: "Duplicate this node with all its settings. The copy will have a blank ID that you must fill in.", + }, + deleteNode: { + description: "Remove this node and all its connections from the workflow.", + }, + deleteEdge: { + description: "Remove this connection between nodes.", + }, + createNodeButton: { + description: "Open the node creation form. You can also right-click the canvas to create a node at a specific position.", + }, + configureGraph: { + description: "Configure workflow-level settings like name, description, and global variables.", + }, + launch: { + description: "Run your workflow with a task prompt. The workflow will execute and show you the results.", + }, + createEdge: { + description: "Create a connection between nodes. You can also drag from a node's handle to create connections visually.", + }, + manageVariables: { + description: "Define global variables (like API keys) that all nodes can access using ${VARIABLE_NAME} syntax.", + }, + manageMemories: { + description: "Configure memory modules for long-term information storage and retrieval across workflow runs.", + }, + renameWorkflow: { + description: "Change the name of this workflow file.", + }, + copyWorkflow: { + description: "Create a duplicate of this entire workflow with a new name.", + } + } +} + +/** + * Get help content by key path + * @param {string} key - Dot-separated path to content (e.g., 'workflowNode.agent') + * @returns {Object} Help content object or fallback + */ +export function getHelpContent(key) { + const keys = key.split('.') + let content = helpContent + + for (const k of keys) { + if (content && typeof content === 'object' && k in content) { + content = content[k] + } else { + console.warn(`[HelpContent] Missing content for key: ${key}`) + return { + description: "Help content coming soon. Check the tutorial for more information.", + learnMoreUrl: "/tutorial" + } + } + } + + // Ensure we return an object with at least a description + if (typeof content === 'string') { + return { description: content } + } + + return content || { description: "Help content coming soon." } +} + +/** + * Get node-specific help content based on node type + * @param {string} nodeType - The type of node (agent, human, python, etc.) + * @returns {Object} Help content for that node type + */ +export function getNodeHelp(nodeType) { + const type = (nodeType || 'unknown').toLowerCase() + return getHelpContent(`workflowNode.${type}`) +} + +/** + * Get edge help content based on edge properties + * @param {Object} edgeData - The edge data object + * @returns {Object} Combined help content for the edge + */ +export function getEdgeHelp(edgeData) { + const base = { ...helpContent.edge.basic } + + // Add trigger information + const trigger = edgeData?.trigger !== undefined ? edgeData.trigger : true + if (!trigger) { + base.description += " " + helpContent.edge.trigger.disabled.description + } + + // Add condition information + if (edgeData?.condition) { + base.description += " " + helpContent.edge.condition.hasCondition.description + if (!base.learnMoreUrl) { + base.learnMoreUrl = helpContent.edge.condition.hasCondition.learnMoreUrl + } + } + + return base +} + +export default { + helpContent, + getHelpContent, + getNodeHelp, + getEdgeHelp +} From 53870bd6f001d3e3af7dc377785dd8e1362b8cff Mon Sep 17 00:00:00 2001 From: laansdole Date: Sun, 8 Feb 2026 10:59:22 +0700 Subject: [PATCH 05/10] feat: wrapping nodes with tooltip content --- frontend/src/components/StartNode.vue | 14 ++-- frontend/src/components/WorkflowEdge.vue | 30 +++++++ frontend/src/components/WorkflowNode.vue | 68 ++++++++------- frontend/src/pages/WorkflowView.vue | 102 ++++++++++++++--------- 4 files changed, 140 insertions(+), 74 deletions(-) diff --git a/frontend/src/components/StartNode.vue b/frontend/src/components/StartNode.vue index 36308697..1aa0b20a 100755 --- a/frontend/src/components/StartNode.vue +++ b/frontend/src/components/StartNode.vue @@ -1,6 +1,8 @@ diff --git a/frontend/src/components/WorkflowNode.vue b/frontend/src/components/WorkflowNode.vue index cfdc8dd9..d6c20584 100755 --- a/frontend/src/components/WorkflowNode.vue +++ b/frontend/src/components/WorkflowNode.vue @@ -3,6 +3,8 @@ import { computed, ref, onMounted, onUnmounted, watch } from 'vue' import { Handle, Position } from '@vue-flow/core' import { getNodeStyles } from '../utils/colorUtils.js' import { spriteFetcher } from '../utils/spriteFetcher.js' +import RichTooltip from './RichTooltip.vue' +import { getNodeHelp } from '../utils/helpContent.js' const props = defineProps({ id: { @@ -37,6 +39,8 @@ const nodeDescription = computed(() => props.data?.description || '') const isActive = computed(() => props.isActive) const dynamicStyles = computed(() => getNodeStyles(nodeType.value)) +const nodeHelpContent = computed(() => getNodeHelp(nodeType.value)) + // Compute the current sprite path based on active state and walking frame const currentSprite = computed(() => { if (!props.sprite) return '' @@ -83,40 +87,42 @@ onUnmounted(() => {