Skip to content

CavaiFlow Analysis

Comprehensive analysis of CavaiFlow.vue and its subsystem -- the flow editor for connecting operators in a creative's dialogue flow. This document maps the current architecture, identifies problems, and informs the refactoring design.

Related issues:

  • AF#1829 -- Cross-repo naming cleanup and directory flattening (Chatbots -> Creatives)
  • CE#558 -- Rename components and directories to improve descriptiveness
  • AF PR#1724 -- Add perfect snapping and distribution tools to flow (merged Oct 2025)

File Statistics

SectionLinesNotes
Template1-422422 lines
Script424-52044780 lines, 123 methods
Styles (unscoped)5206-526054 lines (anchor positioning, animations editor)
Styles (scoped)5261-5521260 lines
Total5521

Method Breakdown by Domain

DomainLines%Key methods
Context Menu / UI90619.5%radialMenuClick (304 lines!), openFCContextMenu (172), updateRadialMenuContent (93)
Data / Lifecycle72815.7%initFlow (117), containerUpdate (63), watchers, props, data
Drag & Drop4599.9%finishMultipleDrag (129), finishDrag (112), startDrag (74)
Zoom / Pan3387.3%changeZoom (165), zoomPanToProblematicOperators (66)
Keyboard3176.8%handleKeyDown (292 lines!), handleKeyUp (25)
Operator CRUD3036.5%createOperator (153), removeMultipleOperators (32)
Copy / Paste3016.5%pasteContent (91), pasteHelper (67), copyCutHelper (56)
Selection / Focus2685.8%dragSelectHandler (70), blurComponent (62)
Styles / Animations editor2545.5%openLargeCodeEditor (46), openStylesEditor (32)
Undo / Redo2114.5%undoAction (71), redoAction (57), saveCheckpointData (55)
Analytics1753.8%getAnalyticsData (157)
Links / Connectors1433.1%makeTempLinkPermanent (69), removeLink (23)
Arrange / Distribution1413.0%arrangeHorizontally (74), arrangeVertically (67)
Grid / Snapping561.2%updateMultipleOpsPos (35)

Current Directory Structure

pages/Chatbots/components/CavaiFlow/
  CavaiFlow.vue                    # 5521 lines -- the monolith
  DataHelper.ts                    # 239 lines -- validation, step/ID mapping
  OperatorHelper.ts                # 232 lines -- operator creation, ID generation
  interfaces.ts                    # 103 lines -- CavaiFlowData interface
  brandingToolbarConstants.ts      # Branding toolbar color/label constants
  BrandingToolbar.vue              # Floating color/label toolbar
  ColorIndicatorPanel.vue          # Color dots panel
  LabelToolbar.vue                 # Label editing toolbar
  NodeLabel.vue                    # Inline operator label
  flowactors/
    OperatorBase.vue               # Operator wrapper (positioning, drag, analytics overlay)
    OperatorLink.vue               # SVG bezier connector between operators
    TemporaryLink.vue              # SVG link shown during link creation
    OperatorStyles.vue             # CSS editor panel (Ace/CodeMirror)
    OperatorJssStyles.vue          # JSS styles panel
    OperatorAnimations.vue         # Animation config panel
    OperatorLargeCode.vue          # Large code editor panel
    RadialContextMenu.vue          # Radial right-click menu
    DragSelect.vue                 # Rectangle selection
    invert.scss                    # Styles for "change" operator types
  operators/                       # 29 operator type components
    AnswerOp.vue, QuestionOp.vue, StatementOp.vue, ...
    MiddlewareOp/                  # Sub-directory for middleware variants
    TargetOpSelector/              # Sub-directory for target selector

Known Bugs

1. Connector kinks on flow re-entry

Symptom: Connectors appear kinked/bent when navigating away from the flow and returning, especially on operators like LibraryScript.

Root cause: OperatorLink.vue calculates connector attachment points using getBoundingClientRect():

js
// OperatorLink.vue:127-143
getConnectorPoint(el, to) {
  const rect = el.getBoundingClientRect()
  const parentRect = el.parentElement.parentElement.getBoundingClientRect()
  const o = this.$parent.$parent.logicPZ.getTransform()

  const leftright = to
    ? rect.left / o.scale + window.scrollX - parentRect.left / o.scale
    : rect.right / o.scale + window.scrollX - parentRect.left / o.scale
  const middle = (0.5 / o.scale) * (rect.top + rect.bottom) + window.scrollY - parentRect.top / o.scale

  return { x: leftright, y: middle }
}

