ResponseMessage.svelte 41 KB

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