ResponseMessage.svelte 37 KB

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