Skip to content

SliderV2 Video Sub-Block 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: Fix two SliderV2 bugs (video upload race condition, scroll-mode backward animation) and add video as a reusable sub-block with per-slide hide/override and video-based autoplay timing.

Architecture: Video becomes a regular slider sub-block like text/graphic. CreativeVideoBlock renders through the existing sub-block loop. Per-slide overrides and a new "hidden" flag control visibility. Video progress drives timer dots automatically.

Tech Stack: Vue 2 + Vuex (Application-Frontend), Vue 3 (Creative-Engine), lodash

Repos:

  • AF = /Users/nicolay/CavaiProduct/Application-Frontend (branch: add-slider-v2)
  • CE = /Users/nicolay/CavaiProduct/Creative-Engine (branch: add-slider-v2)

Spec: AF/docs/superpowers/specs/2026-03-13-sliderv2-video-subblock-design.md


Chunk 1: Bug Fixes (AF + CE)

Task 1: Fix video filename/thumbnail race condition

Files:

  • Modify: AF/src/pages/Chatbots/components/BuilderVisuals/Configuration/components/SlideSection.vue:284-293

  • [ ] Step 1: Replace onSlideVideoUploaded with batched version

Replace lines 284-288:

js
onSlideVideoUploaded({ uid, fileName }) {
  const slides = cloneDeep(this.blockData.slides)
  set(slides, `${this.slideIndex}.videoStreamId`, uid)
  set(slides, `${this.slideIndex}.videoFileName`, fileName)
  set(slides, `${this.slideIndex}.videoUrl`, `https://delivery-6.cavai.com/${uid}/original`)
  this.$emit('update:slides', slides)
},
  • [ ] Step 2: Replace removeSlideVideo with batched version

Replace lines 289-293:

js
removeSlideVideo() {
  const slides = cloneDeep(this.blockData.slides)
  const slide = slides[this.slideIndex]
  delete slide.videoStreamId
  delete slide.videoFileName
  delete slide.videoUrl
  this.$emit('update:slides', slides)
},
  • [ ] Step 3: Verify manually

Upload a video to a slide in the builder. Confirm filename and thumbnail preview persist in the config panel after upload completes.

  • [ ] Step 4: Commit
bash
cd /Users/nicolay/CavaiProduct/Application-Frontend
git add src/pages/Chatbots/components/BuilderVisuals/Configuration/components/SlideSection.vue
git commit -m "fix: batch video upload overrides to prevent race condition"

Task 2: Fix scroll-mode backward animation

Files:

  • Modify: CE/src/components/creative/CreativeSliderBlockV2/CreativeSliderBlockV2.vue:350-380 (data), ~1589-1598 (scrollToSlide)

  • [ ] Step 1: Add _scrollSettleTimeout to data

In data() (~line 350), add after videoProgress: 0, (line 367):

js
_scrollSettleTimeout: 0,
  • [ ] Step 2: Update scroll-mode branch in scrollToSlide

Replace the scroll-mode fallback block (~lines 1589-1598):

js
// Before:
const track = this.$refs.track as HTMLElement
if (!track) return
const slideEl = track.children[index] as HTMLElement
if (!slideEl) return
if (fromAutoplay) this.isAutoplayScrolling = true
if (this.isVertical) {
  track.scrollTo({ top: slideEl.offsetTop, behavior: 'smooth' })
} else {
  track.scrollTo({ left: slideEl.offsetLeft, behavior: 'smooth' })
}

With:

js
const track = this.$refs.track as HTMLElement
if (!track) return
const slideEl = track.children[index] as HTMLElement
if (!slideEl) return
if (fromAutoplay) this.isAutoplayScrolling = true
clearTimeout(this._scrollSettleTimeout)
track.style.scrollSnapType = 'none'
if (this.isVertical) {
  track.scrollTo({ top: slideEl.offsetTop, behavior: 'smooth' })
} else {
  track.scrollTo({ left: slideEl.offsetLeft, behavior: 'smooth' })
}
this._scrollSettleTimeout = setTimeout(() => {
  track.style.scrollSnapType = this.isVertical ? 'y mandatory' : 'x mandatory'
}, 600)
  • [ ] Step 3: Verify manually

In scroll mode (default transition), click left arrow to go to a previous slide. Confirm smooth animation instead of instant jump. Click rapidly multiple times to confirm no glitches.

  • [ ] Step 4: Commit
