feat: add hosted embed URL functionality and enhance widget embed options with tabbed interface
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user