This commit is contained in:
Timothy Jaeryang Baek
2026-03-26 18:36:22 -05:00
parent 925f77f0f5
commit 4c872a8d12
@@ -1,4 +1,5 @@
<script lang="ts">
import { decode } from 'html-entities';
import { getContext } from 'svelte';
import { slide } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
@@ -9,6 +10,9 @@
import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
import Sparkles from '$lib/components/icons/Sparkles.svelte';
import CheckCircle from '$lib/components/icons/CheckCircle.svelte';
import FullHeightIframe from '$lib/components/common/FullHeightIframe.svelte';
import { settings } from '$lib/stores';
const i18n = getContext('i18n');
@@ -20,6 +24,8 @@
name?: string;
done?: string;
duration?: string;
embeds?: string;
arguments?: string;
};
}> = [];
@@ -27,6 +33,14 @@
let open = false;
function parseJSONString(str: string) {
try {
return parseJSONString(JSON.parse(str));
} catch (e) {
return str;
}
}
$: toolCallCount = tokens.filter((t) => t?.attributes?.type === 'tool_calls').length;
$: reasoningCount = tokens.filter((t) => t?.attributes?.type === 'reasoning').length;
$: hasPending =
@@ -35,6 +49,28 @@
$: codeInterpreterCount = tokens.filter((t) => t?.attributes?.type === 'code_interpreter').length;
// Collect all embeds from tool_calls tokens
$: allEmbeds = (() => {
const result: Array<{ name: string; embed: string; args: string }> = [];
for (const t of tokens) {
if (t?.attributes?.type !== 'tool_calls') continue;
const raw = decode(t.attributes?.embeds ?? '');
try {
const parsed = parseJSONString(raw);
if (Array.isArray(parsed) && parsed.length > 0) {
for (const embed of parsed) {
result.push({
name: t.attributes?.name ?? '',
embed,
args: decode(t.attributes?.arguments ?? '')
});
}
}
} catch {}
}
return result;
})();
$: summaryText = (() => {
const parts = [];
@@ -71,57 +107,73 @@
</script>
<div {id} class="w-full">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<button
class="w-fit text-left text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition cursor-pointer"
aria-label={$i18n.t('Toggle details')}
aria-expanded={open}
on:click={() => {
open = !open;
}}
>
<div class="flex items-center gap-1.5">
<!-- Status icon -->
{#if hasPending}
<div>
<Spinner className="size-4" />
</div>
{:else if toolCallCount > 0}
<div class="text-emerald-500 dark:text-emerald-400">
<CheckCircle className="size-4" strokeWidth="2" />
</div>
{:else}
<div class="text-gray-400 dark:text-gray-500">
<Sparkles className="size-3.5" />
</div>
{/if}
<!-- Summary text -->
<div class="flex-1 line-clamp-1">
<span class="text-gray-600 dark:text-gray-300 {hasPending ? 'shimmer' : ''}"
>{prefixText}</span
>
{#if summaryText}
<span class="text-gray-400 dark:text-gray-500">{summaryText}</span>
{/if}
{#if allEmbeds.length > 0}
<!-- Embed mode: render iframes directly like ToolCallDisplay -->
{#each allEmbeds as embedItem, idx}
<div id={`${id}-embed-${idx}`}>
<FullHeightIframe
src={embedItem.embed}
args={embedItem.args}
allowScripts={true}
allowForms={$settings?.iframeSandboxAllowForms ?? false}
allowSameOrigin={$settings?.iframeSandboxAllowSameOrigin ?? false}
allowPopups={true}
/>
</div>
<!-- Chevron -->
<div class="flex shrink-0 self-center text-gray-400 dark:text-gray-500">
{#if open}
<ChevronUp strokeWidth="3.5" className="size-3" />
{/each}
{:else}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<button
class="w-fit text-left text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition cursor-pointer"
aria-label={$i18n.t('Toggle details')}
aria-expanded={open}
on:click={() => {
open = !open;
}}
>
<div class="flex items-center gap-1.5">
<!-- Status icon -->
{#if hasPending}
<div>
<Spinner className="size-4" />
</div>
{:else if toolCallCount > 0}
<div class="text-emerald-500 dark:text-emerald-400">
<CheckCircle className="size-4" strokeWidth="2" />
</div>
{:else}
<ChevronDown strokeWidth="3.5" className="size-3" />
<div class="text-gray-400 dark:text-gray-500">
<Sparkles className="size-3.5" />
</div>
{/if}
</div>
</div>
</button>
{#if open}
<div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
<div class="mb-0.5 space-y-0.5">
<slot name="content" />
<!-- Summary text -->
<div class="flex-1 line-clamp-1">
<span class="text-gray-600 dark:text-gray-300 {hasPending ? 'shimmer' : ''}"
>{prefixText}</span
>
{#if summaryText}
<span class="text-gray-400 dark:text-gray-500">{summaryText}</span>
{/if}
</div>
<!-- Chevron -->
<div class="flex shrink-0 self-center text-gray-400 dark:text-gray-500">
{#if open}
<ChevronUp strokeWidth="3.5" className="size-3" />
{:else}
<ChevronDown strokeWidth="3.5" className="size-3" />
{/if}
</div>
</div>
</div>
</button>
{#if open}
<div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
<div class="mb-0.5 space-y-0.5">
<slot name="content" />
</div>
</div>
{/if}
{/if}
</div>