ResponseMessage.svelte 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003
  1. <script lang="ts">
  2. import { toast } from 'svelte-sonner';
  3. import dayjs from 'dayjs';
  4. import { marked } from 'marked';
  5. import { fade } from 'svelte/transition';
  6. import { createEventDispatcher } from 'svelte';
  7. import { onMount, tick, getContext } from 'svelte';
  8. const i18n = getContext('i18n');
  9. const dispatch = createEventDispatcher();
  10. import { config, models, settings, user } from '$lib/stores';
  11. import { synthesizeOpenAISpeech } from '$lib/apis/audio';
  12. import { imageGenerations } from '$lib/apis/images';
  13. import {
  14. approximateToHumanReadable,
  15. extractSentences,
  16. replaceTokens,
  17. processResponseContent
  18. } from '$lib/utils';
  19. import { WEBUI_BASE_URL } from '$lib/constants';
  20. import Name from './Name.svelte';
  21. import ProfileImage from './ProfileImage.svelte';
  22. import Skeleton from './Skeleton.svelte';
  23. import CodeBlock from './CodeBlock.svelte';
  24. import Image from '$lib/components/common/Image.svelte';
  25. import Tooltip from '$lib/components/common/Tooltip.svelte';
  26. import RateComment from './RateComment.svelte';
  27. import CitationsModal from '$lib/components/chat/Messages/CitationsModal.svelte';
  28. import Spinner from '$lib/components/common/Spinner.svelte';
  29. import WebSearchResults from './ResponseMessage/WebSearchResults.svelte';
  30. import Sparkles from '$lib/components/icons/Sparkles.svelte';
  31. import MarkdownTokens from './MarkdownTokens.svelte';
  32. export let message;
  33. export let siblings;
  34. export let isLastMessage = true;
  35. export let readOnly = false;
  36. export let updateChatMessages: Function;
  37. export let confirmEditResponseMessage: Function;
  38. export let showPreviousMessage: Function;
  39. export let showNextMessage: Function;
  40. export let rateMessage: Function;
  41. export let copyToClipboard: Function;
  42. export let continueGeneration: Function;
  43. export let regenerateResponse: Function;
  44. let model = null;
  45. $: model = $models.find((m) => m.id === message.model);
  46. let edit = false;
  47. let editedContent = '';
  48. let editTextAreaElement: HTMLTextAreaElement;
  49. let tooltipInstance = null;
  50. let sentencesAudio = {};
  51. let speaking = null;
  52. let speakingIdx = null;
  53. let loadingSpeech = false;
  54. let generatingImage = false;
  55. let showRateComment = false;
  56. let showCitationModal = false;
  57. let selectedCitation = null;
  58. let tokens;
  59. import 'katex/dist/katex.min.css';
  60. import markedKatex from '$lib/utils/katex-extension';
  61. const options = {
  62. throwOnError: false
  63. };
  64. marked.use(markedKatex(options));
  65. $: (async () => {
  66. if (message?.content) {
  67. tokens = marked.lexer(
  68. replaceTokens(processResponseContent(message?.content), model?.name, $user?.name)
  69. );
  70. }
  71. })();
  72. const playAudio = (idx) => {
  73. return new Promise((res) => {
  74. speakingIdx = idx;
  75. const audio = sentencesAudio[idx];
  76. audio.play();
  77. audio.onended = async (e) => {
  78. await new Promise((r) => setTimeout(r, 300));
  79. if (Object.keys(sentencesAudio).length - 1 === idx) {
  80. speaking = null;
  81. }
  82. res(e);
  83. };
  84. });
  85. };
  86. const toggleSpeakMessage = async () => {
  87. if (speaking) {
  88. try {
  89. speechSynthesis.cancel();
  90. sentencesAudio[speakingIdx].pause();
  91. sentencesAudio[speakingIdx].currentTime = 0;
  92. } catch {}
  93. speaking = null;
  94. speakingIdx = null;
  95. } else {
  96. if ((message?.content ?? '').trim() !== '') {
  97. speaking = true;
  98. if ($config.audio.tts.engine !== '') {
  99. loadingSpeech = true;
  100. const sentences = extractSentences(message.content).reduce((mergedTexts, currentText) => {
  101. const lastIndex = mergedTexts.length - 1;
  102. if (lastIndex >= 0) {
  103. const previousText = mergedTexts[lastIndex];
  104. const wordCount = previousText.split(/\s+/).length;
  105. if (wordCount < 2) {
  106. mergedTexts[lastIndex] = previousText + ' ' + currentText;
  107. } else {
  108. mergedTexts.push(currentText);
  109. }
  110. } else {
  111. mergedTexts.push(currentText);
  112. }
  113. return mergedTexts;
  114. }, []);
  115. console.log(sentences);
  116. if (sentences.length > 0) {
  117. sentencesAudio = sentences.reduce((a, e, i, arr) => {
  118. a[i] = null;
  119. return a;
  120. }, {});
  121. let lastPlayedAudioPromise = Promise.resolve(); // Initialize a promise that resolves immediately
  122. for (const [idx, sentence] of sentences.entries()) {
  123. const res = await synthesizeOpenAISpeech(
  124. localStorage.token,
  125. $settings?.audio?.tts?.defaultVoice === $config.audio.tts.voice
  126. ? ($settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice)
  127. : $config?.audio?.tts?.voice,
  128. sentence
  129. ).catch((error) => {
  130. toast.error(error);
  131. speaking = null;
  132. loadingSpeech = false;
  133. return null;
  134. });
  135. if (res) {
  136. const blob = await res.blob();
  137. const blobUrl = URL.createObjectURL(blob);
  138. const audio = new Audio(blobUrl);
  139. sentencesAudio[idx] = audio;
  140. loadingSpeech = false;
  141. lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
  142. }
  143. }
  144. } else {
  145. speaking = null;
  146. loadingSpeech = false;
  147. }
  148. } else {
  149. let voices = [];
  150. const getVoicesLoop = setInterval(async () => {
  151. voices = await speechSynthesis.getVoices();
  152. if (voices.length > 0) {
  153. clearInterval(getVoicesLoop);
  154. const voice =
  155. voices
  156. ?.filter(
  157. (v) =>
  158. v.voiceURI === ($settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice)
  159. )
  160. ?.at(0) ?? undefined;
  161. console.log(voice);
  162. const speak = new SpeechSynthesisUtterance(message.content);
  163. console.log(speak);
  164. speak.onend = () => {
  165. speaking = null;
  166. if ($settings.conversationMode) {
  167. document.getElementById('voice-input-button')?.click();
  168. }
  169. };
  170. if (voice) {
  171. speak.voice = voice;
  172. }
  173. speechSynthesis.speak(speak);
  174. }
  175. }, 100);
  176. }
  177. } else {
  178. toast.error($i18n.t('No content to speak'));
  179. }
  180. }
  181. };
  182. const editMessageHandler = async () => {
  183. edit = true;
  184. editedContent = message.content;
  185. await tick();
  186. editTextAreaElement.style.height = '';
  187. editTextAreaElement.style.height = `${editTextAreaElement.scrollHeight}px`;
  188. };
  189. const editMessageConfirmHandler = async () => {
  190. if (editedContent === '') {
  191. editedContent = ' ';
  192. }
  193. confirmEditResponseMessage(message.id, editedContent);
  194. edit = false;
  195. editedContent = '';
  196. await tick();
  197. };
  198. const cancelEditMessage = async () => {
  199. edit = false;
  200. editedContent = '';
  201. await tick();
  202. };
  203. const generateImage = async (message) => {
  204. generatingImage = true;
  205. const res = await imageGenerations(localStorage.token, message.content).catch((error) => {
  206. toast.error(error);
  207. });
  208. console.log(res);
  209. if (res) {
  210. message.files = res.map((image) => ({
  211. type: 'image',
  212. url: `${image.url}`
  213. }));
  214. dispatch('save', message);
  215. }
  216. generatingImage = false;
  217. };
  218. $: if (!edit) {
  219. (async () => {
  220. await tick();
  221. })();
  222. }
  223. onMount(async () => {
  224. await tick();
  225. });
  226. </script>
  227. <CitationsModal bind:show={showCitationModal} citation={selectedCitation} />
  228. {#key message.id}
  229. <div
  230. class=" flex w-full message-{message.id}"
  231. id="message-{message.id}"
  232. dir={$settings.chatDirection}
  233. >
  234. <ProfileImage
  235. src={model?.info?.meta?.profile_image_url ??
  236. ($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
  237. />
  238. <div class="w-full overflow-hidden pl-1">
  239. <Name>
  240. {model?.name ?? message.model}
  241. {#if message.timestamp}
  242. <span
  243. class=" self-center invisible group-hover:visible text-gray-400 text-xs font-medium uppercase"
  244. >
  245. {dayjs(message.timestamp * 1000).format($i18n.t('h:mm a'))}
  246. </span>
  247. {/if}
  248. </Name>
  249. <div>
  250. {#if (message?.files ?? []).filter((f) => f.type === 'image').length > 0}
  251. <div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap">
  252. {#each message.files as file}
  253. <div>
  254. {#if file.type === 'image'}
  255. <Image src={file.url} />
  256. {/if}
  257. </div>
  258. {/each}
  259. </div>
  260. {/if}
  261. <div
  262. class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line"
  263. >
  264. <div>
  265. {#if (message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]).length > 0}
  266. {@const status = (
  267. message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]
  268. ).at(-1)}
  269. <div class="flex items-center gap-2 pt-0.5 pb-1">
  270. {#if status.done === false}
  271. <div class="">
  272. <Spinner className="size-4" />
  273. </div>
  274. {/if}
  275. {#if status?.action === 'web_search' && status?.urls}
  276. <WebSearchResults {status}>
  277. <div class="flex flex-col justify-center -space-y-0.5">
  278. <div class="text-base line-clamp-1 text-wrap">
  279. {status?.description}
  280. </div>
  281. </div>
  282. </WebSearchResults>
  283. {:else}
  284. <div class="flex flex-col justify-center -space-y-0.5">
  285. <div class=" text-gray-500 dark:text-gray-500 text-base line-clamp-1 text-wrap">
  286. {status?.description}
  287. </div>
  288. </div>
  289. {/if}
  290. </div>
  291. {/if}
  292. {#if edit === true}
  293. <div class="w-full bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 my-2">
  294. <textarea
  295. id="message-edit-{message.id}"
  296. bind:this={editTextAreaElement}
  297. class=" bg-transparent outline-none w-full resize-none"
  298. bind:value={editedContent}
  299. on:input={(e) => {
  300. e.target.style.height = '';
  301. e.target.style.height = `${e.target.scrollHeight}px`;
  302. }}
  303. on:keydown={(e) => {
  304. if (e.key === 'Escape') {
  305. document.getElementById('close-edit-message-button')?.click();
  306. }
  307. const isCmdOrCtrlPressed = e.metaKey || e.ctrlKey;
  308. const isEnterPressed = e.key === 'Enter';
  309. if (isCmdOrCtrlPressed && isEnterPressed) {
  310. document.getElementById('save-edit-message-button')?.click();
  311. }
  312. }}
  313. />
  314. <div class=" mt-2 mb-1 flex justify-end space-x-1.5 text-sm font-medium">
  315. <button
  316. id="close-edit-message-button"
  317. class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-800 transition rounded-3xl"
  318. on:click={() => {
  319. cancelEditMessage();
  320. }}
  321. >
  322. {$i18n.t('Cancel')}
  323. </button>
  324. <button
  325. id="save-edit-message-button"
  326. class=" px-4 py-2 bg-gray-900 hover:bg-gray-850 text-gray-100 transition rounded-3xl"
  327. on:click={() => {
  328. editMessageConfirmHandler();
  329. }}
  330. >
  331. {$i18n.t('Save')}
  332. </button>
  333. </div>
  334. </div>
  335. {:else}
  336. <div class="w-full flex flex-col">
  337. {#if message.content === '' && !message.error}
  338. <Skeleton />
  339. {:else if message.content && message.error !== true}
  340. <!-- always show message contents even if there's an error -->
  341. <!-- unless message.error === true which is legacy error handling, where the error message is stored in message.content -->
  342. {#key message.id}
  343. <MarkdownTokens id={message.id} {tokens} />
  344. {/key}
  345. {/if}
  346. {#if message.error}
  347. <div
  348. class="flex mt-2 mb-4 space-x-2 border px-4 py-3 border-red-800 bg-red-800/30 font-medium rounded-lg"
  349. >
  350. <svg
  351. xmlns="http://www.w3.org/2000/svg"
  352. fill="none"
  353. viewBox="0 0 24 24"
  354. stroke-width="1.5"
  355. stroke="currentColor"
  356. class="w-5 h-5 self-center"
  357. >
  358. <path
  359. stroke-linecap="round"
  360. stroke-linejoin="round"
  361. d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
  362. />
  363. </svg>
  364. <div class=" self-center">
  365. {message?.error?.content ?? message.content}
  366. </div>
  367. </div>
  368. {/if}
  369. {#if message.citations}
  370. <div class="mt-1 mb-2 w-full flex gap-1 items-center flex-wrap">
  371. {#each message.citations.reduce((acc, citation) => {
  372. citation.document.forEach((document, index) => {
  373. const metadata = citation.metadata?.[index];
  374. const id = metadata?.source ?? 'N/A';
  375. let source = citation?.source;
  376. if (metadata?.name) {
  377. source = { ...source, name: metadata.name };
  378. }
  379. // Check if ID looks like a URL
  380. if (id.startsWith('http://') || id.startsWith('https://')) {
  381. source = { name: id };
  382. }
  383. const existingSource = acc.find((item) => item.id === id);
  384. if (existingSource) {
  385. existingSource.document.push(document);
  386. existingSource.metadata.push(metadata);
  387. } else {
  388. acc.push( { id: id, source: source, document: [document], metadata: metadata ? [metadata] : [] } );
  389. }
  390. });
  391. return acc;
  392. }, []) as citation, idx}
  393. <div class="flex gap-1 text-xs font-semibold">
  394. <button
  395. class="flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl"
  396. on:click={() => {
  397. showCitationModal = true;
  398. selectedCitation = citation;
  399. }}
  400. >
  401. <div class="bg-white dark:bg-gray-700 rounded-full size-4">
  402. {idx + 1}
  403. </div>
  404. <div class="flex-1 mx-2 line-clamp-1">
  405. {citation.source.name}
  406. </div>
  407. </button>
  408. </div>
  409. {/each}
  410. </div>
  411. {/if}
  412. </div>
  413. {/if}
  414. </div>
  415. </div>
  416. {#if !edit}
  417. {#if message.done || siblings.length > 1}
  418. <div
  419. class=" flex justify-start overflow-x-auto buttons text-gray-600 dark:text-gray-500"
  420. >
  421. {#if siblings.length > 1}
  422. <div class="flex self-center min-w-fit" dir="ltr">
  423. <button
  424. class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
  425. on:click={() => {
  426. showPreviousMessage(message);
  427. }}
  428. >
  429. <svg
  430. xmlns="http://www.w3.org/2000/svg"
  431. fill="none"
  432. viewBox="0 0 24 24"
  433. stroke="currentColor"
  434. stroke-width="2.5"
  435. class="size-3.5"
  436. >
  437. <path
  438. stroke-linecap="round"
  439. stroke-linejoin="round"
  440. d="M15.75 19.5 8.25 12l7.5-7.5"
  441. />
  442. </svg>
  443. </button>
  444. <div
  445. class="text-sm tracking-widest font-semibold self-center dark:text-gray-100 min-w-fit"
  446. >
  447. {siblings.indexOf(message.id) + 1}/{siblings.length}
  448. </div>
  449. <button
  450. class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
  451. on:click={() => {
  452. showNextMessage(message);
  453. }}
  454. >
  455. <svg
  456. xmlns="http://www.w3.org/2000/svg"
  457. fill="none"
  458. viewBox="0 0 24 24"
  459. stroke="currentColor"
  460. stroke-width="2.5"
  461. class="size-3.5"
  462. >
  463. <path
  464. stroke-linecap="round"
  465. stroke-linejoin="round"
  466. d="m8.25 4.5 7.5 7.5-7.5 7.5"
  467. />
  468. </svg>
  469. </button>
  470. </div>
  471. {/if}
  472. {#if message.done}
  473. {#if !readOnly}
  474. <Tooltip content={$i18n.t('Edit')} placement="bottom">
  475. <button
  476. class="{isLastMessage
  477. ? 'visible'
  478. : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
  479. on:click={() => {
  480. editMessageHandler();
  481. }}
  482. >
  483. <svg
  484. xmlns="http://www.w3.org/2000/svg"
  485. fill="none"
  486. viewBox="0 0 24 24"
  487. stroke-width="2.3"
  488. stroke="currentColor"
  489. class="w-4 h-4"
  490. >
  491. <path
  492. stroke-linecap="round"
  493. stroke-linejoin="round"
  494. d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
  495. />
  496. </svg>
  497. </button>
  498. </Tooltip>
  499. {/if}
  500. <Tooltip content={$i18n.t('Copy')} placement="bottom">
  501. <button
  502. class="{isLastMessage
  503. ? 'visible'
  504. : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition copy-response-button"
  505. on:click={() => {
  506. copyToClipboard(message.content);
  507. }}
  508. >
  509. <svg
  510. xmlns="http://www.w3.org/2000/svg"
  511. fill="none"
  512. viewBox="0 0 24 24"
  513. stroke-width="2.3"
  514. stroke="currentColor"
  515. class="w-4 h-4"
  516. >
  517. <path
  518. stroke-linecap="round"
  519. stroke-linejoin="round"
  520. d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
  521. />
  522. </svg>
  523. </button>
  524. </Tooltip>
  525. <Tooltip content={$i18n.t('Read Aloud')} placement="bottom">
  526. <button
  527. id="speak-button-{message.id}"
  528. class="{isLastMessage
  529. ? 'visible'
  530. : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
  531. on:click={() => {
  532. if (!loadingSpeech) {
  533. toggleSpeakMessage(message);
  534. }
  535. }}
  536. >
  537. {#if loadingSpeech}
  538. <svg
  539. class=" w-4 h-4"
  540. fill="currentColor"
  541. viewBox="0 0 24 24"
  542. xmlns="http://www.w3.org/2000/svg"
  543. ><style>
  544. .spinner_S1WN {
  545. animation: spinner_MGfb 0.8s linear infinite;
  546. animation-delay: -0.8s;
  547. }
  548. .spinner_Km9P {
  549. animation-delay: -0.65s;
  550. }
  551. .spinner_JApP {
  552. animation-delay: -0.5s;
  553. }
  554. @keyframes spinner_MGfb {
  555. 93.75%,
  556. 100% {
  557. opacity: 0.2;
  558. }
  559. }
  560. </style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle
  561. class="spinner_S1WN spinner_Km9P"
  562. cx="12"
  563. cy="12"
  564. r="3"
  565. /><circle class="spinner_S1WN spinner_JApP" cx="20" cy="12" r="3" /></svg
  566. >
  567. {:else if speaking}
  568. <svg
  569. xmlns="http://www.w3.org/2000/svg"
  570. fill="none"
  571. viewBox="0 0 24 24"
  572. stroke-width="2.3"
  573. stroke="currentColor"
  574. class="w-4 h-4"
  575. >
  576. <path
  577. stroke-linecap="round"
  578. stroke-linejoin="round"
  579. d="M17.25 9.75 19.5 12m0 0 2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6 4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z"
  580. />
  581. </svg>
  582. {:else}
  583. <svg
  584. xmlns="http://www.w3.org/2000/svg"
  585. fill="none"
  586. viewBox="0 0 24 24"
  587. stroke-width="2.3"
  588. stroke="currentColor"
  589. class="w-4 h-4"
  590. >
  591. <path
  592. stroke-linecap="round"
  593. stroke-linejoin="round"
  594. d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z"
  595. />
  596. </svg>
  597. {/if}
  598. </button>
  599. </Tooltip>
  600. {#if $config?.features.enable_image_generation && !readOnly}
  601. <Tooltip content={$i18n.t('Generate Image')} placement="bottom">
  602. <button
  603. class="{isLastMessage
  604. ? 'visible'
  605. : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
  606. on:click={() => {
  607. if (!generatingImage) {
  608. generateImage(message);
  609. }
  610. }}
  611. >
  612. {#if generatingImage}
  613. <svg
  614. class=" w-4 h-4"
  615. fill="currentColor"
  616. viewBox="0 0 24 24"
  617. xmlns="http://www.w3.org/2000/svg"
  618. ><style>
  619. .spinner_S1WN {
  620. animation: spinner_MGfb 0.8s linear infinite;
  621. animation-delay: -0.8s;
  622. }
  623. .spinner_Km9P {
  624. animation-delay: -0.65s;
  625. }
  626. .spinner_JApP {
  627. animation-delay: -0.5s;
  628. }
  629. @keyframes spinner_MGfb {
  630. 93.75%,
  631. 100% {
  632. opacity: 0.2;
  633. }
  634. }
  635. </style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle
  636. class="spinner_S1WN spinner_Km9P"
  637. cx="12"
  638. cy="12"
  639. r="3"
  640. /><circle class="spinner_S1WN spinner_JApP" cx="20" cy="12" r="3" /></svg
  641. >
  642. {:else}
  643. <svg
  644. xmlns="http://www.w3.org/2000/svg"
  645. fill="none"
  646. viewBox="0 0 24 24"
  647. stroke-width="2.3"
  648. stroke="currentColor"
  649. class="w-4 h-4"
  650. >
  651. <path
  652. stroke-linecap="round"
  653. stroke-linejoin="round"
  654. d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"
  655. />
  656. </svg>
  657. {/if}
  658. </button>
  659. </Tooltip>
  660. {/if}
  661. {#if message.info}
  662. <Tooltip
  663. content={message.info.openai
  664. ? `prompt_tokens: ${message.info.prompt_tokens ?? 'N/A'}<br/>
  665. completion_tokens: ${message.info.completion_tokens ?? 'N/A'}<br/>
  666. total_tokens: ${message.info.total_tokens ?? 'N/A'}`
  667. : `response_token/s: ${
  668. `${
  669. Math.round(
  670. ((message.info.eval_count ?? 0) /
  671. (message.info.eval_duration / 1000000000)) *
  672. 100
  673. ) / 100
  674. } tokens` ?? 'N/A'
  675. }<br/>
  676. prompt_token/s: ${
  677. Math.round(
  678. ((message.info.prompt_eval_count ?? 0) /
  679. (message.info.prompt_eval_duration / 1000000000)) *
  680. 100
  681. ) / 100 ?? 'N/A'
  682. } tokens<br/>
  683. total_duration: ${
  684. Math.round(((message.info.total_duration ?? 0) / 1000000) * 100) / 100 ?? 'N/A'
  685. }ms<br/>
  686. load_duration: ${
  687. Math.round(((message.info.load_duration ?? 0) / 1000000) * 100) / 100 ?? 'N/A'
  688. }ms<br/>
  689. prompt_eval_count: ${message.info.prompt_eval_count ?? 'N/A'}<br/>
  690. prompt_eval_duration: ${
  691. Math.round(((message.info.prompt_eval_duration ?? 0) / 1000000) * 100) / 100 ??
  692. 'N/A'
  693. }ms<br/>
  694. eval_count: ${message.info.eval_count ?? 'N/A'}<br/>
  695. eval_duration: ${
  696. Math.round(((message.info.eval_duration ?? 0) / 1000000) * 100) / 100 ?? 'N/A'
  697. }ms<br/>
  698. approximate_total: ${approximateToHumanReadable(message.info.total_duration)}`}
  699. placement="top"
  700. >
  701. <Tooltip content={$i18n.t('Generation Info')} placement="bottom">
  702. <button
  703. class=" {isLastMessage
  704. ? 'visible'
  705. : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition whitespace-pre-wrap"
  706. on:click={() => {
  707. console.log(message);
  708. }}
  709. id="info-{message.id}"
  710. >
  711. <svg
  712. xmlns="http://www.w3.org/2000/svg"
  713. fill="none"
  714. viewBox="0 0 24 24"
  715. stroke-width="2.3"
  716. stroke="currentColor"
  717. class="w-4 h-4"
  718. >
  719. <path
  720. stroke-linecap="round"
  721. stroke-linejoin="round"
  722. d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
  723. />
  724. </svg>
  725. </button>
  726. </Tooltip>
  727. </Tooltip>
  728. {/if}
  729. {#if !readOnly}
  730. <Tooltip content={$i18n.t('Good Response')} placement="bottom">
  731. <button
  732. class="{isLastMessage
  733. ? 'visible'
  734. : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {(message
  735. ?.annotation?.rating ?? null) === 1
  736. ? 'bg-gray-100 dark:bg-gray-800'
  737. : ''} dark:hover:text-white hover:text-black transition"
  738. on:click={() => {
  739. rateMessage(message.id, 1);
  740. showRateComment = true;
  741. window.setTimeout(() => {
  742. document
  743. .getElementById(`message-feedback-${message.id}`)
  744. ?.scrollIntoView();
  745. }, 0);
  746. }}
  747. >
  748. <svg
  749. stroke="currentColor"
  750. fill="none"
  751. stroke-width="2.3"
  752. viewBox="0 0 24 24"
  753. stroke-linecap="round"
  754. stroke-linejoin="round"
  755. class="w-4 h-4"
  756. xmlns="http://www.w3.org/2000/svg"
  757. ><path
  758. d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
  759. /></svg
  760. >
  761. </button>
  762. </Tooltip>
  763. <Tooltip content={$i18n.t('Bad Response')} placement="bottom">
  764. <button
  765. class="{isLastMessage
  766. ? 'visible'
  767. : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {(message
  768. ?.annotation?.rating ?? null) === -1
  769. ? 'bg-gray-100 dark:bg-gray-800'
  770. : ''} dark:hover:text-white hover:text-black transition"
  771. on:click={() => {
  772. rateMessage(message.id, -1);
  773. showRateComment = true;
  774. window.setTimeout(() => {
  775. document
  776. .getElementById(`message-feedback-${message.id}`)
  777. ?.scrollIntoView();
  778. }, 0);
  779. }}
  780. >
  781. <svg
  782. stroke="currentColor"
  783. fill="none"
  784. stroke-width="2.3"
  785. viewBox="0 0 24 24"
  786. stroke-linecap="round"
  787. stroke-linejoin="round"
  788. class="w-4 h-4"
  789. xmlns="http://www.w3.org/2000/svg"
  790. ><path
  791. d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
  792. /></svg
  793. >
  794. </button>
  795. </Tooltip>
  796. {#if isLastMessage}
  797. <Tooltip content={$i18n.t('Continue Response')} placement="bottom">
  798. <button
  799. type="button"
  800. class="{isLastMessage
  801. ? 'visible'
  802. : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
  803. on:click={() => {
  804. continueGeneration();
  805. }}
  806. >
  807. <svg
  808. xmlns="http://www.w3.org/2000/svg"
  809. fill="none"
  810. viewBox="0 0 24 24"
  811. stroke-width="2.3"
  812. stroke="currentColor"
  813. class="w-4 h-4"
  814. >
  815. <path
  816. stroke-linecap="round"
  817. stroke-linejoin="round"
  818. d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
  819. />
  820. <path
  821. stroke-linecap="round"
  822. stroke-linejoin="round"
  823. d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z"
  824. />
  825. </svg>
  826. </button>
  827. </Tooltip>
  828. <Tooltip content={$i18n.t('Regenerate')} placement="bottom">
  829. <button
  830. type="button"
  831. class="{isLastMessage
  832. ? 'visible'
  833. : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
  834. on:click={() => {
  835. showRateComment = false;
  836. regenerateResponse(message);
  837. }}
  838. >
  839. <svg
  840. xmlns="http://www.w3.org/2000/svg"
  841. fill="none"
  842. viewBox="0 0 24 24"
  843. stroke-width="2.3"
  844. stroke="currentColor"
  845. class="w-4 h-4"
  846. >
  847. <path
  848. stroke-linecap="round"
  849. stroke-linejoin="round"
  850. d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
  851. />
  852. </svg>
  853. </button>
  854. </Tooltip>
  855. {#each model?.actions ?? [] as action}
  856. <Tooltip content={action.name} placement="bottom">
  857. <button
  858. type="button"
  859. class="{isLastMessage
  860. ? 'visible'
  861. : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
  862. on:click={() => {
  863. dispatch('action', action.id);
  864. }}
  865. >
  866. {#if action.icon_url}
  867. <img
  868. src={action.icon_url}
  869. class="w-4 h-4 {action.icon_url.includes('svg')
  870. ? 'dark:invert-[80%]'
  871. : ''}"
  872. style="fill: currentColor;"
  873. alt={action.name}
  874. />
  875. {:else}
  876. <Sparkles strokeWidth="2.1" className="size-4" />
  877. {/if}
  878. </button>
  879. </Tooltip>
  880. {/each}
  881. {/if}
  882. {/if}
  883. {/if}
  884. </div>
  885. {/if}
  886. {#if message.done && showRateComment}
  887. <RateComment
  888. messageId={message.id}
  889. bind:show={showRateComment}
  890. bind:message
  891. on:submit={() => {
  892. updateChatMessages();
  893. }}
  894. />
  895. {/if}
  896. {/if}
  897. </div>
  898. </div>
  899. </div>
  900. {/key}
  901. <style>
  902. .buttons::-webkit-scrollbar {
  903. display: none; /* for Chrome, Safari and Opera */
  904. }
  905. .buttons {
  906. -ms-overflow-style: none; /* IE and Edge */
  907. scrollbar-width: none; /* Firefox */
  908. }
  909. </style>