bash
cd /Users/nicolay/CavaiProduct/Creative-Engine
git add src/components/creative/CreativeSliderBlockV2/CreativeSliderBlockV2.vue
git commit -m "fix: disable scroll-snap during programmatic scroll for smooth backward animation"

Chunk 2: Video as Sub-Block — AF Config (SlideSection + defaults)

Task 3: Add VIDEO to allowed sub-blocks

Files:

  • Modify: AF/src/pages/Chatbots/components/BuilderVisuals/Blocks/data/defaults.ts:912

  • [ ] Step 1: Add BLOCKS.VIDEO to sliderV2 allowedSubBlocks

Change line 912 from:

ts
allowedSubBlocks: [BLOCKS.TEXT, BLOCKS.BUTTON, BLOCKS.GRAPHIC, BLOCKS.HTML],

To:

ts
allowedSubBlocks: [BLOCKS.TEXT, BLOCKS.BUTTON, BLOCKS.GRAPHIC, BLOCKS.HTML, BLOCKS.VIDEO],
  • [ ] Step 2: Commit
bash
cd /Users/nicolay/CavaiProduct/Application-Frontend
git add src/pages/Chatbots/components/BuilderVisuals/Blocks/data/defaults.ts
git commit -m "feat: allow video block as SliderV2 sub-block"

Task 4: Extend SlideSection override system for video

Files:

  • Modify: AF/src/pages/Chatbots/components/BuilderVisuals/Configuration/components/SlideSection.vue

  • [ ] Step 1: Fix component name

Change line 186 from:

js
name: 'BoxShadowSection',

To:

js
name: 'SlideSection',
  • [ ] Step 2: Add 'video' case to getOverrideKey

In getOverrideKey (line 240-254), add after the 'html' case:

js
case 'video':
  return 'video'
  • [ ] Step 3: Update hasOverride to detect hidden overrides

Replace lines 236-238:

js
hasOverride(slide, subBlock) {
  const key = this.getOverrideKey(subBlock)
  if (!(subBlock.blockName in slide)) return false
  const override = slide[subBlock.blockName]
  return (override && typeof override === 'object')
    && (key in override || override.hidden === true)
},
  • [ ] Step 4: Add startEditing handler for video

In startEditing (line 211-227), add after the 'html' case:

js
case 'video':
  this.updateSlideOverride(`${subBlock.blockName}.video`, subBlock.video)
  break
  • [ ] Step 5: Add video override template section

After the v-if="getOverrideKey(subBlock) === 'html'" div (line 154-164), add:

vue
<div v-if="getOverrideKey(subBlock) === 'video'" class="slide-block-video-settings">
  <div class="slide-video-uploader">
    <ImagePreview
      :image="getVideoPreviewUrl(slide, subBlock)"
      class="slide-video-preview"
    />
    <FileUploader
      class="slide-video-input"
      :placeholder="`— ${$t('general.upload_video')} —`"
      :initial-file-name="getVideoOverrideFileName(slide, subBlock)"
      file-type="video/*"
      @video-uploaded="onVideoOverrideUploaded(subBlock.blockName, $event)"
      @file-removed="onVideoOverrideRemoved(subBlock.blockName)"
    />
  </div>
</div>
  • [ ] Step 6: Add video helper methods

Add these methods:

js
getVideoPreviewUrl(slide, subBlock) {
  const override = slide[subBlock.blockName]?.video
  const streamId = override?.streamId || subBlock.video?.streamId
  if (!streamId) return
  return `https://delivery-6.cavai.com/${streamId}/preview.webp`
},
getVideoOverrideFileName(slide, subBlock) {
  const override = slide[subBlock.blockName]?.video
  return override?.fileName || subBlock.video?.fileName || ''
},
onVideoOverrideUploaded(blockName, { uid, fileName }) {
  this.updateSlideOverride(`${blockName}.video`, {
    streamId: uid,
    fileName,
    url: `https://delivery-6.cavai.com/${uid}/original`,
  })
},
onVideoOverrideRemoved(blockName) {
  this.updateSlideOverride(`${blockName}.video`, null)
},
  • [ ] Step 7: Remove hardcoded video section

Remove lines 15-36 (the <OptionRow> with video upload). Remove old methods: onSlideVideoUploaded, removeSlideVideo. Remove computed: slideVideoPreviewUrl.

  • [ ] Step 8: Add video-specific styling

