Skip to content

JumpTo Operator & Flow Fast-Forward Implementation Plan

For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add a JumpTo operator that jumps to any flow operator, then build a "content-aware first" mode that fast-forwards through the flow accumulating all state (CSS, JS, ShowHide, etc.) instead of skipping it.

Architecture: Two features sharing infrastructure. Phase 1 adds the JumpTo operator (new functional operator with target palette). Phase 2 adds a fast-forward mode to startingComponent that replays all operators up to the "first" operator instantly, skipping delays and auto-resolving choices. Both use emitProgression(targetStepId) to land on the target step.

Tech Stack: Vue 2 (Options API), TypeScript, Vuex, Creative-Engine (vanilla TS), Creative-Composer (vanilla TS)


File Structure

Phase 1: JumpTo Operator

ActionFileRepoResponsibility
Createsrc/components/blocks/functional/JumpTo.tsCreative-EngineRuntime: reset state + jump to target step
Createsrc/pages/Chatbots/components/CavaiFlow/operators/JumpToOp.vueApplication-FrontendBuilder UI: target selector palette for picking destination
Modifysrc/utils/funcBlocks.tsCreative-EngineRegister JumpTo in the functional blocks map
Modifysrc/utils/_temp_buildercomponents.tsApplication-FrontendAdd to COMPONENTLIST, switchComponents
Modifysrc/pages/Chatbots/components/CavaiFlow/flowactors/OperatorBase.vueApplication-FrontendImport + register JumpToOp component
Modifysrc/pages/Chatbots/components/CavaiFlow/OperatorHelper.tsApplication-FrontendInitialize jumpTarget in operator props
Modifysrc/remapper/remapData.tsCreative-ComposerMap body.jumpTarget to comp.payload.jumpTarget
Modifysrc/assets/i18n/en.jsApplication-Frontendi18n keys for JumpTo

Phase 2: Flow Fast-Forward ("Content-Aware First")

ActionFileRepoResponsibility
Createsrc/logic-system/processors/flowFastForward.tsCreative-EngineFast-forward engine: replay operators instantly to accumulate state
Modifysrc/logic-system/processors/conversationFlow.tsCreative-EngineAdd fast-forward entry point, integrate with startflow
Modifysrc/pages/Chatbots/components/CavaiFlow/CavaiFlow.vueApplication-FrontendUI toggle for content-aware vs fresh-start first
Modifysrc/pages/Chatbots/components/CavaiFlow/DataHelper.tsApplication-FrontendAlways include all operators in step mapping when fast-forward is on
Modifysrc/remapper/computeComponents.tsCreative-ComposerPass fast-forward metadata to engine

Phase 1: JumpTo Operator

Task 1: Engine - Create JumpTo functional block

Files:

  • Create: Creative-Engine/src/components/blocks/functional/JumpTo.ts
  • Modify: Creative-Engine/src/utils/funcBlocks.ts

This is modeled after Restart.ts but uses emitProgression(targetStepId) instead of emitStartFlow().

Key design decision: JumpTo resets visual state (like Restart) before jumping. This is the safe default. If we later want a "soft jump" that preserves state, we add a boolean flag.

  • [ ] Step 1: Create JumpTo.ts
typescript
// Creative-Engine/src/components/blocks/functional/JumpTo.ts
import eventEmitter from '@/logic-system/eventEmitter'
import { ConversationFlow } from '@/logic-system/processors/conversationFlow'
import { LogicManager } from '@/logic-system/logicManager'
import { DimensionManager } from '@/style-engine/dimensionmanager/dimensionManager'
import { DataStore } from '@/services/dataStore'
import { resetCreative } from './Script'
import { nextTick } from 'vue'
import type { FlowComponentInterface } from '@/interfaces/components/FlowComponent/FlowComponentInterface'

