ResponseMessage.svelte 32 KB

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