Add in the <style scoped> section:

scss
.slide-block-video-settings {
  min-height: 60px;
}
  • [ ] Step 9: Commit
bash
cd /Users/nicolay/CavaiProduct/Application-Frontend
git add src/pages/Chatbots/components/BuilderVisuals/Configuration/components/SlideSection.vue
git commit -m "feat: extend SlideSection override system for video sub-blocks"

Task 5: Add per-slide hidden toggle

Files:

  • Modify: AF/src/pages/Chatbots/components/BuilderVisuals/Configuration/components/SlideSection.vue

This is the "hide on this slide" mechanism, reusable for all block types.

  • [ ] Step 1: Add hidden toggle button in slider-block-header

In the slider-block-header div (line 55-97), after the existing reset/edit buttons and before .block-classname-indicator, add a hide toggle:

vue
<button
  v-if="!hasOverride(slide, subBlock)"
  class="toggle-hidden"
  :title="isHiddenOnSlide(slide, subBlock) ? 'Show on this slide' : 'Hide on this slide'"
  @click="toggleHiddenOnSlide(subBlock)"
>
  <Icon
    :icon="isHiddenOnSlide(slide, subBlock) ? 'eye-off' : 'eye'"
    color="black"
    :size="14"
  />
</button>
  • [ ] Step 2: Add helper methods
js
isHiddenOnSlide(slide, subBlock) {
  return slide[subBlock.blockName]?.hidden === true
},
toggleHiddenOnSlide(subBlock) {
  if (this.isHiddenOnSlide(this.slide, subBlock)) {
    this.updateSlideOverride(`${subBlock.blockName}.hidden`, null)
  } else {
    this.updateSlideOverride(`${subBlock.blockName}.hidden`, true)
  }
},
  • [ ] Step 3: Update dim class to include hidden state

In template (line 44-46), update the dim condition:

js
dim: !hasOverride(slide, subBlock) && !activeEditors[subBlock.blockName] && !isHiddenOnSlide(slide, subBlock),

And add a new hidden-on-slide class:

js
'hidden-on-slide': isHiddenOnSlide(slide, subBlock),
  • [ ] Step 4: Add hidden styling
scss
&.hidden-on-slide {
  opacity: 0.4;

  &:before {
    opacity: 1;
  }
}

.toggle-hidden {
  width: 28px;
  height: 28px;
  z-index: 10;
  position: absolute;
  right: 60px;
  top: -5px;
  border-radius: 2px;
  opacity: 0.5;
  cursor: pointer;

  &:hover {
    opacity: 0.7;
    background-color: rgba(0, 0, 0, 0.08);
  }
}
  • [ ] Step 5: Commit
bash
cd /Users/nicolay/CavaiProduct/Application-Frontend
git add src/pages/Chatbots/components/BuilderVisuals/Configuration/components/SlideSection.vue
git commit -m "feat: add per-slide hidden toggle for any sub-block type"

Chunk 3: Video as Sub-Block — CE Rendering

Task 6: Register CreativeVideoBlock and add helpers

Files:

  • Modify: CE/src/components/creative/CreativeSliderBlockV2/CreativeSliderBlockV2.vue

  • [ ] Step 1: Import and register CreativeVideoBlock

Add import after the existing block imports (~line 330):

ts
import CreativeVideoBlock from '@/components/creative/CreativeVideoBlock/CreativeVideoBlock.vue'

Add to components object (line 341-346):

ts
components: {
  CreativeTextBlock,
  CreativeGraphicBlock,
  CreativeHtmlBlock,
  CreativeButtonBlock,
  CreativeVideoBlock,
},
  • [ ] Step 2: Add isHiddenOnSlide method

Add to methods:

ts
isHiddenOnSlide(slide: any, subBlock: any): boolean {
  return slide[subBlock.blockName]?.hidden === true
},
  • [ ] Step 3: Add subBlockExtraProps method

Add to methods:

ts
subBlockExtraProps(subBlock: any, slideIndex: number) {
  if (!subBlock.blockName.startsWith('video')) return {}
  return {
    creativeBodySize: this.slideSize,
    order: 0,
    instanceIndex: slideIndex,
    lastStartedVideoIndex: this.activeIndex,
  }
},
  • [ ] Step 4: Add slideSize computed