export const JumpTo = {
  init(componentData: FlowComponentInterface[]) {
    const targetStepId = componentData[0]?.payload?.jumpTarget
    if (!targetStepId) return

    const fadeDuration = 120
    const bufferTimeout = 10
    const container = document.getElementById('creative-container')
    const baseBlock = container?.querySelector('[class*="base-block"]') as HTMLElement | null

    // Fade out children of base-block (same pattern as Restart)
    if (baseBlock) {
      for (const child of Array.from(baseBlock.children) as HTMLElement[]) {
        child.style.transition = `opacity ${fadeDuration}ms ease`
        child.style.opacity = '0'
      }
    }

    setTimeout(() => {
      resetCreative()
      DataStore.restartCount.value++
      DataStore.runtimeHidden.value = {}
      DataStore.currentBgClickUrl.value = ''
      DataStore.currentBgImageUrl.value = ''
      DataStore.videoState.shouldRestart.value = true
      ConversationFlow.flowQueue = []
      DataStore.liveFlow.length = 0
      DimensionManager.triggerEvent('update-scrollcontent-minht', { reset: true })
      DataStore.isRestarting = true

      void nextTick(() => {
        if (baseBlock) {
          for (const child of Array.from(baseBlock.children) as HTMLElement[]) {
            child.style.opacity = '0'
          }
        }

        setTimeout(() => {
          ConversationFlow.currStep = 0
          DataStore.isRestarting = false

          ConversationFlow.currSubscription = {
            ticker: false,
            dom: false,
            components: true,
            consent: true,
            textinput: true,
          }
          LogicManager.subscribeEvents(
            'process:conversationflow',
            ConversationFlow.currSubscription,
          )

          // Key difference from Restart: jump to target step, not start
          eventEmitter.emitProgression(Number(targetStepId))

          DataStore.videoState.shouldRestart.value = false

          requestAnimationFrame(() => {
            if (baseBlock) {
              for (const child of Array.from(baseBlock.children) as HTMLElement[]) {
                child.style.transition = `opacity ${fadeDuration}ms ease`
                child.style.opacity = '1'
                setTimeout(() => {
                  child.style.transition = ''
                }, fadeDuration)
              }
            }
          })
        }, bufferTimeout)
      })
    }, fadeDuration)
  },
}
  • [ ] Step 2: Register in funcBlocks.ts

Add to Creative-Engine/src/utils/funcBlocks.ts:

typescript
// #include when blocks have "JumpTo"
import { JumpTo } from '@/components/blocks/functional/JumpTo'
// #end

// In the funcBlocks map:
JumpTo: JumpTo.init,
  • [ ] Step 3: Verify engine builds

Run: cd Creative-Engine && npm run build Expected: Build succeeds

  • [ ] Step 4: Commit
bash
git add src/components/blocks/functional/JumpTo.ts src/utils/funcBlocks.ts
git commit -m "feat: add JumpTo functional block to engine"

Task 2: Composer - Map JumpTo payload

Files:

  • Modify: Creative-Composer/src/remapper/remapData.ts

The composer auto-resolves jumpTo -> JumpTo via getComponentType() default case (capitalize first letter). And getBlockType() will return 'functional' since jumpTo is not in NON_FUNC_COMPONENTS. So we only need to map the payload.

  • [ ] Step 1: Add jumpTo mapping to remapData.ts

In Creative-Composer/src/remapper/remapData.ts, add after the showHide mapping (line ~192):

typescript
} else if (opKey.includes('jumpTo') && body.jumpTarget) {
  comp.payload.jumpTarget = body.jumpTarget
}
  • [ ] Step 2: Verify composer builds

Run: cd Creative-Composer && npm run build Expected: Build succeeds

  • [ ] Step 3: Commit
bash
git add src/remapper/remapData.ts
git commit -m "feat: map jumpTo payload in composer"

Task 3: Frontend - Create JumpToOp builder component

Files:

  • Create: Application-Frontend/src/pages/Chatbots/components/CavaiFlow/operators/JumpToOp.vue

The JumpToOp component reuses TargetOpSelector with changeableComponentTypes set to all flow operator types (statements, answers, links, etc.) and changeableBlockTypes: [] (no blocks, only flow operators).

  • [ ] Step 1: Create JumpToOp.vue
vue
<template>
  <div class="jump-to-op" @mouseover="setParentDrag(false)" @mouseleave="setParentDrag(true)">
    <TargetOpSelector
      :target-abbrev-op-name="op.properties.body.jumpTarget"
      :operator-list="operatorList"
      :changeable-component-types="changeableComponentTypes"
      :changeable-block-types="[]"
      @change="onTargetChange"
    />
    <div class="jump-to-body">
      <Icon icon="forward" :size="20" />
      <span class="jump-to-label">{{ $t('flow.jumpTo.label') }}</span>
      <span v-if="op.properties.body.jumpTarget" class="jump-to-target">
        {{ op.properties.body.jumpTarget }}
      </span>
      <span v-else class="jump-to-placeholder">
        {{ $t('flow.jumpTo.noTarget') }}
      </span>
    </div>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import Icon from '@/components/common/Icon.vue'
import TargetOpSelector from './TargetOpSelector/TargetOpSelector.vue'

