feat: add hosted embed URL functionality and enhance widget embed options with tabbed interface

This commit is contained in:
Rotimi Best
2026-05-19 16:32:42 +01:00
parent eec65283a4
commit 0f388ae0d3
18 changed files with 203 additions and 11 deletions
@@ -66,6 +66,19 @@
let lastStepsCount = $state(0);
let lastCurrentActionSig = $state('');
let lastStreamingSig = $state(0);
// True when the user is at (or very near) the bottom. Streaming auto-scroll is gated on this
// so scrolling up doesn't get clobbered by the next token.
let isPinnedToBottom = $state(true);
const SCROLL_PIN_THRESHOLD = 60;
function handleScroll() {
if (!messagesContainer) return;
const distanceFromBottom =
messagesContainer.scrollHeight - messagesContainer.scrollTop - messagesContainer.clientHeight;
isPinnedToBottom = distanceFromBottom <= SCROLL_PIN_THRESHOLD;
}
const isEmpty = $derived(messages.length === 0);
@@ -101,6 +114,8 @@
if (messages.length === lastMessageCount) return;
lastMessageCount = messages.length;
// A new message (typically the user's) — force scroll and re-pin.
isPinnedToBottom = true;
scrollToBottom();
});
@@ -110,6 +125,9 @@
if (streamingContentSig === lastStreamingSig) return;
lastStreamingSig = streamingContentSig;
if (!isPinnedToBottom) return;
// Use 'auto' during streaming — smooth scrolls queue up per token and lag behind the cursor.
scrollToBottom('auto');
});
@@ -120,7 +138,7 @@
const stepsCount = planExecutionState.steps.length;
if (stepsCount !== lastStepsCount) {
lastStepsCount = stepsCount;
scrollToBottom();
if (isPinnedToBottom) scrollToBottom();
return;
}
@@ -128,12 +146,12 @@
if (actionSig !== lastCurrentActionSig) {
lastCurrentActionSig = actionSig;
scrollToBottom();
if (isPinnedToBottom) scrollToBottom();
}
});
</script>
<div class="flex-1 overflow-y-auto p-4" bind:this={messagesContainer}>
<div class="flex-1 overflow-y-auto p-4" bind:this={messagesContainer} onscroll={handleScroll}>
{#if isEmpty}
<!-- Empty state with quick actions -->
<div class="flex h-full flex-col items-center justify-center gap-4">
@@ -1,5 +1,6 @@
<script lang="ts">
import { Button } from '@cio/ui/base/button';
import * as Tabs from '@cio/ui/base/tabs';
import { TextareaField } from '@cio/ui/custom/textarea-field';
import { t } from '$lib/utils/functions/translations';
import type { WidgetDetail } from '../utils/types';
@@ -11,15 +12,36 @@
}
let { detail, onRollback, onDelete }: Props = $props();
let activeFormat = $state<'html' | 'url'>('html');
</script>
<div class="space-y-6">
<TextareaField label={$t('widgets.form.embed_code')} value={detail.widget.embedCode} rows={6} readonly />
<div class="flex flex-wrap gap-2">
<Button variant="outline" onclick={() => navigator.clipboard.writeText(detail.widget.embedCode)}>
{$t('widgets.actions.copy_embed')}
</Button>
</div>
<Tabs.Root bind:value={activeFormat} class="w-full">
<Tabs.List class="inline-flex w-auto">
<Tabs.Trigger value="html">{$t('widgets.embed.tabs.html')}</Tabs.Trigger>
<Tabs.Trigger value="url">{$t('widgets.embed.tabs.url')}</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="html" class="mt-4 space-y-3">
<TextareaField label={$t('widgets.form.embed_code')} value={detail.widget.embedCode} rows={6} readonly />
<div class="flex flex-wrap gap-2">
<Button variant="outline" onclick={() => navigator.clipboard.writeText(detail.widget.embedCode)}>
{$t('widgets.actions.copy_embed')}
</Button>
</div>
</Tabs.Content>
<Tabs.Content value="url" class="mt-4 space-y-3">
<TextareaField label={$t('widgets.embed.url_label')} value={detail.widget.hostedEmbedUrl} rows={2} readonly />
<p class="ui:text-muted-foreground text-xs">{$t('widgets.embed.url_helper')}</p>
<div class="flex flex-wrap gap-2">
<Button variant="outline" onclick={() => navigator.clipboard.writeText(detail.widget.hostedEmbedUrl)}>
{$t('widgets.actions.copy_url')}
</Button>
</div>
</Tabs.Content>
</Tabs.Root>
{#if detail.versions.length > 0}
<div class="space-y-2">
@@ -4010,9 +4010,18 @@
"archive": "Arkiv",
"restore": "Gendan",
"copy_embed": "Kopiér indlejring",
"copy_url": "Kopiér URL",
"rename": "Omdøb",
"delete": "Slet"
},
"embed": {
"tabs": {
"html": "HTML",
"url": "URL"
},
"url_label": "Hostet embed-URL",
"url_helper": "Brug denne URL på platforme som Notion eller Confluence, der kun accepterer links."
},
"preview": {
"heading": "Forhåndsvisning",
"saved": "Gemt",
@@ -4010,9 +4010,18 @@
"archive": "Archiv",
"restore": "Wiederherstellen",
"copy_embed": "Einbetten kopieren",
"copy_url": "URL kopieren",
"rename": "Umbenennen",
"delete": "Löschen"
},
"embed": {
"tabs": {
"html": "HTML",
"url": "URL"
},
"url_label": "Gehostete Embed-URL",
"url_helper": "Verwenden Sie diese URL auf Plattformen wie Notion oder Confluence, die nur Links akzeptieren."
},
"preview": {
"heading": "Vorschau",
"saved": "Gespeichert",
@@ -1343,9 +1343,18 @@
"archive": "Archive",
"restore": "Restore",
"copy_embed": "Copy embed",
"copy_url": "Copy URL",
"rename": "Rename",
"delete": "Delete"
},
"embed": {
"tabs": {
"html": "HTML",
"url": "URL"
},
"url_label": "Hosted embed URL",
"url_helper": "Use this URL on platforms like Notion or Confluence that only accept a link."
},
"create": {
"description": "Give your widget a name to get started."
},
@@ -4010,9 +4010,18 @@
"archive": "Archivo",
"restore": "Restaurar",
"copy_embed": "Copiar incrustado",
"copy_url": "Copiar URL",
"rename": "Cambiar nombre",
"delete": "Eliminar"
},
"embed": {
"tabs": {
"html": "HTML",
"url": "URL"
},
"url_label": "URL de incrustación alojada",
"url_helper": "Usa esta URL en plataformas como Notion o Confluence que solo aceptan enlaces."
},
"preview": {
"heading": "Vista previa",
"saved": "Guardado",
@@ -4010,9 +4010,18 @@
"archive": "Archiver",
"restore": "Restaurer",
"copy_embed": "Copier l'intégration",
"copy_url": "Copier l'URL",
"rename": "Renommer",
"delete": "Supprimer"
},
"embed": {
"tabs": {
"html": "HTML",
"url": "URL"
},
"url_label": "URL d'intégration hébergée",
"url_helper": "Utilisez cette URL sur des plateformes comme Notion ou Confluence qui n'acceptent que des liens."
},
"preview": {
"heading": "Aperçu",
"saved": "Enregistré",
@@ -4010,9 +4010,18 @@
"archive": "पुरालेख",
"restore": "पुनर्स्थापित करें",
"copy_embed": "एंबेड कॉपी करें",
"copy_url": "URL कॉपी करें",
"rename": "नाम बदलें",
"delete": "हटाएँ"
},
"embed": {
"tabs": {
"html": "HTML",
"url": "URL"
},
"url_label": "होस्ट किया गया एम्बेड URL",
"url_helper": "Notion या Confluence जैसी प्लेटफ़ॉर्म पर इस URL का उपयोग करें जो केवल लिंक स्वीकार करती हैं।"
},
"preview": {
"heading": "पूर्वावलोकन",
"saved": "सहेजा गया",
@@ -4010,9 +4010,18 @@
"archive": "Archiwum",
"restore": "Przywróć",
"copy_embed": "Skopiuj osadzenie",
"copy_url": "Kopiuj URL",
"rename": "Zmień nazwę",
"delete": "Usuń"
},
"embed": {
"tabs": {
"html": "HTML",
"url": "URL"
},
"url_label": "Hostowany URL osadzania",
"url_helper": "Użyj tego adresu URL na platformach takich jak Notion lub Confluence, które akceptują tylko linki."
},
"preview": {
"heading": "Podgląd",
"saved": "Zapisano",
@@ -4010,9 +4010,18 @@
"archive": "Arquivo",
"restore": "Restaurar",
"copy_embed": "Copiar incorporação",
"copy_url": "Copiar URL",
"rename": "Renomear",
"delete": "Excluir"
},
"embed": {
"tabs": {
"html": "HTML",
"url": "URL"
},
"url_label": "URL de incorporação hospedada",
"url_helper": "Use esta URL em plataformas como Notion ou Confluence que só aceitam links."
},
"preview": {
"heading": "Visualização",
"saved": "Salvo",
@@ -4010,9 +4010,18 @@
"archive": "Архив",
"restore": "Восстановить",
"copy_embed": "Копировать встроить",
"copy_url": "Копировать URL",
"rename": "Переименовать",
"delete": "Удалить"
},
"embed": {
"tabs": {
"html": "HTML",
"url": "URL"
},
"url_label": "URL встраивания",
"url_helper": "Используйте этот URL на платформах вроде Notion или Confluence, которые принимают только ссылки."
},
"preview": {
"heading": "Предварительный просмотр",
"saved": "Сохранено",
@@ -4010,9 +4010,18 @@
"archive": "Lưu trữ",
"restore": "Khôi phục",
"copy_embed": "Sao chép nhúng",
"copy_url": "Sao chép URL",
"rename": "Đổi tên",
"delete": "Xóa"
},
"embed": {
"tabs": {
"html": "HTML",
"url": "URL"
},
"url_label": "URL nhúng được lưu trữ",
"url_helper": "Sử dụng URL này trên các nền tảng như Notion hoặc Confluence chỉ chấp nhận liên kết."
},
"preview": {
"heading": "Xem trước",
"saved": "Đã lưu",