Collapsible.svelte 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  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. export let open = false;
  35. export let className = '';
  36. export let buttonClassName =
  37. 'w-fit text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition';
  38. export let id = '';
  39. export let title = null;
  40. export let attributes = null;
  41. export let chevron = false;
  42. export let grow = false;
  43. export let disabled = false;
  44. export let hide = false;
  45. export let onChange: Function = () => {};
  46. $: onChange(open);
  47. const collapsibleId = uuidv4();
  48. function parseJSONString(str) {
  49. try {
  50. return parseJSONString(JSON.parse(str));
  51. } catch (e) {
  52. return str;
  53. }
  54. }
  55. function formatJSONString(str) {
  56. try {
  57. const parsed = parseJSONString(str);
  58. // If parsed is an object/array, then it's valid JSON
  59. if (typeof parsed === 'object') {
  60. return JSON.stringify(parsed, null, 2);
  61. } else {
  62. // It's a primitive value like a number, boolean, etc.
  63. return `${JSON.stringify(String(parsed))}`;
  64. }
  65. } catch (e) {
  66. // Not valid JSON, return as-is
  67. return str;
  68. }
  69. }
  70. </script>
  71. <div {id} class={className}>
  72. {#if title !== null}
  73. <!-- svelte-ignore a11y-no-static-element-interactions -->
  74. <!-- svelte-ignore a11y-click-events-have-key-events -->
  75. <div
  76. class="{buttonClassName} cursor-pointer"
  77. on:pointerup={() => {
  78. if (!disabled) {
  79. open = !open;
  80. }
  81. }}
  82. >
  83. <div
  84. class=" w-full font-medium flex items-center justify-between gap-2 {attributes?.done &&
  85. attributes?.done !== 'true'
  86. ? 'shimmer'
  87. : ''}
  88. "
  89. >
  90. {#if attributes?.done && attributes?.done !== 'true'}
  91. <div>
  92. <Spinner className="size-4" />
  93. </div>
  94. {/if}
  95. <div class="">
  96. {#if attributes?.type === 'reasoning'}
  97. {#if attributes?.done === 'true' && attributes?.duration}
  98. {#if attributes.duration < 1}
  99. {$i18n.t('Thought for less than a second')}
  100. {:else if attributes.duration < 60}
  101. {$i18n.t('Thought for {{DURATION}} seconds', {
  102. DURATION: attributes.duration
  103. })}
  104. {:else}
  105. {$i18n.t('Thought for {{DURATION}}', {
  106. DURATION: dayjs.duration(attributes.duration, 'seconds').humanize()
  107. })}
  108. {/if}
  109. {:else}
  110. {$i18n.t('Thinking...')}
  111. {/if}
  112. {:else if attributes?.type === 'code_interpreter'}
  113. {#if attributes?.done === 'true'}
  114. {$i18n.t('Analyzed')}
  115. {:else}
  116. {$i18n.t('Analyzing...')}
  117. {/if}
  118. {:else if attributes?.type === 'tool_calls'}
  119. {#if attributes?.done === 'true'}
  120. <Markdown
  121. id={`${collapsibleId}-tool-calls-${attributes?.id}`}
  122. content={$i18n.t('View Result from **{{NAME}}**', {
  123. NAME: attributes.name
  124. })}
  125. />
  126. {:else}
  127. <Markdown
  128. id={`${collapsibleId}-tool-calls-${attributes?.id}-executing`}
  129. content={$i18n.t('Executing **{{NAME}}**...', {
  130. NAME: attributes.name
  131. })}
  132. />
  133. {/if}
  134. {:else}
  135. {title}
  136. {/if}
  137. </div>
  138. <div class="flex self-center translate-y-[1px]">
  139. {#if open}
  140. <ChevronUp strokeWidth="3.5" className="size-3.5" />
  141. {:else}
  142. <ChevronDown strokeWidth="3.5" className="size-3.5" />
  143. {/if}
  144. </div>
  145. </div>
  146. </div>
  147. {:else}
  148. <!-- svelte-ignore a11y-no-static-element-interactions -->
  149. <!-- svelte-ignore a11y-click-events-have-key-events -->
  150. <div
  151. class="{buttonClassName} cursor-pointer"
  152. on:click={(e) => {
  153. e.stopPropagation();
  154. }}
  155. on:pointerup={(e) => {
  156. if (!disabled) {
  157. open = !open;
  158. }
  159. }}
  160. >
  161. <div>
  162. <div class="flex items-start justify-between">
  163. <slot />
  164. {#if chevron}
  165. <div class="flex self-start translate-y-1">
  166. {#if open}
  167. <ChevronUp strokeWidth="3.5" className="size-3.5" />
  168. {:else}
  169. <ChevronDown strokeWidth="3.5" className="size-3.5" />
  170. {/if}
  171. </div>
  172. {/if}
  173. </div>
  174. {#if grow}
  175. {#if open && !hide}
  176. <div
  177. transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}
  178. on:pointerup={(e) => {
  179. e.stopPropagation();
  180. }}
  181. >
  182. <slot name="content" />
  183. </div>
  184. {/if}
  185. {/if}
  186. </div>
  187. </div>
  188. {/if}
  189. {#if attributes?.type === 'tool_calls'}
  190. {@const args = decode(attributes?.arguments)}
  191. {@const result = decode(attributes?.result ?? '')}
  192. {@const files = parseJSONString(decode(attributes?.files ?? ''))}
  193. {#if !grow}
  194. {#if open && !hide}
  195. <div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
  196. {#if attributes?.type === 'tool_calls'}
  197. {#if attributes?.done === 'true'}
  198. <Markdown
  199. id={`${collapsibleId}-tool-calls-${attributes?.id}-result`}
  200. content={`> \`\`\`json
  201. > ${formatJSONString(args)}
  202. > ${formatJSONString(result)}
  203. > \`\`\``}
  204. />
  205. {:else}
  206. <Markdown
  207. id={`${collapsibleId}-tool-calls-${attributes?.id}-result`}
  208. content={`> \`\`\`json
  209. > ${formatJSONString(args)}
  210. > \`\`\``}
  211. />
  212. {/if}
  213. {:else}
  214. <slot name="content" />
  215. {/if}
  216. </div>
  217. {/if}
  218. {#if attributes?.done === 'true'}
  219. {#if typeof files === 'object'}
  220. {#each files ?? [] as file, idx}
  221. {#if file.startsWith('data:image/')}
  222. <Image
  223. id={`${collapsibleId}-tool-calls-${attributes?.id}-result-${idx}`}
  224. src={file}
  225. alt="Image"
  226. />
  227. {/if}
  228. {/each}
  229. {/if}
  230. {/if}
  231. {/if}
  232. {:else if !grow}
  233. {#if open && !hide}
  234. <div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
  235. <slot name="content" />
  236. </div>
  237. {/if}
  238. {/if}
  239. </div>