ResponseMessage.svelte 33 KB

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