Add to computed:

ts
slideSize(): { width: number; height: number } {
  const track = this.$refs.track as HTMLElement
  if (!track?.children?.[0]) return { width: 0, height: 0 }
  const slide = track.children[0] as HTMLElement
  return { width: slide.clientWidth, height: slide.clientHeight }
},
  • [ ] Step 5: Add mergedSubBlock method

Add to methods (replaces inline spread in template):

ts
mergedSubBlock(slide: any, subBlock: any) {
  return {
    ...subBlock,
    ...(subBlock.blockName in slide ? slide[subBlock.blockName] : {}),
  }
},
  • [ ] Step 6: Commit
bash
cd /Users/nicolay/CavaiProduct/Creative-Engine
git add src/components/creative/CreativeSliderBlockV2/CreativeSliderBlockV2.vue
git commit -m "feat: register CreativeVideoBlock and add slider sub-block helpers"

Task 7: Update sub-block rendering in all 4 template sections

Files:

  • Modify: CE/src/components/creative/CreativeSliderBlockV2/CreativeSliderBlockV2.vue

Each template section (cube, carousel, slide, default) needs the same changes:

  1. Remove the hardcoded <video> element
  2. Remove the !(slideVideoUrl(...)) condition from sub-block v-if
  3. Add isHiddenOnSlide check and v-bind="subBlockExtraProps(...)"
  4. Use mergedSubBlock helper
  • [ ] Step 1: Update cube mode template (~lines 55-81)

Remove the <video> element (lines 56-67). Replace the sub-block loop (lines 68-80):

vue
<template v-for="(subBlock, subBlockKey) in subBlocks">
  <component
    :is="blockTypeToVisualBlockComponentName(subBlock.blockType)"
    v-if="!subBlock.hidden && !isHiddenOnSlide(visibleSlides[faceIndex], subBlock)"
    :key="subBlockKey"
    :block="mergedSubBlock(visibleSlides[faceIndex], subBlock)"
    :feed-slide-data="feedData && feedData[faceIndex]"
    :namespace-class="`cavai-slider-slide-${faceIndex}`"
    v-bind="subBlockExtraProps(subBlock, faceIndex)"
    @video-timeupdate="onSubBlockVideoTimeUpdate($event, faceIndex)"
    @video-ended="onSubBlockVideoEnded(faceIndex)"
  />
</template>
  • [ ] Step 2: Update carousel mode template (~lines 97-121)

Same pattern: remove <video>, update <component> with isHiddenOnSlide, mergedSubBlock, subBlockExtraProps, and video events using slide index i.

  • [ ] Step 3: Update slide mode template (~lines 140-164)

Same pattern using slide index i.

  • [ ] Step 4: Update default mode template (~lines 186-210)

Same pattern using slide index i.

  • [ ] Step 5: Remove old video code

Remove slideVideoUrl() method. Remove pauseSlideVideo() and playSlideVideo() methods. Remove the video-related lines from the activeIndex watcher that call pauseSlideVideo/playSlideVideo (~line 1117-1118). Remove video-specific styles from computed styles (~lines 1100-1108).

  • [ ] Step 6: Commit
bash
cd /Users/nicolay/CavaiProduct/Creative-Engine
git add src/components/creative/CreativeSliderBlockV2/CreativeSliderBlockV2.vue
git commit -m "feat: render video through sub-block loop, remove hardcoded video elements"

Chunk 4: Video-Based Autoplay Timing (CE)

Task 8: Add video event emissions to CreativeVideoBlock

Files:

  • Modify: CE/src/components/creative/CreativeVideoBlock/CreativeVideoBlock.vue

  • [ ] Step 1: Add $emit for video-timeupdate

In the videoProgress method (~line 557), after computing currentTime and duration (line 564), add:

ts
this.$emit('video-timeupdate', { currentTime, duration })
  • [ ] Step 2: Add $emit for video-ended

In the onEnded method (~line 536), at the start (before this.plays += 1), add:

ts
this.$emit('video-ended')
  • [ ] Step 3: Commit
bash
cd /Users/nicolay/CavaiProduct/Creative-Engine
git add src/components/creative/CreativeVideoBlock/CreativeVideoBlock.vue
git commit -m "feat: emit video-timeupdate and video-ended events from CreativeVideoBlock"

Task 9: Wire video events and timer dots in SliderV2

