| <script lang="ts"> |
| import { decode } from 'html-entities'; |
| import { v4 as uuidv4 } from 'uuid'; |
|
|
| import { getContext } from 'svelte'; |
| const i18n = getContext('i18n'); |
|
|
| import { slide } from 'svelte/transition'; |
| import { quintOut } from 'svelte/easing'; |
|
|
| import ChevronUp from '../icons/ChevronUp.svelte'; |
| import ChevronDown from '../icons/ChevronDown.svelte'; |
| import Spinner from './Spinner.svelte'; |
| import Markdown from '../chat/Messages/Markdown.svelte'; |
| import Image from './Image.svelte'; |
| import FullHeightIframe from './FullHeightIframe.svelte'; |
|
|
| export let id: string = ''; |
| export let attributes: { |
| type?: string; |
| id?: string; |
| name?: string; |
| arguments?: string; |
| result?: string; |
| files?: string; |
| embeds?: string; |
| done?: string; |
| } = {}; |
|
|
| export let open = false; |
| export let className = ''; |
| export let buttonClassName = |
| 'w-fit text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition'; |
|
|
| const componentId = id || uuidv4(); |
|
|
| function parseJSONString(str: string) { |
| try { |
| return parseJSONString(JSON.parse(str)); |
| } catch (e) { |
| return str; |
| } |
| } |
|
|
| function formatJSONString(str: string) { |
| try { |
| const parsed = parseJSONString(str); |
| |
| if (typeof parsed === 'object') { |
| return JSON.stringify(parsed, null, 2); |
| } else { |
| |
| return `${JSON.stringify(String(parsed))}`; |
| } |
| } catch (e) { |
| |
| return str; |
| } |
| } |
|
|
| |
| $: args = decode(attributes?.arguments ?? ''); |
| $: result = decode(attributes?.result ?? ''); |
| $: files = parseJSONString(decode(attributes?.files ?? '')); |
| $: embeds = parseJSONString(decode(attributes?.embeds ?? '')); |
| $: isDone = attributes?.done === 'true'; |
| $: isExecuting = attributes?.done && attributes?.done !== 'true'; |
| </script> |
|
|
| <div {id} class={className}> |
| {#if embeds && Array.isArray(embeds) && embeds.length > 0} |
| |
| <div class="py-1 w-full cursor-pointer"> |
| <div class="w-full text-xs text-gray-500"> |
| <div class=""> |
| {attributes.name} |
| </div> |
| </div> |
| |
| {#each embeds as embed, idx} |
| <div class="my-2" id={`${componentId}-tool-call-embed-${idx}`}> |
| <FullHeightIframe |
| src={embed} |
| {args} |
| allowScripts={true} |
| allowForms={true} |
| allowSameOrigin={true} |
| allowPopups={true} |
| /> |
| </div> |
| {/each} |
| </div> |
| {:else} |
| |
| <div |
| class="{buttonClassName} cursor-pointer" |
| on:pointerup={() => { |
| open = !open; |
| }} |
| > |
| <div |
| class="w-full font-medium flex items-center justify-between gap-2 {isExecuting |
| ? 'shimmer' |
| : ''}" |
| > |
| {#if isExecuting} |
| <div> |
| <Spinner className="size-4" /> |
| </div> |
| {/if} |
| |
| <div class=""> |
| {#if isDone} |
| <Markdown |
| id={`${componentId}-tool-call-title`} |
| content={$i18n.t('View Result from **{{NAME}}**', { |
| NAME: attributes.name |
| })} |
| /> |
| {:else} |
| <Markdown |
| id={`${componentId}-tool-call-executing`} |
| content={$i18n.t('Executing **{{NAME}}**...', { |
| NAME: attributes.name |
| })} |
| /> |
| {/if} |
| </div> |
| |
| <div class="flex self-center translate-y-[1px]"> |
| {#if open} |
| <ChevronUp strokeWidth="3.5" className="size-3.5" /> |
| {:else} |
| <ChevronDown strokeWidth="3.5" className="size-3.5" /> |
| {/if} |
| </div> |
| </div> |
| </div> |
| |
| {#if open} |
| <div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}> |
| {#if isDone} |
| <Markdown |
| id={`${componentId}-tool-call-result`} |
| content={`> \`\`\`json |
| > ${formatJSONString(args)} |
| > ${formatJSONString(result)} |
| > \`\`\``} |
| /> |
| {:else} |
| <Markdown |
| id={`${componentId}-tool-call-args`} |
| content={`> \`\`\`json |
| > ${formatJSONString(args)} |
| > \`\`\``} |
| /> |
| {/if} |
| </div> |
| {/if} |
| {/if} |
| |
| |
| {#if isDone} |
| {#if typeof files === 'object'} |
| {#each files ?? [] as file, idx} |
| {#if typeof file === 'string'} |
| {#if file.startsWith('data:image/')} |
| <Image id={`${componentId}-tool-call-result-${idx}`} src={file} alt="Image" /> |
| {/if} |
| {:else if typeof file === 'object'} |
| {#if (file.type === 'image' || (file?.content_type ?? '').startsWith('image/')) && file.url} |
| <Image id={`${componentId}-tool-call-result-${idx}`} src={file.url} alt="Image" /> |
| {/if} |
| {/if} |
| {/each} |
| {/if} |
| {/if} |
| </div> |
|
|