ResponseMessage.svelte 33 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037
  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. if ((message?.content ?? '').trim() !== '') {
  180. speaking = true;
  181. if ($config.audio.tts.engine === 'openai') {
  182. loadingSpeech = true;
  183. const sentences = extractSentences(message.content).reduce((mergedTexts, currentText) => {
  184. const lastIndex = mergedTexts.length - 1;
  185. if (lastIndex >= 0) {
  186. const previousText = mergedTexts[lastIndex];
  187. const wordCount = previousText.split(/\s+/).length;
  188. if (wordCount < 2) {
  189. mergedTexts[lastIndex] = previousText + ' ' + currentText;
  190. } else {
  191. mergedTexts.push(currentText);
  192. }
  193. } else {
  194. mergedTexts.push(currentText);
  195. }
  196. return mergedTexts;
  197. }, []);
  198. console.log(sentences);
  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?.voice ?? $config?.audio?.tts?.voice,
  208. sentence
  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
  232. ?.filter(
  233. (v) =>
  234. v.voiceURI === ($settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice)
  235. )
  236. ?.at(0) ?? undefined;
  237. console.log(voice);
  238. const speak = new SpeechSynthesisUtterance(message.content);
  239. console.log(speak);
  240. speak.onend = () => {
  241. speaking = null;
  242. if ($settings.conversationMode) {
  243. document.getElementById('voice-input-button')?.click();
  244. }
  245. };
  246. if (voice) {
  247. speak.voice = voice;
  248. }
  249. speechSynthesis.speak(speak);
  250. }
  251. }, 100);
  252. }
  253. } else {
  254. toast.error('No content to speak');
  255. }
  256. }
  257. };
  258. const editMessageHandler = async () => {
  259. edit = true;
  260. editedContent = message.content;
  261. await tick();
  262. editTextAreaElement.style.height = '';
  263. editTextAreaElement.style.height = `${editTextAreaElement.scrollHeight}px`;
  264. };
  265. const editMessageConfirmHandler = async () => {
  266. if (editedContent === '') {
  267. editedContent = ' ';
  268. }
  269. confirmEditResponseMessage(message.id, editedContent);
  270. edit = false;
  271. editedContent = '';
  272. await tick();
  273. renderStyling();
  274. };
  275. const cancelEditMessage = async () => {
  276. edit = false;
  277. editedContent = '';
  278. await tick();
  279. renderStyling();
  280. };
  281. const generateImage = async (message) => {
  282. generatingImage = true;
  283. const res = await imageGenerations(localStorage.token, message.content).catch((error) => {
  284. toast.error(error);
  285. });
  286. console.log(res);
  287. if (res) {
  288. message.files = res.map((image) => ({
  289. type: 'image',
  290. url: `${image.url}`
  291. }));
  292. dispatch('save', message);
  293. }
  294. generatingImage = false;
  295. };
  296. $: if (!edit) {
  297. (async () => {
  298. await tick();
  299. renderStyling();
  300. await mermaid.run({
  301. querySelector: '.mermaid'
  302. });
  303. })();
  304. }
  305. onMount(async () => {
  306. await tick();
  307. renderStyling();
  308. await mermaid.run({
  309. querySelector: '.mermaid'
  310. });
  311. });
  312. </script>
  313. <CitationsModal bind:show={showCitationModal} citation={selectedCitation} />
  314. {#key message.id}
  315. <div
  316. class=" flex w-full message-{message.id}"
  317. id="message-{message.id}"
  318. dir={$settings.chatDirection}
  319. >
  320. <ProfileImage
  321. src={model?.info?.meta?.profile_image_url ??
  322. ($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
  323. />
  324. <div class="w-full overflow-hidden pl-1">
  325. <Name>
  326. {model?.name ?? message.model}
  327. {#if message.timestamp}
  328. <span
  329. class=" self-center invisible group-hover:visible text-gray-400 text-xs font-medium uppercase"
  330. >
  331. {dayjs(message.timestamp * 1000).format($i18n.t('h:mm a'))}
  332. </span>
  333. {/if}
  334. </Name>
  335. {#if (message?.files ?? []).filter((f) => f.type === 'image').length > 0}
  336. <div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap">
  337. {#each message.files as file}
  338. <div>
  339. {#if file.type === 'image'}
  340. <Image src={file.url} />
  341. {/if}
  342. </div>
  343. {/each}
  344. </div>
  345. {/if}
  346. <div
  347. 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"
  348. >
  349. <div>
  350. {#if (message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]).length > 0}
  351. {@const status = (
  352. message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]
  353. ).at(-1)}
  354. <div class="flex items-center gap-2 pt-1 pb-1">
  355. {#if status.done === false}
  356. <div class="">
  357. <Spinner className="size-4" />
  358. </div>
  359. {/if}
  360. {#if status?.action === 'web_search' && status?.urls}
  361. <WebSearchResults {status}>
  362. <div class="flex flex-col justify-center -space-y-0.5">
  363. <div class="text-base line-clamp-1 text-wrap">
  364. {status?.description}
  365. </div>
  366. </div>
  367. </WebSearchResults>
  368. {:else}
  369. <div class="flex flex-col justify-center -space-y-0.5">
  370. <div class=" text-gray-500 dark:text-gray-500 text-base line-clamp-1 text-wrap">
  371. {status?.description}
  372. </div>
  373. </div>
  374. {/if}
  375. </div>
  376. {/if}
  377. {#if edit === true}
  378. <div class="w-full bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 my-2">
  379. <textarea
  380. id="message-edit-{message.id}"
  381. bind:this={editTextAreaElement}
  382. class=" bg-transparent outline-none w-full resize-none"
  383. bind:value={editedContent}
  384. on:input={(e) => {
  385. e.target.style.height = '';
  386. e.target.style.height = `${e.target.scrollHeight}px`;
  387. }}
  388. />
  389. <div class=" mt-2 mb-1 flex justify-end space-x-1.5 text-sm font-medium">
  390. <button
  391. id="close-edit-message-button"
  392. class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-800 transition rounded-3xl"
  393. on:click={() => {
  394. cancelEditMessage();
  395. }}
  396. >
  397. {$i18n.t('Cancel')}
  398. </button>
  399. <button
  400. id="save-edit-message-button"
  401. class=" px-4 py-2 bg-gray-900 hover:bg-gray-850 text-gray-100 transition rounded-3xl"
  402. on:click={() => {
  403. editMessageConfirmHandler();
  404. }}
  405. >
  406. {$i18n.t('Save')}
  407. </button>
  408. </div>
  409. </div>
  410. {:else}
  411. <div class="w-full">
  412. {#if message.content === '' && !message.error}
  413. <Skeleton />
  414. {:else if message.content && message.error !== true}
  415. <!-- always show message contents even if there's an error -->
  416. <!-- unless message.error === true which is legacy error handling, where the error message is stored in message.content -->
  417. {#each tokens as token, tokenIdx}
  418. {#if token.type === 'code'}
  419. {#if token.lang === 'mermaid'}
  420. <pre class="mermaid">{revertSanitizedResponseContent(token.text)}</pre>
  421. {:else}
  422. <CodeBlock
  423. id={`${message.id}-${tokenIdx}`}
  424. lang={token?.lang ?? ''}
  425. code={revertSanitizedResponseContent(token?.text ?? '')}
  426. />
  427. {/if}
  428. {:else}
  429. {@html marked.parse(token.raw, {
  430. ...defaults,
  431. gfm: true,
  432. breaks: true,
  433. renderer
  434. })}
  435. {/if}
  436. {/each}
  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. // Check if ID looks like a URL
  469. if (id.startsWith('http://') || id.startsWith('https://')) {
  470. source = { name: id };
  471. }
  472. const existingSource = acc.find((item) => item.id === id);
  473. if (existingSource) {
  474. existingSource.document.push(document);
  475. existingSource.metadata.push(metadata);
  476. } else {
  477. acc.push( { id: id, source: source, document: [document], metadata: metadata ? [metadata] : [] } );
  478. }
  479. });
  480. return acc;
  481. }, []) as citation, idx}
  482. <div class="flex gap-1 text-xs font-semibold">
  483. <button
  484. 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"
  485. on:click={() => {
  486. showCitationModal = true;
  487. selectedCitation = citation;
  488. }}
  489. >
  490. <div class="bg-white dark:bg-gray-700 rounded-full size-4">
  491. {idx + 1}
  492. </div>
  493. <div class="flex-1 mx-2 line-clamp-1">
  494. {citation.source.name}
  495. </div>
  496. </button>
  497. </div>
  498. {/each}
  499. </div>
  500. {/if}
  501. {#if message.done || siblings.length > 1}
  502. <div
  503. class=" flex justify-start overflow-x-auto buttons text-gray-600 dark:text-gray-500"
  504. >
  505. {#if siblings.length > 1}
  506. <div class="flex self-center min-w-fit" dir="ltr">
  507. <button
  508. class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
  509. on:click={() => {
  510. showPreviousMessage(message);
  511. }}
  512. >
  513. <svg
  514. xmlns="http://www.w3.org/2000/svg"
  515. fill="none"
  516. viewBox="0 0 24 24"
  517. stroke="currentColor"
  518. stroke-width="2.5"
  519. class="size-3.5"
  520. >
  521. <path
  522. stroke-linecap="round"
  523. stroke-linejoin="round"
  524. d="M15.75 19.5 8.25 12l7.5-7.5"
  525. />
  526. </svg>
  527. </button>
  528. <div
  529. class="text-sm tracking-widest font-semibold self-center dark:text-gray-100 min-w-fit"
  530. >
  531. {siblings.indexOf(message.id) + 1}/{siblings.length}
  532. </div>
  533. <button
  534. class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
  535. on:click={() => {
  536. showNextMessage(message);
  537. }}
  538. >
  539. <svg
  540. xmlns="http://www.w3.org/2000/svg"
  541. fill="none"
  542. viewBox="0 0 24 24"
  543. stroke="currentColor"
  544. stroke-width="2.5"
  545. class="size-3.5"
  546. >
  547. <path
  548. stroke-linecap="round"
  549. stroke-linejoin="round"
  550. d="m8.25 4.5 7.5 7.5-7.5 7.5"
  551. />
  552. </svg>
  553. </button>
  554. </div>
  555. {/if}
  556. {#if message.done}
  557. {#if !readOnly}
  558. <Tooltip content={$i18n.t('Edit')} placement="bottom">
  559. <button
  560. class="{isLastMessage
  561. ? 'visible'
  562. : '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"
  563. on:click={() => {
  564. editMessageHandler();
  565. }}
  566. >
  567. <svg
  568. xmlns="http://www.w3.org/2000/svg"
  569. fill="none"
  570. viewBox="0 0 24 24"
  571. stroke-width="2.3"
  572. stroke="currentColor"
  573. class="w-4 h-4"
  574. >
  575. <path
  576. stroke-linecap="round"
  577. stroke-linejoin="round"
  578. 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"
  579. />
  580. </svg>
  581. </button>
  582. </Tooltip>
  583. {/if}
  584. <Tooltip content={$i18n.t('Copy')} placement="bottom">
  585. <button
  586. class="{isLastMessage
  587. ? 'visible'
  588. : '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"
  589. on:click={() => {
  590. copyToClipboard(message.content);
  591. }}
  592. >
  593. <svg
  594. xmlns="http://www.w3.org/2000/svg"
  595. fill="none"
  596. viewBox="0 0 24 24"
  597. stroke-width="2.3"
  598. stroke="currentColor"
  599. class="w-4 h-4"
  600. >
  601. <path
  602. stroke-linecap="round"
  603. stroke-linejoin="round"
  604. 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"
  605. />
  606. </svg>
  607. </button>
  608. </Tooltip>
  609. <Tooltip content={$i18n.t('Read Aloud')} placement="bottom">
  610. <button
  611. id="speak-button-{message.id}"
  612. class="{isLastMessage
  613. ? 'visible'
  614. : '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"
  615. on:click={() => {
  616. if (!loadingSpeech) {
  617. toggleSpeakMessage(message);
  618. }
  619. }}
  620. >
  621. {#if loadingSpeech}
  622. <svg
  623. class=" w-4 h-4"
  624. fill="currentColor"
  625. viewBox="0 0 24 24"
  626. xmlns="http://www.w3.org/2000/svg"
  627. ><style>
  628. .spinner_S1WN {
  629. animation: spinner_MGfb 0.8s linear infinite;
  630. animation-delay: -0.8s;
  631. }
  632. .spinner_Km9P {
  633. animation-delay: -0.65s;
  634. }
  635. .spinner_JApP {
  636. animation-delay: -0.5s;
  637. }
  638. @keyframes spinner_MGfb {
  639. 93.75%,
  640. 100% {
  641. opacity: 0.2;
  642. }
  643. }
  644. </style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle
  645. class="spinner_S1WN spinner_Km9P"
  646. cx="12"
  647. cy="12"
  648. r="3"
  649. /><circle
  650. class="spinner_S1WN spinner_JApP"
  651. cx="20"
  652. cy="12"
  653. r="3"
  654. /></svg
  655. >
  656. {:else if speaking}
  657. <svg
  658. xmlns="http://www.w3.org/2000/svg"
  659. fill="none"
  660. viewBox="0 0 24 24"
  661. stroke-width="2.3"
  662. stroke="currentColor"
  663. class="w-4 h-4"
  664. >
  665. <path
  666. stroke-linecap="round"
  667. stroke-linejoin="round"
  668. 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"
  669. />
  670. </svg>
  671. {:else}
  672. <svg
  673. xmlns="http://www.w3.org/2000/svg"
  674. fill="none"
  675. viewBox="0 0 24 24"
  676. stroke-width="2.3"
  677. stroke="currentColor"
  678. class="w-4 h-4"
  679. >
  680. <path
  681. stroke-linecap="round"
  682. stroke-linejoin="round"
  683. 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"
  684. />
  685. </svg>
  686. {/if}
  687. </button>
  688. </Tooltip>
  689. {#if $config?.features.enable_image_generation && !readOnly}
  690. <Tooltip content={$i18n.t('Generate Image')} placement="bottom">
  691. <button
  692. class="{isLastMessage
  693. ? 'visible'
  694. : '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"
  695. on:click={() => {
  696. if (!generatingImage) {
  697. generateImage(message);
  698. }
  699. }}
  700. >
  701. {#if generatingImage}
  702. <svg
  703. class=" w-4 h-4"
  704. fill="currentColor"
  705. viewBox="0 0 24 24"
  706. xmlns="http://www.w3.org/2000/svg"
  707. ><style>
  708. .spinner_S1WN {
  709. animation: spinner_MGfb 0.8s linear infinite;
  710. animation-delay: -0.8s;
  711. }
  712. .spinner_Km9P {
  713. animation-delay: -0.65s;
  714. }
  715. .spinner_JApP {
  716. animation-delay: -0.5s;
  717. }
  718. @keyframes spinner_MGfb {
  719. 93.75%,
  720. 100% {
  721. opacity: 0.2;
  722. }
  723. }
  724. </style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle
  725. class="spinner_S1WN spinner_Km9P"
  726. cx="12"
  727. cy="12"
  728. r="3"
  729. /><circle
  730. class="spinner_S1WN spinner_JApP"
  731. cx="20"
  732. cy="12"
  733. r="3"
  734. /></svg
  735. >
  736. {:else}
  737. <svg
  738. xmlns="http://www.w3.org/2000/svg"
  739. fill="none"
  740. viewBox="0 0 24 24"
  741. stroke-width="2.3"
  742. stroke="currentColor"
  743. class="w-4 h-4"
  744. >
  745. <path
  746. stroke-linecap="round"
  747. stroke-linejoin="round"
  748. 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"
  749. />
  750. </svg>
  751. {/if}
  752. </button>
  753. </Tooltip>
  754. {/if}
  755. {#if message.info}
  756. <Tooltip content={$i18n.t('Generation Info')} placement="bottom">
  757. <button
  758. class=" {isLastMessage
  759. ? 'visible'
  760. : '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"
  761. on:click={() => {
  762. console.log(message);
  763. }}
  764. id="info-{message.id}"
  765. >
  766. <svg
  767. xmlns="http://www.w3.org/2000/svg"
  768. fill="none"
  769. viewBox="0 0 24 24"
  770. stroke-width="2.3"
  771. stroke="currentColor"
  772. class="w-4 h-4"
  773. >
  774. <path
  775. stroke-linecap="round"
  776. stroke-linejoin="round"
  777. 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"
  778. />
  779. </svg>
  780. </button>
  781. </Tooltip>
  782. {/if}
  783. {#if !readOnly}
  784. <Tooltip content={$i18n.t('Good Response')} placement="bottom">
  785. <button
  786. class="{isLastMessage
  787. ? 'visible'
  788. : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {message
  789. ?.annotation?.rating === 1
  790. ? 'bg-gray-100 dark:bg-gray-800'
  791. : ''} dark:hover:text-white hover:text-black transition"
  792. on:click={() => {
  793. rateMessage(message.id, 1);
  794. showRateComment = true;
  795. window.setTimeout(() => {
  796. document
  797. .getElementById(`message-feedback-${message.id}`)
  798. ?.scrollIntoView();
  799. }, 0);
  800. }}
  801. >
  802. <svg
  803. stroke="currentColor"
  804. fill="none"
  805. stroke-width="2.3"
  806. viewBox="0 0 24 24"
  807. stroke-linecap="round"
  808. stroke-linejoin="round"
  809. class="w-4 h-4"
  810. xmlns="http://www.w3.org/2000/svg"
  811. ><path
  812. 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"
  813. /></svg
  814. >
  815. </button>
  816. </Tooltip>
  817. <Tooltip content={$i18n.t('Bad Response')} placement="bottom">
  818. <button
  819. class="{isLastMessage
  820. ? 'visible'
  821. : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {message
  822. ?.annotation?.rating === -1
  823. ? 'bg-gray-100 dark:bg-gray-800'
  824. : ''} dark:hover:text-white hover:text-black transition"
  825. on:click={() => {
  826. rateMessage(message.id, -1);
  827. showRateComment = true;
  828. window.setTimeout(() => {
  829. document
  830. .getElementById(`message-feedback-${message.id}`)
  831. ?.scrollIntoView();
  832. }, 0);
  833. }}
  834. >
  835. <svg
  836. stroke="currentColor"
  837. fill="none"
  838. stroke-width="2.3"
  839. viewBox="0 0 24 24"
  840. stroke-linecap="round"
  841. stroke-linejoin="round"
  842. class="w-4 h-4"
  843. xmlns="http://www.w3.org/2000/svg"
  844. ><path
  845. 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"
  846. /></svg
  847. >
  848. </button>
  849. </Tooltip>
  850. {/if}
  851. {#if isLastMessage && !readOnly}
  852. <Tooltip content={$i18n.t('Continue Response')} placement="bottom">
  853. <button
  854. type="button"
  855. class="{isLastMessage
  856. ? 'visible'
  857. : '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"
  858. on:click={() => {
  859. continueGeneration();
  860. }}
  861. >
  862. <svg
  863. xmlns="http://www.w3.org/2000/svg"
  864. fill="none"
  865. viewBox="0 0 24 24"
  866. stroke-width="2.3"
  867. stroke="currentColor"
  868. class="w-4 h-4"
  869. >
  870. <path
  871. stroke-linecap="round"
  872. stroke-linejoin="round"
  873. d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
  874. />
  875. <path
  876. stroke-linecap="round"
  877. stroke-linejoin="round"
  878. 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"
  879. />
  880. </svg>
  881. </button>
  882. </Tooltip>
  883. <Tooltip content={$i18n.t('Regenerate')} placement="bottom">
  884. <button
  885. type="button"
  886. class="{isLastMessage
  887. ? 'visible'
  888. : '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"
  889. on:click={() => {
  890. showRateComment = false;
  891. regenerateResponse(message);
  892. }}
  893. >
  894. <svg
  895. xmlns="http://www.w3.org/2000/svg"
  896. fill="none"
  897. viewBox="0 0 24 24"
  898. stroke-width="2.3"
  899. stroke="currentColor"
  900. class="w-4 h-4"
  901. >
  902. <path
  903. stroke-linecap="round"
  904. stroke-linejoin="round"
  905. 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"
  906. />
  907. </svg>
  908. </button>
  909. </Tooltip>
  910. {/if}
  911. {/if}
  912. </div>
  913. {/if}
  914. {#if message.done && showRateComment}
  915. <RateComment
  916. messageId={message.id}
  917. bind:show={showRateComment}
  918. bind:message
  919. on:submit={() => {
  920. updateChatMessages();
  921. }}
  922. />
  923. {/if}
  924. </div>
  925. {/if}
  926. </div>
  927. </div>
  928. </div>
  929. </div>
  930. {/key}
  931. <style>
  932. .buttons::-webkit-scrollbar {
  933. display: none; /* for Chrome, Safari and Opera */
  934. }
  935. .buttons {
  936. -ms-overflow-style: none; /* IE and Edge */
  937. scrollbar-width: none; /* Firefox */
  938. }
  939. </style>