Files:

  • Modify: CE/src/components/creative/CreativeSliderBlockV2/CreativeSliderBlockV2.vue

  • [ ] Step 1: Update activeSlideHasVideo computed

Replace existing activeSlideHasVideo (or add if missing):

ts
activeSlideHasVideo(): boolean {
  const slide = this.visibleSlides[this.activeIndex]
  if (!slide) return false
  return (this.subBlocks as any[]).some((b: any) =>
    b.blockName.startsWith('video') && !b.hidden && !this.isHiddenOnSlide(slide, b)
  )
},
  • [ ] Step 2: Add video event handlers

Add to methods:

ts
onSubBlockVideoTimeUpdate(event: { currentTime: number; duration: number }, slideIndex: number) {
  if (slideIndex !== this.activeIndex) return
  if (event.duration) {
    this.videoProgress = event.currentTime / event.duration
  }
},

onSubBlockVideoEnded(slideIndex: number) {
  this.videoProgress = 0
  if (slideIndex !== this.activeIndex) return
  const next = this.typedBlock.loop
    ? (this.activeIndex + 1) % this.visibleSlides.length
    : Math.min(this.activeIndex + 1, this.visibleSlides.length - 1)
  this.scrollToSlide(next, true)
},
  • [ ] Step 3: Add effectiveTimerProgress computed
ts
effectiveTimerProgress(): number {
  return this.activeSlideHasVideo ? this.videoProgress : this.timerProgress
},
  • [ ] Step 4: Update timer template bindings

In template, change the line fill binding (~line 251) from:

vue
:style="{ width: (timerProgress * 100) + '%' }"

To:

vue
:style="{ width: (effectiveTimerProgress * 100) + '%' }"
  • [ ] Step 5: Update timerDashOffset computed

Change (~line 470-472) from:

ts
timerDashOffset(): number {
  return this.timerCircumference * (1 - this.timerProgress)
},

To:

ts
timerDashOffset(): number {
  return this.timerCircumference * (1 - this.effectiveTimerProgress)
},
  • [ ] Step 6: Skip rAF timer for video slides

In restartTimerAnimation (~line 1776), add early return after existing checks:

ts
restartTimerAnimation() {
  this.stopTimerAnimation()
  if (!this.showTimerDots || !this.typedBlock.autoplay) return
  if (this.activeSlideHasVideo) return // video drives progress via videoProgress
  // ... rest of existing rAF code unchanged
  • [ ] Step 7: Remove old video handlers

Remove old onVideoTimeUpdate and onVideoEnded methods (they referenced the hardcoded <video> elements, now replaced by onSubBlockVideoTimeUpdate and onSubBlockVideoEnded).

  • [ ] Step 8: Commit
bash
cd /Users/nicolay/CavaiProduct/Creative-Engine
git add src/components/creative/CreativeSliderBlockV2/CreativeSliderBlockV2.vue
git commit -m "feat: video-based autoplay timing with timer dot integration"

Chunk 5: Manual Verification & Cleanup

Task 10: End-to-end verification

  • [ ] Step 1: Verify video sub-block in AF
  1. Open a SliderV2 creative in the builder
  2. Add a video sub-block via the add-block tool
  3. Upload a video to the video block
  4. Switch to the Slides tab — confirm video appears in all slide overrides
  5. Override video on slide 2 — upload a different video
  6. Hide video on slide 3 using the eye toggle
  7. Confirm the creative preview shows: slide 1 = default video, slide 2 = overridden video, slide 3 = no video
  • [ ] Step 2: Verify scroll-mode backward animation
  1. In scroll mode (default transition), navigate forward then backward via arrows
  2. Confirm smooth animation in both directions
  3. Rapid-click the back arrow multiple times — confirm no glitches
  • [ ] Step 3: Verify video-based timer dots
  1. Enable autoplay with timer dots
  2. On a slide with video: timer dot progress follows video playback
  3. On a slide without video (hidden): timer dot uses fixed interval
  4. Video ending auto-advances to next slide
  • [ ] Step 4: Build check
bash
cd /Users/nicolay/CavaiProduct/Creative-Engine && npm run build:library 2>&1 | tail -5
cd /Users/nicolay/CavaiProduct/Application-Frontend && npm run build 2>&1 | tail -5
  • [ ] Step 5: Final commit if any cleanup needed

Internal documentation