This is timing-dependent: when the flow re-renders, showLinks is set to true in a single $nextTick (line 2283), but complex operators (LibraryScript, Slider, ShowHide with targets) haven't reached their final height yet. The connector calculates against a partially-rendered element.

There is no automatic link refresh when the flow becomes visible. Links only update via explicit updateLink() calls, which increment updateCount to force the computed property to re-evaluate.

2. Connector bend from height mismatch

Symptom: Connectors between operators of different heights have visible curves even when operators are horizontally aligned.

Root cause: The connector attaches at each operator's vertical center. Different heights mean different Y attachment points. The bezier curve:

js
// OperatorLink.vue:100-112
const bendDistance = Math.min(200, Math.max(deltaX, deltaY))

const curveCommands = [
  [`M${start.x}`, start.y],
  [`C${start.x + bendDistance}`, start.y],
  [end.x - bendDistance, end.y],
  [end.x, end.y],
]

When operators have different vertical centers, deltaY > 0, which increases bendDistance, which pushes the control points further apart, amplifying the visual curve. For operators that are close horizontally but far apart vertically, the S-curve becomes very pronounced.

3. Arrange spacing not reproducible manually

Symptom: After using Cmd+Shift+V or Cmd+Shift+H to arrange operators, the resulting spacing (72px) cannot be manually reproduced by dragging.

Root cause: The arrange functions use spacing = 72:

js
// CavaiFlow.vue:1620
const spacing = 72

But the grid snap uses xGrid = 48 and yGrid = 2:

js
// CavaiFlow.vue:2883-2884
const yGrid = 2
const xGrid = 48

72 is not a multiple of 48. The Y grid (2px) is essentially free-positioning. There's no guide or snap at 72px intervals, so users can't manually place operators at the same spacing the arrange function produces.

Additionally, arrangeVertically() centers operators horizontally using component width midpoints, which produces non-grid-aligned X positions.

4. Y grid is meaningless

The grid SVG (builder-grid.svg) draws 48px cells:

svg
<svg width="192" height="192" viewBox="0 0 192 192">
  <!-- 4 vertical lines at x=48, 96, 144, 192 -->
  <!-- 4 horizontal lines at y=45/48, 93/96, 141/144, 189/192 -->
</svg>

But the Y snap is yGrid = 2, meaning operators snap to every 2 pixels vertically -- invisible and useless as a positioning aid. X snaps to 48px (matching grid lines), creating an asymmetry.


Code Redundancy

Duplicated snap logic

finishDrag() (lines 2878-2945) and finishMultipleDrag() (lines 2674-2753) contain nearly identical snap-to-grid code:

  • Same yGrid = 2, xGrid = 48, snapThreshold = 24
  • Same midpoint calculation pattern
  • Same nearby-component-midpoint search
  • Only difference: opHeight vs selectionHeight

Duplicated arrow key handling

handleKeyDown (line ~1400-1440) has four nearly identical branches for ArrowLeft/Up/Right/Down, each doing:

  1. Update component position
  2. Call updateLinks(op)
  3. Update ghost selection container position

Unused parameter

updateOpPos(op, X, Y) is called with a fourth src argument ('arrange', 'dragend', 'multipledragend', 'dragover') throughout the codebase, but the function signature only uses three parameters and ignores the source.

$parent.$parent anti-pattern

Both OperatorLink.vue and TemporaryLink.vue access the CavaiFlow instance via this.$parent.$parent:

  • this.$parent.$parent.flowInitDone
  • this.$parent.$parent.$refs[...]
  • this.$parent.$parent.logicPZ.getTransform()

This creates tight coupling and breaks if the component hierarchy changes.


Relationship to Issue #1829

Issue #1829 plans to rename Chatbots/ -> Creatives/ and flatten the directory structure. The proposed new location for CavaiFlow:

pages/Creatives/Builder/Flow/     # was pages/Chatbots/components/CavaiFlow/
  operators/
  actors/                         # was flowactors/

Our refactoring should be designed so that:

  1. Extracted modules have clean imports (no deep relative paths)
  2. New file names align with the planned rename (e.g., FlowGrid.ts not CavaiFlowGrid.ts)
  3. The extraction reduces the blast radius of the future directory rename

Internal documentation