ResponseMessage.svelte 33 KB

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