ResponseMessage.svelte 33 KB

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