ResponseMessage.svelte 33 KB

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