This commit is contained in:
Timothy Jaeryang Baek 2025-09-18 21:25:26 -05:00
parent 07c5b25bc8
commit a5d8882bba
3 changed files with 217 additions and 214 deletions

View file

@ -465,6 +465,15 @@
return; return;
} }
if (event.data.type === 'action:submit') {
console.debug(event.data.text);
if (prompt !== '') {
await tick();
submitPrompt(prompt);
}
}
// Replace with your iframe's origin // Replace with your iframe's origin
if (event.data.type === 'input:prompt') { if (event.data.type === 'input:prompt') {
console.debug(event.data.text); console.debug(event.data.text);
@ -477,15 +486,6 @@
} }
} }
if (event.data.type === 'action:submit') {
console.debug(event.data.text);
if (prompt !== '') {
await tick();
submitPrompt(prompt);
}
}
if (event.data.type === 'input:prompt:submit') { if (event.data.type === 'input:prompt:submit') {
console.debug(event.data.text); console.debug(event.data.text);

View file

@ -89,54 +89,55 @@
</script> </script>
<div {id} class={className}> <div {id} class={className}>
{#if title !== null} {#if attributes?.type === 'tool_calls'}
<!-- svelte-ignore a11y-no-static-element-interactions --> {@const args = decode(attributes?.arguments)}
<!-- svelte-ignore a11y-click-events-have-key-events --> {@const result = decode(attributes?.result ?? '')}
<div {@const files = parseJSONString(decode(attributes?.files ?? ''))}
class="{buttonClassName} cursor-pointer" {@const embeds = parseJSONString(decode(attributes?.embeds ?? ''))}
on:pointerup={() => {
if (!disabled) {
open = !open;
}
}}
>
<div
class=" w-full font-medium flex items-center justify-between gap-2 {attributes?.done &&
attributes?.done !== 'true'
? 'shimmer'
: ''}
"
>
{#if attributes?.done && attributes?.done !== 'true'}
<div>
<Spinner className="size-4" />
</div>
{/if}
<div class=""> {#if embeds && Array.isArray(embeds) && embeds.length > 0}
{#if attributes?.type === 'reasoning'} <div class="py-1 w-full cursor-pointer">
{#if attributes?.done === 'true' && attributes?.duration} <div class=" w-full text-xs text-gray-500">
{#if attributes.duration < 1} <div class="">
{$i18n.t('Thought for less than a second')} {attributes.name}
{:else if attributes.duration < 60} </div>
{$i18n.t('Thought for {{DURATION}} seconds', { </div>
DURATION: attributes.duration
})} {#each embeds as embed, idx}
{:else} <div class="my-2" id={`${collapsibleId}-tool-calls-${attributes?.id}-embed-${idx}`}>
{$i18n.t('Thought for {{DURATION}}', { <FullHeightIframe
DURATION: dayjs.duration(attributes.duration, 'seconds').humanize() src={embed}
})} allowScripts={true}
{/if} allowForms={true}
{:else} allowSameOrigin={true}
{$i18n.t('Thinking...')} allowPopups={true}
{/if} />
{:else if attributes?.type === 'code_interpreter'} </div>
{#if attributes?.done === 'true'} {/each}
{$i18n.t('Analyzed')} </div>
{:else} {:else}
{$i18n.t('Analyzing...')} <div
{/if} class="{buttonClassName} cursor-pointer"
{:else if attributes?.type === 'tool_calls'} on:pointerup={() => {
if (!disabled) {
open = !open;
}
}}
>
<div
class=" w-full font-medium flex items-center justify-between gap-2 {attributes?.done &&
attributes?.done !== 'true'
? 'shimmer'
: ''}
"
>
{#if attributes?.done && attributes?.done !== 'true'}
<div>
<Spinner className="size-4" />
</div>
{/if}
<div class="">
{#if attributes?.done === 'true'} {#if attributes?.done === 'true'}
<Markdown <Markdown
id={`${collapsibleId}-tool-calls-${attributes?.id}`} id={`${collapsibleId}-tool-calls-${attributes?.id}`}
@ -152,130 +153,172 @@
})} })}
/> />
{/if} {/if}
{:else} </div>
{title}
{/if}
</div>
<div class="flex self-center translate-y-[1px]"> <div class="flex self-center translate-y-[1px]">
{#if open} {#if open}
<ChevronUp strokeWidth="3.5" className="size-3.5" /> <ChevronUp strokeWidth="3.5" className="size-3.5" />
{:else} {:else}
<ChevronDown strokeWidth="3.5" className="size-3.5" /> <ChevronDown strokeWidth="3.5" className="size-3.5" />
{/if} {/if}
</div>
</div> </div>
</div> </div>
</div>
{:else}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="{buttonClassName} cursor-pointer"
on:click={(e) => {
e.stopPropagation();
}}
on:pointerup={(e) => {
if (!disabled) {
open = !open;
}
}}
>
<div>
<div class="flex items-start justify-between">
<slot />
{#if chevron} {#if !grow}
<div class="flex self-start translate-y-1"> {#if open && !hide}
{#if open} <div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
<ChevronUp strokeWidth="3.5" className="size-3.5" /> {#if attributes?.type === 'tool_calls'}
{#if attributes?.done === 'true'}
<Markdown
id={`${collapsibleId}-tool-calls-${attributes?.id}-result`}
content={`> \`\`\`json
> ${formatJSONString(args)}
> ${formatJSONString(result)}
> \`\`\``}
/>
{:else} {:else}
<ChevronDown strokeWidth="3.5" className="size-3.5" /> <Markdown
id={`${collapsibleId}-tool-calls-${attributes?.id}-result`}
content={`> \`\`\`json
> ${formatJSONString(args)}
> \`\`\``}
/>
{/if} {/if}
</div> {:else}
{/if}
</div>
{#if grow}
{#if open && !hide}
<div
transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}
on:pointerup={(e) => {
e.stopPropagation();
}}
>
<slot name="content" /> <slot name="content" />
</div> {/if}
</div>
{/if}
{#if attributes?.done === 'true'}
{#if typeof files === 'object'}
{#each files ?? [] as file, idx}
{#if file.startsWith('data:image/')}
<Image
id={`${collapsibleId}-tool-calls-${attributes?.id}-result-${idx}`}
src={file}
alt="Image"
/>
{/if}
{/each}
{/if} {/if}
{/if} {/if}
</div> {/if}
</div> {/if}
{/if} {:else}
{#if title !== null}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="{buttonClassName} cursor-pointer"
on:pointerup={() => {
if (!disabled) {
open = !open;
}
}}
>
<div
class=" w-full font-medium flex items-center justify-between gap-2 {attributes?.done &&
attributes?.done !== 'true'
? 'shimmer'
: ''}
"
>
{#if attributes?.done && attributes?.done !== 'true'}
<div>
<Spinner className="size-4" />
</div>
{/if}
{#if attributes?.type === 'tool_calls'} <div class="">
{@const args = decode(attributes?.arguments)} {#if attributes?.type === 'reasoning'}
{@const result = decode(attributes?.result ?? '')} {#if attributes?.done === 'true' && attributes?.duration}
{@const files = parseJSONString(decode(attributes?.files ?? ''))} {#if attributes.duration < 1}
{@const embeds = parseJSONString(decode(attributes?.embeds ?? ''))} {$i18n.t('Thought for less than a second')}
{:else if attributes.duration < 60}
{$i18n.t('Thought for {{DURATION}} seconds', {
DURATION: attributes.duration
})}
{:else}
{$i18n.t('Thought for {{DURATION}}', {
DURATION: dayjs.duration(attributes.duration, 'seconds').humanize()
})}
{/if}
{:else}
{$i18n.t('Thinking...')}
{/if}
{:else if attributes?.type === 'code_interpreter'}
{#if attributes?.done === 'true'}
{$i18n.t('Analyzed')}
{:else}
{$i18n.t('Analyzing...')}
{/if}
{:else}
{title}
{/if}
</div>
{#if embeds && Array.isArray(embeds) && embeds.length > 0} <div class="flex self-center translate-y-[1px]">
{#each embeds as embed, idx} {#if open}
<div class="my-2" id={`${collapsibleId}-tool-calls-${attributes?.id}-embed-${idx}`}> <ChevronUp strokeWidth="3.5" className="size-3.5" />
<FullHeightIframe {:else}
src={embed} <ChevronDown strokeWidth="3.5" className="size-3.5" />
allowScripts={true} {/if}
allowForms={$settings?.iframeSandboxAllowForms ?? false} </div>
allowSameOrigin={$settings?.iframeSandboxAllowSameOrigin ?? false}
allowPopups={$settings?.iframeSandboxAllowPopups ?? false}
/>
</div> </div>
{/each} </div>
{:else}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="{buttonClassName} cursor-pointer"
on:click={(e) => {
e.stopPropagation();
}}
on:pointerup={(e) => {
if (!disabled) {
open = !open;
}
}}
>
<div>
<div class="flex items-start justify-between">
<slot />
{#if chevron}
<div class="flex self-start translate-y-1">
{#if open}
<ChevronUp strokeWidth="3.5" className="size-3.5" />
{:else}
<ChevronDown strokeWidth="3.5" className="size-3.5" />
{/if}
</div>
{/if}
</div>
{#if grow}
{#if open && !hide}
<div
transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}
on:pointerup={(e) => {
e.stopPropagation();
}}
>
<slot name="content" />
</div>
{/if}
{/if}
</div>
</div>
{/if} {/if}
{#if !grow} {#if !grow}
{#if open && !hide} {#if open && !hide}
<div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}> <div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
{#if attributes?.type === 'tool_calls'} <slot name="content" />
{#if attributes?.done === 'true'}
<Markdown
id={`${collapsibleId}-tool-calls-${attributes?.id}-result`}
content={`> \`\`\`json
> ${formatJSONString(args)}
> ${formatJSONString(result)}
> \`\`\``}
/>
{:else}
<Markdown
id={`${collapsibleId}-tool-calls-${attributes?.id}-result`}
content={`> \`\`\`json
> ${formatJSONString(args)}
> \`\`\``}
/>
{/if}
{:else}
<slot name="content" />
{/if}
</div> </div>
{/if} {/if}
{#if attributes?.done === 'true'}
{#if typeof files === 'object'}
{#each files ?? [] as file, idx}
{#if file.startsWith('data:image/')}
<Image
id={`${collapsibleId}-tool-calls-${attributes?.id}-result-${idx}`}
src={file}
alt="Image"
/>
{/if}
{/each}
{/if}
{/if}
{/if}
{:else if !grow}
{#if open && !hide}
<div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
<slot name="content" />
</div>
{/if} {/if}
{/if} {/if}
</div> </div>

View file

@ -4,7 +4,8 @@
// Props // Props
export let src: string | null = null; // URL or raw HTML (auto-detected) export let src: string | null = null; // URL or raw HTML (auto-detected)
export let title = 'Embedded Content'; export let title = 'Embedded Content';
export let initialHeight = 400; // fallback height if we can't measure export let initialHeight: number | null = null; // initial height in px, null = auto
export let allowScripts = true; export let allowScripts = true;
export let allowForms = false; export let allowForms = false;
@ -33,13 +34,14 @@
// Detect URL vs raw HTML and prep src/srcdoc // Detect URL vs raw HTML and prep src/srcdoc
$: isUrl = typeof src === 'string' && /^(https?:)?\/\//i.test(src); $: isUrl = typeof src === 'string' && /^(https?:)?\/\//i.test(src);
$: iframeSrc = isUrl ? (src as string) : null; $: iframeSrc = isUrl ? (src as string) : null;
$: iframeDoc = !isUrl && src ? ensureAutosizer(src) : null; $: iframeDoc = !isUrl ? src : null;
// Try to measure same-origin content safely // Try to measure same-origin content safely
function resizeSameOrigin() { function resizeSameOrigin() {
if (!iframe) return; if (!iframe) return;
try { try {
const doc = iframe.contentDocument || iframe.contentWindow?.document; const doc = iframe.contentDocument || iframe.contentWindow?.document;
console.log('iframe doc:', doc);
if (!doc) return; if (!doc) return;
const h = Math.max(doc.documentElement?.scrollHeight ?? 0, doc.body?.scrollHeight ?? 0); const h = Math.max(doc.documentElement?.scrollHeight ?? 0, doc.body?.scrollHeight ?? 0);
if (h > 0) iframe.style.height = h + 20 + 'px'; if (h > 0) iframe.style.height = h + 20 + 'px';
@ -57,6 +59,11 @@
} }
} }
// When the iframe loads, try same-origin resize (cross-origin will noop)
function onLoad() {
requestAnimationFrame(resizeSameOrigin);
}
// Ensure event listener bound only while component lives // Ensure event listener bound only while component lives
onMount(() => { onMount(() => {
window.addEventListener('message', onMessage); window.addEventListener('message', onMessage);
@ -65,52 +72,6 @@
onDestroy(() => { onDestroy(() => {
window.removeEventListener('message', onMessage); window.removeEventListener('message', onMessage);
}); });
// When the iframe loads, try same-origin resize (cross-origin will noop)
function onLoad() {
// schedule after layout
requestAnimationFrame(resizeSameOrigin);
}
/**
* If user passes raw HTML, we inject a tiny autosizer that posts height.
* This helps both same-origin and "about:srcdoc" cases.
* (No effect if the caller already includes their own autosizer.)
*/
function ensureAutosizer(html: string): string {
const hasOurHook = /iframe:height/.test(html) || /postMessage\(.+height/i.test(html);
if (hasOurHook) return html;
// This script uses ResizeObserver to post the document height
const autosizer = `
<script>
(function () {
function send() {
try {
var h = Math.max(
document.documentElement.scrollHeight || 0,
document.body ? document.body.scrollHeight : 0
);
parent.postMessage({ type: 'iframe:height', height: h + 20 }, '*');
} catch (e) {}
}
var ro = new ResizeObserver(function(){ send(); });
ro.observe(document.documentElement);
window.addEventListener('load', send);
// Also observe body if present
if (document.body) ro.observe(document.body);
// Periodic guard in case of late content
setTimeout(send, 0);
setTimeout(send, 250);
setTimeout(send, 1000);
})();
<\/script>`;
// inject before </body> if present, else append
return (
html.replace(/<\/body\s*>/i, autosizer + '</body>') +
(/<\/body\s*>/i.test(html) ? '' : autosizer)
);
}
</script> </script>
{#if iframeDoc} {#if iframeDoc}
@ -118,12 +79,11 @@
bind:this={iframe} bind:this={iframe}
srcdoc={iframeDoc} srcdoc={iframeDoc}
{title} {title}
class="w-full rounded-xl" class="w-full rounded-2xl"
style={`height:${initialHeight}px;`} style={`${initialHeight ? `height:${initialHeight}px;` : ''}`}
width="100%" width="100%"
frameborder="0" frameborder="0"
{sandbox} {sandbox}
referrerpolicy={referrerPolicy}
{allowFullscreen} {allowFullscreen}
on:load={onLoad} on:load={onLoad}
/> />
@ -132,8 +92,8 @@
bind:this={iframe} bind:this={iframe}
src={iframeSrc} src={iframeSrc}
{title} {title}
class="w-full rounded-xl" class="w-full rounded-2xl"
style={`height:${initialHeight}px;`} style={`${initialHeight ? `height:${initialHeight}px;` : ''}`}
width="100%" width="100%"
frameborder="0" frameborder="0"
{sandbox} {sandbox}