Appearance
Preview Restart System
Overview
The builder has two preview modes, each with a different restart mechanism:
| Mode | Component | When used | Restart method |
|---|---|---|---|
| Local build | LocalBuildPreview.vue | Builder (editing) | Dispatches creative-data-update event to iframe |
| Standalone | PreviewIframe.vue | Standalone preview page | Reloads iframe by changing its src URL |
Both are rendered by PreviewPanel.vue, which switches between them based on isStandalonePreview.
Local Build Preview (Builder)
Files:
src/pages/Chatbots/components/BuilderVisuals/Preview/LocalBuildPreview.vuesrc/preview-iframe.ts(runs inside the iframe)preview-frame.html(iframe entry point)
How it works
- The iframe loads
preview-frame.html, which runspreview-iframe.tsas a module script preview-iframe.tsregisters acreative-data-updatelistener and postspreview-iframe-readyto the parentLocalBuildPreviewwatches Vuex block state changes and dispatchescreative-data-updateto the iframe'scontentDocument- Inside the iframe,
handleEventunmounts the previous Vue 3 creative app, clears the#previewcontainer, and builds + mounts a new one viabuildCreative()from Creative-Engine
Restart flow
Restart button (PreviewPanel)
→ $refs.previewIframe.reloadCreative()
→ LocalBuildPreview.localBuildCreative() [debounced 400ms]
→ Creates payload via createPayload()
→ Dispatches 'creative-data-update' to iframe contentDocument
→ preview-iframe.ts handleEvent():
→ creative.unmount()
→ preview.innerHTML = '' (clear leftover DOM)
→ buildCreative() → new Vue 3 app
→ requestAnimationFrame → creative.mount('#preview')Key detail: preview-iframe-ready
The iframe loads preview-iframe.ts as a module script, which means event listeners are registered AFTER the iframe's load event fires. LocalBuildPreview listens for the preview-iframe-ready postMessage to know when the iframe is ready to receive creative-data-update events.
Standalone Preview
File: src/pages/Chatbots/components/BuilderVisuals/Preview/PreviewIframe.vue
How it works
- The iframe loads a built creative's
tagFileURL (e.g.,https://delivery.cavai.com/assets/creatives/{id}/{buildId}/built/assets/stub.js) - This is a fully built creative running independently — no local build, no
creative-data-updateevents - The iframe is cross-origin (delivery CDN), so the parent cannot access its DOM or dispatch events
Restart flow
Restart button (PreviewPanel)
→ $refs.previewIframe.reloadCreative()
→ PreviewIframe.reloadCreativeCORSWorkaround()
→ Sets iframeUrl to null (removes iframe)
→ After 100ms timeout:
→ Sets iframeUrl to original URL + cache-bust query param (?t=timestamp)
→ Browser fetches fresh copy of the creative
→ iframe renders the creative from scratchWhy cache-bust is needed
The original implementation set iframeUrl back to the exact same URL. Modern browsers optimize this by serving the cached version, so the creative never actually reloaded. Adding ?t={timestamp} forces a fresh fetch. The server ignores the query parameter — it serves the same static creative files regardless.
Why we can't use location.reload()
The iframe loads from a different origin (delivery CDN), so window.frames[name].location.reload() throws a SecurityError. The workaround removes and re-adds the iframe by toggling its src attribute via Vue reactivity.
Known limitations
- Local development CORS: When running locally, Creative-Engine's
fetchImage()usesfetch()to load images from the delivery CDN as blobs. This fails due to CORS (localhost origin vs. delivery CDN). Images loaded via CSSbackground-imageor<img>tags are not affected. This only impacts local development — production serves everything from the same domain. - Standalone preview requires a build: The standalone preview loads a pre-built creative via
tagFile. If the creative has never been built (buildIdis null), the iframe gets a 404. Builder preview (local build) does not have this limitation.