ResponseMessage.svelte 35 KB

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