ResponseMessage.svelte 47 KB

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