Appearance
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
onSlideVideoUploadedwith 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)
},1
2
3
4
5
6
7
2
3
4
5
6
7
- [ ] Step 2: Replace
removeSlideVideowith 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)
},1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
- [ ] 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"1
2
3
2
3
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
_scrollSettleTimeoutto data
In data() (~line 350), add after videoProgress: 0, (line 367):
js
_scrollSettleTimeout: 0,1
- [ ] 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' })
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
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)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- [ ] 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"1
2
3
2
3
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],1
To:
ts
allowedSubBlocks: [BLOCKS.TEXT, BLOCKS.BUTTON, BLOCKS.GRAPHIC, BLOCKS.HTML, BLOCKS.VIDEO],1
- [ ] 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"1
2
3
2
3
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',1
To:
js
name: 'SlideSection',1
- [ ] Step 2: Add
'video'case togetOverrideKey
In getOverrideKey (line 240-254), add after the 'html' case:
js
case 'video':
return 'video'1
2
2
- [ ] Step 3: Update
hasOverrideto 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)
},1
2
3
4
5
6
7
2
3
4
5
6
7
- [ ] Step 4: Add
startEditinghandler for video
In startEditing (line 211-227), add after the 'html' case:
js
case 'video':
this.updateSlideOverride(`${subBlock.blockName}.video`, subBlock.video)
break1
2
3
2
3
- [ ] 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>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- [ ] 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)
},1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- [ ] 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;
}1
2
3
2
3
- [ ] 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"1
2
3
2
3
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>1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
- [ ] 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)
}
},1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
- [ ] 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),1
And add a new hidden-on-slide class:
js
'hidden-on-slide': isHiddenOnSlide(slide, subBlock),1
- [ ] 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);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- [ ] 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"1
2
3
2
3
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'1
Add to components object (line 341-346):
ts
components: {
CreativeTextBlock,
CreativeGraphicBlock,
CreativeHtmlBlock,
CreativeButtonBlock,
CreativeVideoBlock,
},1
2
3
4
5
6
7
2
3
4
5
6
7
- [ ] Step 2: Add
isHiddenOnSlidemethod
Add to methods:
ts
isHiddenOnSlide(slide: any, subBlock: any): boolean {
return slide[subBlock.blockName]?.hidden === true
},1
2
3
2
3
- [ ] Step 3: Add
subBlockExtraPropsmethod
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,
}
},1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
- [ ] Step 4: Add
slideSizecomputed
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 }
},1
2
3
4
5
6
2
3
4
5
6
- [ ] Step 5: Add
mergedSubBlockmethod
Add to methods (replaces inline spread in template):
ts
mergedSubBlock(slide: any, subBlock: any) {
return {
...subBlock,
...(subBlock.blockName in slide ? slide[subBlock.blockName] : {}),
}
},1
2
3
4
5
6
2
3
4
5
6
- [ ] 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"1
2
3
2
3
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:
- Remove the hardcoded
<video>element - Remove the
!(slideVideoUrl(...))condition from sub-block v-if - Add
isHiddenOnSlidecheck andv-bind="subBlockExtraProps(...)" - Use
mergedSubBlockhelper
- [ ] 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>1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
- [ ] 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"1
2
3
2
3
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
$emitfor video-timeupdate
In the videoProgress method (~line 557), after computing currentTime and duration (line 564), add:
ts
this.$emit('video-timeupdate', { currentTime, duration })1
- [ ] Step 2: Add
$emitfor video-ended
In the onEnded method (~line 536), at the start (before this.plays += 1), add:
ts
this.$emit('video-ended')1
- [ ] 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"1
2
3
2
3
Task 9: Wire video events and timer dots in SliderV2
Files:
Modify:
CE/src/components/creative/CreativeSliderBlockV2/CreativeSliderBlockV2.vue[ ] Step 1: Update
activeSlideHasVideocomputed
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)
)
},1
2
3
4
5
6
7
2
3
4
5
6
7
- [ ] 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)
},1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- [ ] Step 3: Add
effectiveTimerProgresscomputed
ts
effectiveTimerProgress(): number {
return this.activeSlideHasVideo ? this.videoProgress : this.timerProgress
},1
2
3
2
3
- [ ] Step 4: Update timer template bindings
In template, change the line fill binding (~line 251) from:
vue
:style="{ width: (timerProgress * 100) + '%' }"1
To:
vue
:style="{ width: (effectiveTimerProgress * 100) + '%' }"1
- [ ] Step 5: Update
timerDashOffsetcomputed
Change (~line 470-472) from:
ts
timerDashOffset(): number {
return this.timerCircumference * (1 - this.timerProgress)
},1
2
3
2
3
To:
ts
timerDashOffset(): number {
return this.timerCircumference * (1 - this.effectiveTimerProgress)
},1
2
3
2
3
- [ ] 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 unchanged1
2
3
4
5
2
3
4
5
- [ ] 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"1
2
3
2
3
Chunk 5: Manual Verification & Cleanup
Task 10: End-to-end verification
- [ ] Step 1: Verify video sub-block in AF
- Open a SliderV2 creative in the builder
- Add a video sub-block via the add-block tool
- Upload a video to the video block
- Switch to the Slides tab — confirm video appears in all slide overrides
- Override video on slide 2 — upload a different video
- Hide video on slide 3 using the eye toggle
- Confirm the creative preview shows: slide 1 = default video, slide 2 = overridden video, slide 3 = no video
- [ ] Step 2: Verify scroll-mode backward animation
- In scroll mode (default transition), navigate forward then backward via arrows
- Confirm smooth animation in both directions
- Rapid-click the back arrow multiple times — confirm no glitches
- [ ] Step 3: Verify video-based timer dots
- Enable autoplay with timer dots
- On a slide with video: timer dot progress follows video playback
- On a slide without video (hidden): timer dot uses fixed interval
- 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 -51
2
2
- [ ] Step 5: Final commit if any cleanup needed