ResponseMessage.svelte 46 KB

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