ResponseMessage.svelte 39 KB

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