ResponseMessage.svelte 38 KB

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