ResponseMessage.svelte 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051
  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. $: tokens = marked.lexer(
  64. replaceTokens(sanitizeResponseContent(message?.content), model?.name, $user?.name)
  65. );
  66. $: if (message) {
  67. renderStyling();
  68. }
  69. const renderStyling = async () => {
  70. await tick();
  71. if (tooltipInstance) {
  72. tooltipInstance[0]?.destroy();
  73. }
  74. renderLatex();
  75. if (message.info) {
  76. let tooltipContent = '';
  77. if (message.info.openai) {
  78. tooltipContent = `prompt_tokens: ${message.info.prompt_tokens ?? 'N/A'}<br/>
  79. completion_tokens: ${message.info.completion_tokens ?? 'N/A'}<br/>
  80. total_tokens: ${message.info.total_tokens ?? 'N/A'}`;
  81. } else {
  82. tooltipContent = `response_token/s: ${
  83. `${
  84. Math.round(
  85. ((message.info.eval_count ?? 0) / (message.info.eval_duration / 1000000000)) * 100
  86. ) / 100
  87. } tokens` ?? 'N/A'
  88. }<br/>
  89. prompt_token/s: ${
  90. Math.round(
  91. ((message.info.prompt_eval_count ?? 0) /
  92. (message.info.prompt_eval_duration / 1000000000)) *
  93. 100
  94. ) / 100 ?? 'N/A'
  95. } tokens<br/>
  96. total_duration: ${
  97. Math.round(((message.info.total_duration ?? 0) / 1000000) * 100) / 100 ??
  98. 'N/A'
  99. }ms<br/>
  100. load_duration: ${
  101. Math.round(((message.info.load_duration ?? 0) / 1000000) * 100) / 100 ?? 'N/A'
  102. }ms<br/>
  103. prompt_eval_count: ${message.info.prompt_eval_count ?? 'N/A'}<br/>
  104. prompt_eval_duration: ${
  105. Math.round(((message.info.prompt_eval_duration ?? 0) / 1000000) * 100) /
  106. 100 ?? 'N/A'
  107. }ms<br/>
  108. eval_count: ${message.info.eval_count ?? 'N/A'}<br/>
  109. eval_duration: ${
  110. Math.round(((message.info.eval_duration ?? 0) / 1000000) * 100) / 100 ?? 'N/A'
  111. }ms<br/>
  112. approximate_total: ${approximateToHumanReadable(message.info.total_duration)}`;
  113. }
  114. tooltipInstance = tippy(`#info-${message.id}`, {
  115. content: `<span class="text-xs" id="tooltip-${message.id}">${tooltipContent}</span>`,
  116. allowHTML: true,
  117. theme: 'dark',
  118. arrow: false,
  119. offset: [0, 4]
  120. });
  121. }
  122. };
  123. const renderLatex = () => {
  124. let chatMessageElements = document
  125. .getElementById(`message-${message.id}`)
  126. ?.getElementsByClassName('chat-assistant');
  127. if (chatMessageElements) {
  128. for (const element of chatMessageElements) {
  129. auto_render(element, {
  130. // customised options
  131. // • auto-render specific keys, e.g.:
  132. delimiters: [
  133. { left: '$$', right: '$$', display: false },
  134. { left: '$ ', right: ' $', display: false },
  135. { left: '\\pu{', right: '}', display: false },
  136. { left: '\\ce{', right: '}', display: false },
  137. { left: '\\(', right: '\\)', display: false },
  138. { left: '( ', right: ' )', display: false },
  139. { left: '\\[', right: '\\]', display: false },
  140. { left: '[ ', right: ' ]', display: false }
  141. ],
  142. // • rendering keys, e.g.:
  143. throwOnError: false
  144. });
  145. }
  146. }
  147. };
  148. const playAudio = (idx) => {
  149. return new Promise((res) => {
  150. speakingIdx = idx;
  151. const audio = sentencesAudio[idx];
  152. audio.play();
  153. audio.onended = async (e) => {
  154. await new Promise((r) => setTimeout(r, 300));
  155. if (Object.keys(sentencesAudio).length - 1 === idx) {
  156. speaking = null;
  157. }
  158. res(e);
  159. };
  160. });
  161. };
  162. const toggleSpeakMessage = async () => {
  163. if (speaking) {
  164. try {
  165. speechSynthesis.cancel();
  166. sentencesAudio[speakingIdx].pause();
  167. sentencesAudio[speakingIdx].currentTime = 0;
  168. } catch {}
  169. speaking = null;
  170. speakingIdx = null;
  171. } else {
  172. if ((message?.content ?? '').trim() !== '') {
  173. speaking = true;
  174. if ($config.audio.tts.engine !== '') {
  175. loadingSpeech = true;
  176. const sentences = extractSentences(message.content).reduce((mergedTexts, currentText) => {
  177. const lastIndex = mergedTexts.length - 1;
  178. if (lastIndex >= 0) {
  179. const previousText = mergedTexts[lastIndex];
  180. const wordCount = previousText.split(/\s+/).length;
  181. if (wordCount < 2) {
  182. mergedTexts[lastIndex] = previousText + ' ' + currentText;
  183. } else {
  184. mergedTexts.push(currentText);
  185. }
  186. } else {
  187. mergedTexts.push(currentText);
  188. }
  189. return mergedTexts;
  190. }, []);
  191. console.log(sentences);
  192. if (sentences.length > 0) {
  193. sentencesAudio = sentences.reduce((a, e, i, arr) => {
  194. a[i] = null;
  195. return a;
  196. }, {});
  197. let lastPlayedAudioPromise = Promise.resolve(); // Initialize a promise that resolves immediately
  198. for (const [idx, sentence] of sentences.entries()) {
  199. const res = await synthesizeOpenAISpeech(
  200. localStorage.token,
  201. $settings?.audio?.tts?.defaultVoice === $config.audio.tts.voice
  202. ? $settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice
  203. : $config?.audio?.tts?.voice,
  204. sentence
  205. ).catch((error) => {
  206. toast.error(error);
  207. speaking = null;
  208. loadingSpeech = false;
  209. return null;
  210. });
  211. if (res) {
  212. const blob = await res.blob();
  213. const blobUrl = URL.createObjectURL(blob);
  214. const audio = new Audio(blobUrl);
  215. sentencesAudio[idx] = audio;
  216. loadingSpeech = false;
  217. lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
  218. }
  219. }
  220. } else {
  221. speaking = null;
  222. loadingSpeech = false;
  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($i18n.t('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. <div>
  336. {#if (message?.files ?? []).filter((f) => f.type === 'image').length > 0}
  337. <div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap">
  338. {#each message.files as file}
  339. <div>
  340. {#if file.type === 'image'}
  341. <Image src={file.url} />
  342. {/if}
  343. </div>
  344. {/each}
  345. </div>
  346. {/if}
  347. <div
  348. class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-p:my-0 prose-img:my-1 whitespace-pre-line"
  349. >
  350. <div>
  351. {#if (message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]).length > 0}
  352. {@const status = (
  353. message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]
  354. ).at(-1)}
  355. <div class="flex items-center gap-2 pt-0.5 pb-1">
  356. {#if status.done === false}
  357. <div class="">
  358. <Spinner className="size-4" />
  359. </div>
  360. {/if}
  361. {#if status?.action === 'web_search' && status?.urls}
  362. <WebSearchResults {status}>
  363. <div class="flex flex-col justify-center -space-y-0.5">
  364. <div class="text-base line-clamp-1 text-wrap">
  365. {status?.description}
  366. </div>
  367. </div>
  368. </WebSearchResults>
  369. {:else}
  370. <div class="flex flex-col justify-center -space-y-0.5">
  371. <div class=" text-gray-500 dark:text-gray-500 text-base line-clamp-1 text-wrap">
  372. {status?.description}
  373. </div>
  374. </div>
  375. {/if}
  376. </div>
  377. {/if}
  378. {#if edit === true}
  379. <div class="w-full bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 my-2">
  380. <textarea
  381. id="message-edit-{message.id}"
  382. bind:this={editTextAreaElement}
  383. class=" bg-transparent outline-none w-full resize-none"
  384. bind:value={editedContent}
  385. on:input={(e) => {
  386. e.target.style.height = '';
  387. e.target.style.height = `${e.target.scrollHeight}px`;
  388. }}
  389. on:keydown={(e) => {
  390. if (e.key === 'Escape') {
  391. document.getElementById('close-edit-message-button')?.click();
  392. }
  393. const isCmdOrCtrlPressed = e.metaKey || e.ctrlKey;
  394. const isEnterPressed = e.key === 'Enter';
  395. if (isCmdOrCtrlPressed && isEnterPressed) {
  396. document.getElementById('save-edit-message-button')?.click();
  397. }
  398. }}
  399. />
  400. <div class=" mt-2 mb-1 flex justify-end space-x-1.5 text-sm font-medium">
  401. <button
  402. id="close-edit-message-button"
  403. class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-800 transition rounded-3xl"
  404. on:click={() => {
  405. cancelEditMessage();
  406. }}
  407. >
  408. {$i18n.t('Cancel')}
  409. </button>
  410. <button
  411. id="save-edit-message-button"
  412. class=" px-4 py-2 bg-gray-900 hover:bg-gray-850 text-gray-100 transition rounded-3xl"
  413. on:click={() => {
  414. editMessageConfirmHandler();
  415. }}
  416. >
  417. {$i18n.t('Save')}
  418. </button>
  419. </div>
  420. </div>
  421. {:else}
  422. <div class="w-full">
  423. {#if message.content === '' && !message.error}
  424. <Skeleton />
  425. {:else if message.content && message.error !== true}
  426. <!-- always show message contents even if there's an error -->
  427. <!-- unless message.error === true which is legacy error handling, where the error message is stored in message.content -->
  428. <MarkdownTokens id={message.id} {tokens} />
  429. {/if}
  430. {#if message.error}
  431. <div
  432. 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"
  433. >
  434. <svg
  435. xmlns="http://www.w3.org/2000/svg"
  436. fill="none"
  437. viewBox="0 0 24 24"
  438. stroke-width="1.5"
  439. stroke="currentColor"
  440. class="w-5 h-5 self-center"
  441. >
  442. <path
  443. stroke-linecap="round"
  444. stroke-linejoin="round"
  445. d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
  446. />
  447. </svg>
  448. <div class=" self-center">
  449. {message?.error?.content ?? message.content}
  450. </div>
  451. </div>
  452. {/if}
  453. {#if message.citations}
  454. <div class="mt-1 mb-2 w-full flex gap-1 items-center flex-wrap">
  455. {#each message.citations.reduce((acc, citation) => {
  456. citation.document.forEach((document, index) => {
  457. const metadata = citation.metadata?.[index];
  458. const id = metadata?.source ?? 'N/A';
  459. let source = citation?.source;
  460. if (metadata?.name) {
  461. source = { ...source, name: metadata.name };
  462. }
  463. // Check if ID looks like a URL
  464. if (id.startsWith('http://') || id.startsWith('https://')) {
  465. source = { name: id };
  466. }
  467. const existingSource = acc.find((item) => item.id === id);
  468. if (existingSource) {
  469. existingSource.document.push(document);
  470. existingSource.metadata.push(metadata);
  471. } else {
  472. acc.push( { id: id, source: source, document: [document], metadata: metadata ? [metadata] : [] } );
  473. }
  474. });
  475. return acc;
  476. }, []) as citation, idx}
  477. <div class="flex gap-1 text-xs font-semibold">
  478. <button
  479. 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"
  480. on:click={() => {
  481. showCitationModal = true;
  482. selectedCitation = citation;
  483. }}
  484. >
  485. <div class="bg-white dark:bg-gray-700 rounded-full size-4">
  486. {idx + 1}
  487. </div>
  488. <div class="flex-1 mx-2 line-clamp-1">
  489. {citation.source.name}
  490. </div>
  491. </button>
  492. </div>
  493. {/each}
  494. </div>
  495. {/if}
  496. </div>
  497. {/if}
  498. </div>
  499. </div>
  500. {#if !edit}
  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 class="spinner_S1WN spinner_JApP" cx="20" cy="12" r="3" /></svg
  650. >
  651. {:else if speaking}
  652. <svg
  653. xmlns="http://www.w3.org/2000/svg"
  654. fill="none"
  655. viewBox="0 0 24 24"
  656. stroke-width="2.3"
  657. stroke="currentColor"
  658. class="w-4 h-4"
  659. >
  660. <path
  661. stroke-linecap="round"
  662. stroke-linejoin="round"
  663. 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"
  664. />
  665. </svg>
  666. {:else}
  667. <svg
  668. xmlns="http://www.w3.org/2000/svg"
  669. fill="none"
  670. viewBox="0 0 24 24"
  671. stroke-width="2.3"
  672. stroke="currentColor"
  673. class="w-4 h-4"
  674. >
  675. <path
  676. stroke-linecap="round"
  677. stroke-linejoin="round"
  678. 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"
  679. />
  680. </svg>
  681. {/if}
  682. </button>
  683. </Tooltip>
  684. {#if $config?.features.enable_image_generation && !readOnly}
  685. <Tooltip content={$i18n.t('Generate Image')} placement="bottom">
  686. <button
  687. class="{isLastMessage
  688. ? 'visible'
  689. : '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"
  690. on:click={() => {
  691. if (!generatingImage) {
  692. generateImage(message);
  693. }
  694. }}
  695. >
  696. {#if generatingImage}
  697. <svg
  698. class=" w-4 h-4"
  699. fill="currentColor"
  700. viewBox="0 0 24 24"
  701. xmlns="http://www.w3.org/2000/svg"
  702. ><style>
  703. .spinner_S1WN {
  704. animation: spinner_MGfb 0.8s linear infinite;
  705. animation-delay: -0.8s;
  706. }
  707. .spinner_Km9P {
  708. animation-delay: -0.65s;
  709. }
  710. .spinner_JApP {
  711. animation-delay: -0.5s;
  712. }
  713. @keyframes spinner_MGfb {
  714. 93.75%,
  715. 100% {
  716. opacity: 0.2;
  717. }
  718. }
  719. </style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle
  720. class="spinner_S1WN spinner_Km9P"
  721. cx="12"
  722. cy="12"
  723. r="3"
  724. /><circle class="spinner_S1WN spinner_JApP" cx="20" cy="12" r="3" /></svg
  725. >
  726. {:else}
  727. <svg
  728. xmlns="http://www.w3.org/2000/svg"
  729. fill="none"
  730. viewBox="0 0 24 24"
  731. stroke-width="2.3"
  732. stroke="currentColor"
  733. class="w-4 h-4"
  734. >
  735. <path
  736. stroke-linecap="round"
  737. stroke-linejoin="round"
  738. 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"
  739. />
  740. </svg>
  741. {/if}
  742. </button>
  743. </Tooltip>
  744. {/if}
  745. {#if message.info}
  746. <Tooltip content={$i18n.t('Generation Info')} placement="bottom">
  747. <button
  748. class=" {isLastMessage
  749. ? 'visible'
  750. : '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"
  751. on:click={() => {
  752. console.log(message);
  753. }}
  754. id="info-{message.id}"
  755. >
  756. <svg
  757. xmlns="http://www.w3.org/2000/svg"
  758. fill="none"
  759. viewBox="0 0 24 24"
  760. stroke-width="2.3"
  761. stroke="currentColor"
  762. class="w-4 h-4"
  763. >
  764. <path
  765. stroke-linecap="round"
  766. stroke-linejoin="round"
  767. 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"
  768. />
  769. </svg>
  770. </button>
  771. </Tooltip>
  772. {/if}
  773. {#if !readOnly}
  774. <Tooltip content={$i18n.t('Good Response')} placement="bottom">
  775. <button
  776. class="{isLastMessage
  777. ? 'visible'
  778. : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {(message
  779. ?.annotation?.rating ?? null) === 1
  780. ? 'bg-gray-100 dark:bg-gray-800'
  781. : ''} dark:hover:text-white hover:text-black transition"
  782. on:click={() => {
  783. rateMessage(message.id, 1);
  784. showRateComment = true;
  785. window.setTimeout(() => {
  786. document
  787. .getElementById(`message-feedback-${message.id}`)
  788. ?.scrollIntoView();
  789. }, 0);
  790. }}
  791. >
  792. <svg
  793. stroke="currentColor"
  794. fill="none"
  795. stroke-width="2.3"
  796. viewBox="0 0 24 24"
  797. stroke-linecap="round"
  798. stroke-linejoin="round"
  799. class="w-4 h-4"
  800. xmlns="http://www.w3.org/2000/svg"
  801. ><path
  802. 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"
  803. /></svg
  804. >
  805. </button>
  806. </Tooltip>
  807. <Tooltip content={$i18n.t('Bad Response')} placement="bottom">
  808. <button
  809. class="{isLastMessage
  810. ? 'visible'
  811. : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {(message
  812. ?.annotation?.rating ?? null) === -1
  813. ? 'bg-gray-100 dark:bg-gray-800'
  814. : ''} dark:hover:text-white hover:text-black transition"
  815. on:click={() => {
  816. rateMessage(message.id, -1);
  817. showRateComment = true;
  818. window.setTimeout(() => {
  819. document
  820. .getElementById(`message-feedback-${message.id}`)
  821. ?.scrollIntoView();
  822. }, 0);
  823. }}
  824. >
  825. <svg
  826. stroke="currentColor"
  827. fill="none"
  828. stroke-width="2.3"
  829. viewBox="0 0 24 24"
  830. stroke-linecap="round"
  831. stroke-linejoin="round"
  832. class="w-4 h-4"
  833. xmlns="http://www.w3.org/2000/svg"
  834. ><path
  835. 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"
  836. /></svg
  837. >
  838. </button>
  839. </Tooltip>
  840. {#if isLastMessage}
  841. <Tooltip content={$i18n.t('Continue Response')} placement="bottom">
  842. <button
  843. type="button"
  844. class="{isLastMessage
  845. ? 'visible'
  846. : '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"
  847. on:click={() => {
  848. continueGeneration();
  849. }}
  850. >
  851. <svg
  852. xmlns="http://www.w3.org/2000/svg"
  853. fill="none"
  854. viewBox="0 0 24 24"
  855. stroke-width="2.3"
  856. stroke="currentColor"
  857. class="w-4 h-4"
  858. >
  859. <path
  860. stroke-linecap="round"
  861. stroke-linejoin="round"
  862. d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
  863. />
  864. <path
  865. stroke-linecap="round"
  866. stroke-linejoin="round"
  867. 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"
  868. />
  869. </svg>
  870. </button>
  871. </Tooltip>
  872. <Tooltip content={$i18n.t('Regenerate')} placement="bottom">
  873. <button
  874. type="button"
  875. class="{isLastMessage
  876. ? 'visible'
  877. : '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"
  878. on:click={() => {
  879. showRateComment = false;
  880. regenerateResponse(message);
  881. }}
  882. >
  883. <svg
  884. xmlns="http://www.w3.org/2000/svg"
  885. fill="none"
  886. viewBox="0 0 24 24"
  887. stroke-width="2.3"
  888. stroke="currentColor"
  889. class="w-4 h-4"
  890. >
  891. <path
  892. stroke-linecap="round"
  893. stroke-linejoin="round"
  894. 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"
  895. />
  896. </svg>
  897. </button>
  898. </Tooltip>
  899. {#each model?.actions ?? [] as action}
  900. <Tooltip content={action.name} placement="bottom">
  901. <button
  902. type="button"
  903. class="{isLastMessage
  904. ? 'visible'
  905. : '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"
  906. on:click={() => {
  907. dispatch('action', action.id);
  908. }}
  909. >
  910. {#if action.icon_url}
  911. <img
  912. src={action.icon_url}
  913. class="w-4 h-4 {action.icon_url.includes('svg')
  914. ? 'dark:invert-[80%]'
  915. : ''}"
  916. style="fill: currentColor;"
  917. alt={action.name}
  918. />
  919. {:else}
  920. <Sparkles strokeWidth="2.1" className="size-4" />
  921. {/if}
  922. </button>
  923. </Tooltip>
  924. {/each}
  925. {/if}
  926. {/if}
  927. {/if}
  928. </div>
  929. {/if}
  930. {#if message.done && showRateComment}
  931. <RateComment
  932. messageId={message.id}
  933. bind:show={showRateComment}
  934. bind:message
  935. on:submit={() => {
  936. updateChatMessages();
  937. }}
  938. />
  939. {/if}
  940. {/if}
  941. </div>
  942. </div>
  943. </div>
  944. {/key}
  945. <style>
  946. .buttons::-webkit-scrollbar {
  947. display: none; /* for Chrome, Safari and Opera */
  948. }
  949. .buttons {
  950. -ms-overflow-style: none; /* IE and Edge */
  951. scrollbar-width: none; /* Firefox */
  952. }
  953. </style>