ResponseMessage.svelte 34 KB

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