export default Vue.extend({
  name: 'JumpToOp',
  components: { Icon, TargetOpSelector },
  props: {
    op: {
      type: Object,
      default() {
        return {}
      },
    },
    opKey: {
      type: String,
      default: '',
    },
    operatorList: {
      type: Object,
      default() {
        return {}
      },
    },
    linkList: {
      type: Array,
      default() {
        return []
      },
    },
  },
  data() {
    return {
      changeableComponentTypes: [
        'statement',
        'answer',
        'link',
        'image',
        'slider',
        'shop',
        'text_input',
        'video',
        'delay',
        'tag',
        'css',
        'showHide',
        'restart',
        'reset',
        'changeText',
        'changeImage',
        'changeUrl',
        'changeVideo',
      ],
    }
  },
  methods: {
    onTargetChange(target: string) {
      this.$set(this.op.properties.body, 'jumpTarget', target)
    },
    setParentDrag(val: boolean) {
      this.$parent.setDraggable(val)
    },
    validateInput() {
      if (!this.op.properties.body.jumpTarget) {
        return 'Jump To operator must have a target selected'
      }
      return ''
    },
  },
})
</script>

<style lang="scss" scoped>
.jump-to-op {
  position: relative;
}

.jump-to-body {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  padding: 8px;
  color: $illustration-minus4;
}

.jump-to-label {
  font-size: 12px;
  font-weight: 400;
  line-height: 1.2;
}

.jump-to-target {
  font-size: 14px;
  font-weight: 600;
  text-transform: uppercase;
  color: $primary-default;
}

.jump-to-placeholder {
  font-size: 11px;
  font-style: italic;
  color: $secondary-font-color;
}
</style>
  • [ ] Step 2: Add i18n keys

In Application-Frontend/src/assets/i18n/en.js, add under the flow section (or create it if needed):

javascript
flow: {
  jumpTo: {
    label: 'Jump To',
    noTarget: 'No target',
  },
},
  • [ ] Step 3: Commit
bash
git add src/pages/Chatbots/components/CavaiFlow/operators/JumpToOp.vue src/assets/i18n/en.js
git commit -m "feat: add JumpToOp builder component with target palette"

Task 4: Frontend - Register JumpTo operator

Files:

  • Modify: Application-Frontend/src/utils/_temp_buildercomponents.ts

  • Modify: Application-Frontend/src/pages/Chatbots/components/CavaiFlow/flowactors/OperatorBase.vue

  • Modify: Application-Frontend/src/pages/Chatbots/components/CavaiFlow/OperatorHelper.ts

  • [ ] Step 1: Add to COMPONENTLIST in _temp_buildercomponents.ts

typescript
// After the Restart entry in COMPONENTLIST:
{ title: 'Jump To', type: 'jumpTo', apiKey: 'JumpTo' },
  • [ ] Step 2: Add to switchComponents
typescript
export const switchComponents = ['reset', 'delay', 'tag', 'css', 'resetChanges', 'showHide', 'restart', 'jumpTo', 'libraryScript']
export const switchComponentsTitles = ['Wipe', 'Delay', 'JS', 'CSS', 'Reset', 'Show/Hide', 'Restart', 'Jump To', 'Library Script']

Note: Do NOT add to FIRSTCOMPONENTLIST (JumpTo as first operator makes no sense).

  • [ ] Step 3: Import and register in OperatorBase.vue

Add import:

typescript
import JumpToOp from '../operators/JumpToOp.vue'

Add to components object:

typescript
components: {
  // ... existing
  JumpToOp,
}
  • [ ] Step 4: Initialize jumpTarget in OperatorHelper.ts

In getOperatorProps(), add:

typescript
} else if (opType === 'jumpTo') {
  props.body.jumpTarget = null
}
  • [ ] Step 5: Verify frontend builds

Run: cd Application-Frontend && npm run build Expected: Build succeeds

  • [ ] Step 6: Commit
bash
git add src/utils/_temp_buildercomponents.ts \
  src/pages/Chatbots/components/CavaiFlow/flowactors/OperatorBase.vue \
  src/pages/Chatbots/components/CavaiFlow/OperatorHelper.ts
git commit -m "feat: register JumpTo operator in builder"

Task 5: Manual test - JumpTo end-to-end

  • [ ] Step 1: Create a test creative with a flow

Build a simple flow: Message -> Choice -> Message (branch A) / Message (branch B), add a JumpTo after branch A that targets the first Message.

  • [ ] Step 2: Verify the target palette shows only flow operators

Open the JumpTo operator, click the target palette. Should show abbreviations for all operators in the flow (s1, a1, s2, s3, etc.) but no block abbreviations.

  • [ ] Step 3: Preview and verify the jump works

Preview the creative, go through branch A, verify that the JumpTo resets and lands on the first Message.

  • [ ] Step 4: Verify save/load roundtrip

