ResponseMessage.svelte 37 KB

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