Collapsible.svelte 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. <script lang="ts">
  2. import { decode } from 'html-entities';
  3. import { v4 as uuidv4 } from 'uuid';
  4. import { getContext } from 'svelte';
  5. const i18n = getContext('i18n');
  6. import dayjs from '$lib/dayjs';
  7. import duration from 'dayjs/plugin/duration';
  8. import relativeTime from 'dayjs/plugin/relativeTime';
  9. dayjs.extend(duration);
  10. dayjs.extend(relativeTime);
  11. async function loadLocale(locales) {
  12. if (!locales || !Array.isArray(locales)) {
  13. return;
  14. }
  15. for (const locale of locales) {
  16. try {
  17. dayjs.locale(locale);
  18. break; // Stop after successfully loading the first available locale
  19. } catch (error) {
  20. console.error(`Could not load locale '${locale}':`, error);
  21. }
  22. }
  23. }
  24. // Assuming $i18n.languages is an array of language codes
  25. $: loadLocale($i18n.languages);
  26. import { slide } from 'svelte/transition';
  27. import { quintOut } from 'svelte/easing';
  28. import ChevronUp from '../icons/ChevronUp.svelte';
  29. import ChevronDown from '../icons/ChevronDown.svelte';
  30. import Spinner from './Spinner.svelte';
  31. import CodeBlock from '../chat/Messages/CodeBlock.svelte';
  32. import Markdown from '../chat/Messages/Markdown.svelte';
  33. import Image from './Image.svelte';
  34. import FullHeightIframe from './FullHeightIframe.svelte';
  35. import { settings } from '$lib/stores';
  36. export let open = false;
  37. export let className = '';
  38. export let buttonClassName =
  39. 'w-fit text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition';
  40. export let id = '';
  41. export let title = null;
  42. export let attributes = null;
  43. export let chevron = false;
  44. export let grow = false;
  45. export let disabled = false;
  46. export let hide = false;
  47. export let onChange: Function = () => {};
  48. $: onChange(open);
  49. const collapsibleId = uuidv4();
  50. function parseJSONString(str) {
  51. try {
  52. return parseJSONString(JSON.parse(str));
  53. } catch (e) {
  54. return str;
  55. }
  56. }
  57. function formatJSONString(str) {
  58. try {
  59. const parsed = parseJSONString(str);
  60. // If parsed is an object/array, then it's valid JSON
  61. if (typeof parsed === 'object') {
  62. return JSON.stringify(parsed, null, 2);
  63. } else {
  64. // It's a primitive value like a number, boolean, etc.
  65. return `${JSON.stringify(String(parsed))}`;
  66. }
  67. } catch (e) {
  68. // Not valid JSON, return as-is
  69. return str;
  70. }
  71. }
  72. </script>
  73. <div {id} class={className}>
  74. {#if title !== null}
  75. <!-- svelte-ignore a11y-no-static-element-interactions -->
  76. <!-- svelte-ignore a11y-click-events-have-key-events -->
  77. <div
  78. class="{buttonClassName} cursor-pointer"
  79. on:pointerup={() => {
  80. if (!disabled) {
  81. open = !open;
  82. }
  83. }}
  84. >
  85. <div
  86. class=" w-full font-medium flex items-center justify-between gap-2 {attributes?.done &&
  87. attributes?.done !== 'true'
  88. ? 'shimmer'
  89. : ''}
  90. "
  91. >
  92. {#if attributes?.done && attributes?.done !== 'true'}
  93. <div>
  94. <Spinner className="size-4" />
  95. </div>
  96. {/if}
  97. <div class="">
  98. {#if attributes?.type === 'reasoning'}
  99. {#if attributes?.done === 'true' && attributes?.duration}
  100. {#if attributes.duration < 1}
  101. {$i18n.t('Thought for less than a second')}
  102. {:else if attributes.duration < 60}
  103. {$i18n.t('Thought for {{DURATION}} seconds', {
  104. DURATION: attributes.duration
  105. })}
  106. {:else}
  107. {$i18n.t('Thought for {{DURATION}}', {
  108. DURATION: dayjs.duration(attributes.duration, 'seconds').humanize()
  109. })}
  110. {/if}
  111. {:else}
  112. {$i18n.t('Thinking...')}
  113. {/if}
  114. {:else if attributes?.type === 'code_interpreter'}
  115. {#if attributes?.done === 'true'}
  116. {$i18n.t('Analyzed')}
  117. {:else}
  118. {$i18n.t('Analyzing...')}
  119. {/if}
  120. {:else if attributes?.type === 'tool_calls'}
  121. {#if attributes?.done === 'true'}
  122. <Markdown
  123. id={`${collapsibleId}-tool-calls-${attributes?.id}`}
  124. content={$i18n.t('View Result from **{{NAME}}**', {
  125. NAME: attributes.name
  126. })}
  127. />
  128. {:else}
  129. <Markdown
  130. id={`${collapsibleId}-tool-calls-${attributes?.id}-executing`}
  131. content={$i18n.t('Executing **{{NAME}}**...', {
  132. NAME: attributes.name
  133. })}
  134. />
  135. {/if}
  136. {:else}
  137. {title}
  138. {/if}
  139. </div>
  140. <div class="flex self-center translate-y-[1px]">
  141. {#if open}
  142. <ChevronUp strokeWidth="3.5" className="size-3.5" />
  143. {:else}
  144. <ChevronDown strokeWidth="3.5" className="size-3.5" />
  145. {/if}
  146. </div>
  147. </div>
  148. </div>
  149. {:else}
  150. <!-- svelte-ignore a11y-no-static-element-interactions -->
  151. <!-- svelte-ignore a11y-click-events-have-key-events -->
  152. <div
  153. class="{buttonClassName} cursor-pointer"
  154. on:click={(e) => {
  155. e.stopPropagation();
  156. }}
  157. on:pointerup={(e) => {
  158. if (!disabled) {
  159. open = !open;
  160. }
  161. }}
  162. >
  163. <div>
  164. <div class="flex items-start justify-between">
  165. <slot />
  166. {#if chevron}
  167. <div class="flex self-start translate-y-1">
  168. {#if open}
  169. <ChevronUp strokeWidth="3.5" className="size-3.5" />
  170. {:else}
  171. <ChevronDown strokeWidth="3.5" className="size-3.5" />
  172. {/if}
  173. </div>
  174. {/if}
  175. </div>
  176. {#if grow}
  177. {#if open && !hide}
  178. <div
  179. transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}
  180. on:pointerup={(e) => {
  181. e.stopPropagation();
  182. }}
  183. >
  184. <slot name="content" />
  185. </div>
  186. {/if}
  187. {/if}
  188. </div>
  189. </div>
  190. {/if}
  191. {#if attributes?.type === 'tool_calls'}
  192. {@const args = decode(attributes?.arguments)}
  193. {@const result = decode(attributes?.result ?? '')}
  194. {@const files = parseJSONString(decode(attributes?.files ?? ''))}
  195. {@const embeds = parseJSONString(decode(attributes?.embeds ?? ''))}
  196. {#if embeds && Array.isArray(embeds) && embeds.length > 0}
  197. {#each embeds as embed, idx}
  198. <div class="my-2" id={`${collapsibleId}-tool-calls-${attributes?.id}-embed-${idx}`}>
  199. <FullHeightIframe
  200. src={embed}
  201. allowScripts={true}
  202. allowForms={$settings?.iframeSandboxAllowForms ?? false}
  203. allowSameOrigin={$settings?.iframeSandboxAllowSameOrigin ?? false}
  204. allowPopups={$settings?.iframeSandboxAllowPopups ?? false}
  205. />
  206. </div>
  207. {/each}
  208. {/if}
  209. {#if !grow}
  210. {#if open && !hide}
  211. <div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
  212. {#if attributes?.type === 'tool_calls'}
  213. {#if attributes?.done === 'true'}
  214. <Markdown
  215. id={`${collapsibleId}-tool-calls-${attributes?.id}-result`}
  216. content={`> \`\`\`json
  217. > ${formatJSONString(args)}
  218. > ${formatJSONString(result)}
  219. > \`\`\``}
  220. />
  221. {:else}
  222. <Markdown
  223. id={`${collapsibleId}-tool-calls-${attributes?.id}-result`}
  224. content={`> \`\`\`json
  225. > ${formatJSONString(args)}
  226. > \`\`\``}
  227. />
  228. {/if}
  229. {:else}
  230. <slot name="content" />
  231. {/if}
  232. </div>
  233. {/if}
  234. {#if attributes?.done === 'true'}
  235. {#if typeof files === 'object'}
  236. {#each files ?? [] as file, idx}
  237. {#if file.startsWith('data:image/')}
  238. <Image
  239. id={`${collapsibleId}-tool-calls-${attributes?.id}-result-${idx}`}
  240. src={file}
  241. alt="Image"
  242. />
  243. {/if}
  244. {/each}
  245. {/if}
  246. {/if}
  247. {/if}
  248. {:else if !grow}
  249. {#if open && !hide}
  250. <div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
  251. <slot name="content" />
  252. </div>
  253. {/if}
  254. {/if}
  255. </div>