Save the creative, reload, verify the JumpTo operator still has its target set.


Phase 2: Flow Fast-Forward ("Content-Aware First")

Note: Phase 2 depends on Phase 1 being complete and tested. The fast-forward feature is more complex and can be a separate branch.

Task 6: Engine - Create flowFastForward processor

Files:

  • Create: Creative-Engine/src/logic-system/processors/flowFastForward.ts

This is the core of the feature. It replays all operators from step 1 up to a target step, executing them instantly:

  • Functional operators (CSS, JS, ShowHide, Change*) execute normally (they're already synchronous)

  • Delay operators are skipped (customDelay = 0)

  • Visual operators (Message, Choice, etc.) are skipped but their side effects (animation triggers) are noted

  • Choice operators: auto-resolve by finding which answer leads toward the target step

  • [ ] Step 1: Create flowFastForward.ts

typescript
// Creative-Engine/src/logic-system/processors/flowFastForward.ts
import { DataStore } from '@/services/dataStore'
import { funcBlocks } from '@/utils/funcBlocks'
import type { FlowComponentInterface } from '@/interfaces/components/FlowComponent/FlowComponentInterface'

/**
 * Finds a path from startStep to targetStep through the component graph.
 * Returns an ordered array of step IDs, or null if no path exists.
 */
function findPathToTarget(
  components: Record<number, FlowComponentInterface[]>,
  startStep: number,
  targetStep: number,
): number[] | null {
  const visited = new Set<number>()
  const queue: { step: number, path: number[] }[] = [
    { step: startStep, path: [startStep] },
  ]

  while (queue.length > 0) {
    const { step, path } = queue.shift()!
    if (step === targetStep) return path
    if (visited.has(step)) continue
    visited.add(step)

    const compSet = components[step]
    if (!compSet || compSet.length === 0) continue

    // Collect all possible next steps
    const nextSteps = new Set<number>()
    for (const comp of compSet) {
      if (comp.nextStepId && comp.nextStepId > 0) {
        nextSteps.add(comp.nextStepId)
      }
    }

    for (const next of nextSteps) {
      if (!visited.has(next)) {
        queue.push({ step: next, path: [...path, next] })
      }
    }
  }

  return null
}

/**
 * Fast-forwards through the flow from step 1 to targetStep,
 * executing all functional operators instantly and skipping
 * visual operators and delays.
 *
 * Returns true if fast-forward succeeded, false if path not found.
 */
export function fastForwardToStep(targetStep: number): boolean {
  const components = DataStore.componentData.components
  const path = findPathToTarget(components, 1, targetStep)

  if (!path) {
    console.warn('[flowFastForward] No path found to target step', targetStep)
    return false
  }

  // Execute each step along the path (except the target itself)
  for (const step of path) {
    if (step === targetStep) break

    const compSet = components[step]
    if (!compSet || compSet.length === 0) continue

    const firstComp = compSet[0]

    // Only execute functional operators
    if (firstComp.blockType === 'functional') {
      // Skip delays entirely
      if (firstComp.type === 'Delay') continue

      // Skip Restart/JumpTo (would cause loops)
      if (firstComp.type === 'Restart' || firstComp.type === 'JumpTo') continue

      // Execute the functional operator
      const handler = funcBlocks[firstComp.type]
      if (handler) {
        handler(compSet)
        DataStore.customDelay = 0 // Reset any delay set by the operator
      }
    }
    // Visual operators: skip (don't add to liveFlow)
  }

  return true
}
  • [ ] Step 2: Commit
bash
git add src/logic-system/processors/flowFastForward.ts
git commit -m "feat: add flowFastForward processor for content-aware first"

Task 7: Engine - Integrate fast-forward with conversationFlow

Files:

  • Modify: Creative-Engine/src/logic-system/processors/conversationFlow.ts

When the flow starts and a fast-forward target is set, run the fast-forward processor before beginning normal flow execution.

  • [ ] Step 1: Add fast-forward integration to conversationFlow.ts

In handleComponentsEvent, modify the startflow case:

typescript
case 'startflow':
  if (DataStore.fastForwardTarget?.value) {
    const targetStep = DataStore.fastForwardTarget.value
    DataStore.fastForwardTarget.value = 0 // Clear to prevent re-triggering
    const success = fastForwardToStep(targetStep)
    if (success) {
      // Jump directly to the target step
      this.currStep = targetStep - 1 // -1 because addComponentsToFlow increments
      this.addComponentsToFlow({})
      break
    }
  }
  this.addComponentsToFlow({})
  break

This requires adding fastForwardTarget to DataStore (a ref(0) reactive value). When set to a non-zero step ID, the flow will fast-forward to that step on start.

  • [ ] Step 2: Add fastForwardTarget to DataStore

Add to DataStore's reactive refs:

typescript
fastForwardTarget: ref(0),
  • [ ] Step 3: Commit
bash
git add src/logic-system/processors/conversationFlow.ts src/services/dataStore.ts
git commit -m "feat: integrate fast-forward with conversation flow startup"

Task 8: Frontend + Composer - Add content-aware first UI and data path

Files:

  • Modify: Application-Frontend/src/pages/Chatbots/components/CavaiFlow/CavaiFlow.vue
  • Modify: Application-Frontend/src/pages/Chatbots/components/CavaiFlow/DataHelper.ts
  • Modify: Creative-Composer/src/remapper/computeComponents.ts

This task adds a toggle in the "Make First" context menu or operator properties that switches between "Fresh Start" (current behavior) and "Content-Aware" (fast-forward) mode.

  • [ ] Step 1: Add firstMode property to operator data

In CavaiFlow.vue, modify the makeFirst() method to also set a firstMode property:

typescript
// On the operator marked as first:
Vue.set(props, 'firstMode', props.firstMode || 'fresh')
// 'fresh' = current behavior (skip everything before)
// 'contentAware' = fast-forward through everything before
  • [ ] Step 2: Add context menu option to toggle firstMode

In the context menu (where "Make First" appears), add a sub-option:

  • "Fresh Start" (default, current behavior)
  • "Content-Aware" (fast-forward mode)

Only visible when the operator is already the starting component.

  • [ ] Step 3: Modify DataHelper.mapStepsAndIDs for content-aware mode

When firstMode === 'contentAware', the step mapping should:

  1. Start from the actual first operator in the flow (the topmost connected operator), NOT from the startingComponent
  2. Mark the startingComponent's step ID as the fastForwardTarget
  3. Pass this metadata through to the composer/engine
typescript
// In mapStepsAndIDs:
const startOp = opKeyList.find((el) => opList[el].properties.startingComponent === 'true')
const firstMode = opList[startOp]?.properties.firstMode || 'fresh'

if (firstMode === 'contentAware') {
  // Find the true root operator (first operator that nothing links TO, or lowest curStepId)
  // Map from the root, but mark startOp's step as fastForwardTarget
  // ... (implementation depends on how root is determined)
}
  • [ ] Step 4: Pass fastForwardTarget through composer to engine

In computeComponents.ts, when firstMode === 'contentAware', set a metadata field that the engine reads during initialization to populate DataStore.fastForwardTarget.

  • [ ] Step 5: Add i18n keys
javascript
flow: {
  firstMode: {
    fresh: 'Fresh Start',
    contentAware: 'Content-Aware',
  },
},
  • [ ] Step 6: Commit
bash
git add -A
git commit -m "feat: add content-aware first mode with UI toggle"

Task 9: Manual test - Fast-forward end-to-end

  • [ ] Step 1: Create a test creative

Build a flow with CSS operators early on that move elements, then set "first" on an operator later in the flow.

  • [ ] Step 2: Test fresh start mode

Set first mode to "Fresh Start". Preview. Verify elements are in their default positions (CSS not applied). This is the current behavior.

  • [ ] Step 3: Test content-aware mode

Toggle to "Content-Aware". Preview. Verify:

  • CSS changes from before the first operator are applied

  • ShowHide states are correct

  • No delays are visible

  • The flow starts from the first operator with correct accumulated state

  • [ ] Step 4: Test with choices in the path

Build a flow with a Choice operator before the "first" operator. Verify the pathfinding auto-resolves through the correct answer to reach the first operator.


Open Questions (to resolve during implementation)

  1. Naming: "Content-Aware" vs "Fresh Start"? Or something else? This is a UX decision. Options:

    • "Content-Aware" / "Fresh Start"
    • "With History" / "Clean Start"
    • "Resume" / "Skip"
    • Could also be a single toggle: "Apply prior state" checkbox
  2. Choice auto-resolution: When multiple paths lead to the target, which one to take? Options:

    • First path found (BFS - shortest path)
    • Always take position 1 (first answer)
    • Let the user specify which answer to auto-select (most flexible, most complex)
  3. Should JumpTo support soft-jump? (Jump without resetting state). This would be a separate property on the JumpTo operator. Defer to Phase 3 if needed.

  4. OnEvent operators: When the OnEvent system is eventually implemented, fast-forward will need to handle event subscriptions that would have been registered during fast-forwarded steps. Not a concern now, but worth noting.

Internal documentation