ResponseMessage.svelte 41 KB

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