123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281 |
- <script lang="ts">
- import { decode } from 'html-entities';
- import { v4 as uuidv4 } from 'uuid';
- import { getContext } from 'svelte';
- const i18n = getContext('i18n');
- import dayjs from '$lib/dayjs';
- import duration from 'dayjs/plugin/duration';
- import relativeTime from 'dayjs/plugin/relativeTime';
- dayjs.extend(duration);
- dayjs.extend(relativeTime);
- async function loadLocale(locales) {
- if (!locales || !Array.isArray(locales)) {
- return;
- }
- for (const locale of locales) {
- try {
- dayjs.locale(locale);
- break; // Stop after successfully loading the first available locale
- } catch (error) {
- console.error(`Could not load locale '${locale}':`, error);
- }
- }
- }
- // Assuming $i18n.languages is an array of language codes
- $: loadLocale($i18n.languages);
- 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 CodeBlock from '../chat/Messages/CodeBlock.svelte';
- import Markdown from '../chat/Messages/Markdown.svelte';
- import Image from './Image.svelte';
- import FullHeightIframe from './FullHeightIframe.svelte';
- import { settings } from '$lib/stores';
- 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';
- export let id = '';
- export let title = null;
- export let attributes = null;
- export let chevron = false;
- export let grow = false;
- export let disabled = false;
- export let hide = false;
- export let onChange: Function = () => {};
- $: onChange(open);
- const collapsibleId = uuidv4();
- function parseJSONString(str) {
- try {
- return parseJSONString(JSON.parse(str));
- } catch (e) {
- return str;
- }
- }
- function formatJSONString(str) {
- try {
- const parsed = parseJSONString(str);
- // If parsed is an object/array, then it's valid JSON
- if (typeof parsed === 'object') {
- return JSON.stringify(parsed, null, 2);
- } else {
- // It's a primitive value like a number, boolean, etc.
- return `${JSON.stringify(String(parsed))}`;
- }
- } catch (e) {
- // Not valid JSON, return as-is
- return str;
- }
- }
- </script>
- <div {id} class={className}>
- {#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}
- <div class="">
- {#if attributes?.type === 'reasoning'}
- {#if attributes?.done === 'true' && attributes?.duration}
- {#if attributes.duration < 1}
- {$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 if attributes?.type === 'tool_calls'}
- {#if attributes?.done === 'true'}
- <Markdown
- id={`${collapsibleId}-tool-calls-${attributes?.id}`}
- content={$i18n.t('View Result from **{{NAME}}**', {
- NAME: attributes.name
- })}
- />
- {:else}
- <Markdown
- id={`${collapsibleId}-tool-calls-${attributes?.id}-executing`}
- content={$i18n.t('Executing **{{NAME}}**...', {
- NAME: attributes.name
- })}
- />
- {/if}
- {:else}
- {title}
- {/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>
- {: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 attributes?.type === 'tool_calls'}
- {@const args = decode(attributes?.arguments)}
- {@const result = decode(attributes?.result ?? '')}
- {@const files = parseJSONString(decode(attributes?.files ?? ''))}
- {@const embeds = parseJSONString(decode(attributes?.embeds ?? ''))}
- {#if embeds && Array.isArray(embeds) && embeds.length > 0}
- {#each embeds as embed, idx}
- <div class="my-2" id={`${collapsibleId}-tool-calls-${attributes?.id}-embed-${idx}`}>
- <FullHeightIframe
- src={embed}
- allowScripts={true}
- allowForms={$settings?.iframeSandboxAllowForms ?? false}
- allowSameOrigin={$settings?.iframeSandboxAllowSameOrigin ?? false}
- allowPopups={$settings?.iframeSandboxAllowPopups ?? false}
- />
- </div>
- {/each}
- {/if}
- {#if !grow}
- {#if open && !hide}
- <div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
- {#if attributes?.type === 'tool_calls'}
- {#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>
- {/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}
- </div>
|