|
1 | 1 | <script lang="ts"> |
| 2 | + import { decode } from 'html-entities'; |
2 | 3 | import { getContext } from 'svelte'; |
3 | 4 | import { slide } from 'svelte/transition'; |
4 | 5 | import { quintOut } from 'svelte/easing'; |
|
9 | 10 | import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte'; |
10 | 11 | import Sparkles from '$lib/components/icons/Sparkles.svelte'; |
11 | 12 | import CheckCircle from '$lib/components/icons/CheckCircle.svelte'; |
| 13 | + import FullHeightIframe from '$lib/components/common/FullHeightIframe.svelte'; |
| 14 | +
|
| 15 | + import { settings } from '$lib/stores'; |
12 | 16 |
|
13 | 17 | const i18n = getContext('i18n'); |
14 | 18 |
|
|
20 | 24 | name?: string; |
21 | 25 | done?: string; |
22 | 26 | duration?: string; |
| 27 | + embeds?: string; |
| 28 | + arguments?: string; |
23 | 29 | }; |
24 | 30 | }> = []; |
25 | 31 |
|
26 | 32 | export let messageDone = true; |
27 | 33 |
|
28 | 34 | let open = false; |
29 | 35 |
|
| 36 | + function parseJSONString(str: string) { |
| 37 | + try { |
| 38 | + return parseJSONString(JSON.parse(str)); |
| 39 | + } catch (e) { |
| 40 | + return str; |
| 41 | + } |
| 42 | + } |
| 43 | +
|
30 | 44 | $: toolCallCount = tokens.filter((t) => t?.attributes?.type === 'tool_calls').length; |
31 | 45 | $: reasoningCount = tokens.filter((t) => t?.attributes?.type === 'reasoning').length; |
32 | 46 | $: hasPending = |
|
35 | 49 |
|
36 | 50 | $: codeInterpreterCount = tokens.filter((t) => t?.attributes?.type === 'code_interpreter').length; |
37 | 51 |
|
| 52 | + // Collect all embeds from tool_calls tokens |
| 53 | + $: allEmbeds = (() => { |
| 54 | + const result: Array<{ name: string; embed: string; args: string }> = []; |
| 55 | + for (const t of tokens) { |
| 56 | + if (t?.attributes?.type !== 'tool_calls') continue; |
| 57 | + const raw = decode(t.attributes?.embeds ?? ''); |
| 58 | + try { |
| 59 | + const parsed = parseJSONString(raw); |
| 60 | + if (Array.isArray(parsed) && parsed.length > 0) { |
| 61 | + for (const embed of parsed) { |
| 62 | + result.push({ |
| 63 | + name: t.attributes?.name ?? '', |
| 64 | + embed, |
| 65 | + args: decode(t.attributes?.arguments ?? '') |
| 66 | + }); |
| 67 | + } |
| 68 | + } |
| 69 | + } catch {} |
| 70 | + } |
| 71 | + return result; |
| 72 | + })(); |
| 73 | +
|
38 | 74 | $: summaryText = (() => { |
39 | 75 | const parts = []; |
40 | 76 |
|
|
71 | 107 | </script> |
72 | 108 |
|
73 | 109 | <div {id} class="w-full"> |
74 | | - <!-- svelte-ignore a11y-no-static-element-interactions --> |
75 | | - <button |
76 | | - class="w-fit text-left text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition cursor-pointer" |
77 | | - aria-label={$i18n.t('Toggle details')} |
78 | | - aria-expanded={open} |
79 | | - on:click={() => { |
80 | | - open = !open; |
81 | | - }} |
82 | | - > |
83 | | - <div class="flex items-center gap-1.5"> |
84 | | - <!-- Status icon --> |
85 | | - {#if hasPending} |
86 | | - <div> |
87 | | - <Spinner className="size-4" /> |
88 | | - </div> |
89 | | - {:else if toolCallCount > 0} |
90 | | - <div class="text-emerald-500 dark:text-emerald-400"> |
91 | | - <CheckCircle className="size-4" strokeWidth="2" /> |
92 | | - </div> |
93 | | - {:else} |
94 | | - <div class="text-gray-400 dark:text-gray-500"> |
95 | | - <Sparkles className="size-3.5" /> |
96 | | - </div> |
97 | | - {/if} |
98 | | - |
99 | | - <!-- Summary text --> |
100 | | - <div class="flex-1 line-clamp-1"> |
101 | | - <span class="text-gray-600 dark:text-gray-300 {hasPending ? 'shimmer' : ''}" |
102 | | - >{prefixText}</span |
103 | | - > |
104 | | - {#if summaryText} |
105 | | - <span class="text-gray-400 dark:text-gray-500">{summaryText}</span> |
106 | | - {/if} |
| 110 | + {#if allEmbeds.length > 0} |
| 111 | + <!-- Embed mode: render iframes directly like ToolCallDisplay --> |
| 112 | + {#each allEmbeds as embedItem, idx} |
| 113 | + <div id={`${id}-embed-${idx}`}> |
| 114 | + <FullHeightIframe |
| 115 | + src={embedItem.embed} |
| 116 | + args={embedItem.args} |
| 117 | + allowScripts={true} |
| 118 | + allowForms={$settings?.iframeSandboxAllowForms ?? false} |
| 119 | + allowSameOrigin={$settings?.iframeSandboxAllowSameOrigin ?? false} |
| 120 | + allowPopups={true} |
| 121 | + /> |
107 | 122 | </div> |
108 | | - |
109 | | - <!-- Chevron --> |
110 | | - <div class="flex shrink-0 self-center text-gray-400 dark:text-gray-500"> |
111 | | - {#if open} |
112 | | - <ChevronUp strokeWidth="3.5" className="size-3" /> |
| 123 | + {/each} |
| 124 | + {:else} |
| 125 | + <!-- svelte-ignore a11y-no-static-element-interactions --> |
| 126 | + <button |
| 127 | + class="w-fit text-left text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition cursor-pointer" |
| 128 | + aria-label={$i18n.t('Toggle details')} |
| 129 | + aria-expanded={open} |
| 130 | + on:click={() => { |
| 131 | + open = !open; |
| 132 | + }} |
| 133 | + > |
| 134 | + <div class="flex items-center gap-1.5"> |
| 135 | + <!-- Status icon --> |
| 136 | + {#if hasPending} |
| 137 | + <div> |
| 138 | + <Spinner className="size-4" /> |
| 139 | + </div> |
| 140 | + {:else if toolCallCount > 0} |
| 141 | + <div class="text-emerald-500 dark:text-emerald-400"> |
| 142 | + <CheckCircle className="size-4" strokeWidth="2" /> |
| 143 | + </div> |
113 | 144 | {:else} |
114 | | - <ChevronDown strokeWidth="3.5" className="size-3" /> |
| 145 | + <div class="text-gray-400 dark:text-gray-500"> |
| 146 | + <Sparkles className="size-3.5" /> |
| 147 | + </div> |
115 | 148 | {/if} |
| 149 | + |
| 150 | + <!-- Summary text --> |
| 151 | + <div class="flex-1 line-clamp-1"> |
| 152 | + <span class="text-gray-600 dark:text-gray-300 {hasPending ? 'shimmer' : ''}" |
| 153 | + >{prefixText}</span |
| 154 | + > |
| 155 | + {#if summaryText} |
| 156 | + <span class="text-gray-400 dark:text-gray-500">{summaryText}</span> |
| 157 | + {/if} |
| 158 | + </div> |
| 159 | + |
| 160 | + <!-- Chevron --> |
| 161 | + <div class="flex shrink-0 self-center text-gray-400 dark:text-gray-500"> |
| 162 | + {#if open} |
| 163 | + <ChevronUp strokeWidth="3.5" className="size-3" /> |
| 164 | + {:else} |
| 165 | + <ChevronDown strokeWidth="3.5" className="size-3" /> |
| 166 | + {/if} |
| 167 | + </div> |
116 | 168 | </div> |
117 | | - </div> |
118 | | - </button> |
| 169 | + </button> |
119 | 170 |
|
120 | | - {#if open} |
121 | | - <div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}> |
122 | | - <div class="mb-0.5 space-y-0.5"> |
123 | | - <slot name="content" /> |
| 171 | + {#if open} |
| 172 | + <div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}> |
| 173 | + <div class="mb-0.5 space-y-0.5"> |
| 174 | + <slot name="content" /> |
| 175 | + </div> |
124 | 176 | </div> |
125 | | - </div> |
| 177 | + {/if} |
126 | 178 | {/if} |
127 | 179 | </div> |
0 commit comments