Appearance
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
| Section | Lines | Notes |
|---|---|---|
| Template | 1-422 | 422 lines |
| Script | 424-5204 | 4780 lines, 123 methods |
| Styles (unscoped) | 5206-5260 | 54 lines (anchor positioning, animations editor) |
| Styles (scoped) | 5261-5521 | 260 lines |
| Total | 5521 |
Method Breakdown by Domain
| Domain | Lines | % | Key methods |
|---|---|---|---|
| Context Menu / UI | 906 | 19.5% | radialMenuClick (304 lines!), openFCContextMenu (172), updateRadialMenuContent (93) |
| Data / Lifecycle | 728 | 15.7% | initFlow (117), containerUpdate (63), watchers, props, data |
| Drag & Drop | 459 | 9.9% | finishMultipleDrag (129), finishDrag (112), startDrag (74) |
| Zoom / Pan | 338 | 7.3% | changeZoom (165), zoomPanToProblematicOperators (66) |
| Keyboard | 317 | 6.8% | handleKeyDown (292 lines!), handleKeyUp (25) |
| Operator CRUD | 303 | 6.5% | createOperator (153), removeMultipleOperators (32) |
| Copy / Paste | 301 | 6.5% | pasteContent (91), pasteHelper (67), copyCutHelper (56) |
| Selection / Focus | 268 | 5.8% | dragSelectHandler (70), blurComponent (62) |
| Styles / Animations editor | 254 | 5.5% | openLargeCodeEditor (46), openStylesEditor (32) |
| Undo / Redo | 211 | 4.5% | undoAction (71), redoAction (57), saveCheckpointData (55) |
| Analytics | 175 | 3.8% | getAnalyticsData (157) |
| Links / Connectors | 143 | 3.1% | makeTempLinkPermanent (69), removeLink (23) |
| Arrange / Distribution | 141 | 3.0% | arrangeHorizontally (74), arrangeVertically (67) |
| Grid / Snapping | 56 | 1.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 selectorKnown 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 = 72But the grid snap uses xGrid = 48 and yGrid = 2:
js
// CavaiFlow.vue:2883-2884
const yGrid = 2
const xGrid = 4872 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:
opHeightvsselectionHeight
Duplicated arrow key handling
handleKeyDown (line ~1400-1440) has four nearly identical branches for ArrowLeft/Up/Right/Down, each doing:
- Update component position
- Call
updateLinks(op) - 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.flowInitDonethis.$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:
- Extracted modules have clean imports (no deep relative paths)
- New file names align with the planned rename (e.g.,
FlowGrid.tsnotCavaiFlowGrid.ts) - The extraction reduces the blast radius of the future directory rename