Messages.svelte 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698
  1. <script lang="ts">
  2. import { marked } from 'marked';
  3. import { v4 as uuidv4 } from 'uuid';
  4. import hljs from 'highlight.js';
  5. import 'highlight.js/styles/github-dark.min.css';
  6. import auto_render from 'katex/dist/contrib/auto-render.mjs';
  7. import 'katex/dist/katex.min.css';
  8. import { config, db, settings, user } from '$lib/stores';
  9. import { tick } from 'svelte';
  10. import toast from 'svelte-french-toast';
  11. export let sendPrompt: Function;
  12. export let regenerateResponse: Function;
  13. export let autoScroll;
  14. export let history = {};
  15. export let messages = [];
  16. $: if (messages && messages.length > 0 && (messages.at(-1).done ?? false)) {
  17. (async () => {
  18. await tick();
  19. renderLatex();
  20. hljs.highlightAll();
  21. createCopyCodeBlockButton();
  22. })();
  23. }
  24. const createCopyCodeBlockButton = () => {
  25. // use a class selector if available
  26. let blocks = document.querySelectorAll('pre');
  27. blocks.forEach((block) => {
  28. // only add button if browser supports Clipboard API
  29. if (navigator.clipboard && block.childNodes.length < 2) {
  30. let code = block.querySelector('code');
  31. code.style.borderTopRightRadius = 0;
  32. code.style.borderTopLeftRadius = 0;
  33. let topBarDiv = document.createElement('div');
  34. topBarDiv.style.backgroundColor = '#202123';
  35. topBarDiv.style.overflowX = 'auto';
  36. topBarDiv.style.display = 'flex';
  37. topBarDiv.style.justifyContent = 'space-between';
  38. topBarDiv.style.padding = '0 1rem';
  39. topBarDiv.style.paddingTop = '4px';
  40. topBarDiv.style.borderTopRightRadius = '8px';
  41. topBarDiv.style.borderTopLeftRadius = '8px';
  42. let langDiv = document.createElement('div');
  43. let codeClassNames = code?.className.split(' ');
  44. langDiv.textContent =
  45. codeClassNames[0] === 'hljs' ? codeClassNames[1].slice(9) : codeClassNames[0].slice(9);
  46. langDiv.style.color = 'white';
  47. langDiv.style.margin = '4px';
  48. langDiv.style.fontSize = '0.75rem';
  49. let button = document.createElement('button');
  50. button.textContent = 'Copy Code';
  51. button.style.background = 'none';
  52. button.style.fontSize = '0.75rem';
  53. button.style.border = 'none';
  54. button.style.margin = '4px';
  55. button.style.cursor = 'pointer';
  56. button.style.color = '#ddd';
  57. button.addEventListener('click', () => copyCode(block, button));
  58. topBarDiv.appendChild(langDiv);
  59. topBarDiv.appendChild(button);
  60. block.prepend(topBarDiv);
  61. // button.addEventListener('click', async () => {
  62. // await copyCode(block, button);
  63. // });
  64. }
  65. });
  66. async function copyCode(block, button) {
  67. let code = block.querySelector('code');
  68. let text = code.innerText;
  69. await navigator.clipboard.writeText(text);
  70. // visual feedback that task is completed
  71. button.innerText = 'Copied!';
  72. setTimeout(() => {
  73. button.innerText = 'Copy Code';
  74. }, 1000);
  75. }
  76. };
  77. const renderLatex = () => {
  78. let chatMessageElements = document.getElementsByClassName('chat-assistant');
  79. // let lastChatMessageElement = chatMessageElements[chatMessageElements.length - 1];
  80. for (const element of chatMessageElements) {
  81. auto_render(element, {
  82. // customised options
  83. // • auto-render specific keys, e.g.:
  84. delimiters: [
  85. { left: '$$', right: '$$', display: true },
  86. { left: '$', right: '$', display: true },
  87. { left: '\\(', right: '\\)', display: true },
  88. { left: '\\[', right: '\\]', display: true }
  89. ],
  90. // • rendering keys, e.g.:
  91. throwOnError: false
  92. });
  93. }
  94. };
  95. const copyToClipboard = (text) => {
  96. if (!navigator.clipboard) {
  97. var textArea = document.createElement('textarea');
  98. textArea.value = text;
  99. // Avoid scrolling to bottom
  100. textArea.style.top = '0';
  101. textArea.style.left = '0';
  102. textArea.style.position = 'fixed';
  103. document.body.appendChild(textArea);
  104. textArea.focus();
  105. textArea.select();
  106. try {
  107. var successful = document.execCommand('copy');
  108. var msg = successful ? 'successful' : 'unsuccessful';
  109. console.log('Fallback: Copying text command was ' + msg);
  110. } catch (err) {
  111. console.error('Fallback: Oops, unable to copy', err);
  112. }
  113. document.body.removeChild(textArea);
  114. return;
  115. }
  116. navigator.clipboard.writeText(text).then(
  117. function () {
  118. console.log('Async: Copying to clipboard was successful!');
  119. toast.success('Copying to clipboard was successful!');
  120. },
  121. function (err) {
  122. console.error('Async: Could not copy text: ', err);
  123. }
  124. );
  125. };
  126. const editMessageHandler = async (messageId) => {
  127. // let editMessage = history.messages[messageId];
  128. history.messages[messageId].edit = true;
  129. history.messages[messageId].editedContent = history.messages[messageId].content;
  130. };
  131. const confirmEditMessage = async (messageId) => {
  132. history.messages[messageId].edit = false;
  133. let userPrompt = history.messages[messageId].editedContent;
  134. let userMessageId = uuidv4();
  135. let userMessage = {
  136. id: userMessageId,
  137. parentId: history.messages[messageId].parentId,
  138. childrenIds: [],
  139. role: 'user',
  140. content: userPrompt
  141. };
  142. let messageParentId = history.messages[messageId].parentId;
  143. if (messageParentId !== null) {
  144. history.messages[messageParentId].childrenIds = [
  145. ...history.messages[messageParentId].childrenIds,
  146. userMessageId
  147. ];
  148. }
  149. history.messages[userMessageId] = userMessage;
  150. history.currentId = userMessageId;
  151. await tick();
  152. await sendPrompt(userPrompt, userMessageId);
  153. };
  154. const cancelEditMessage = (messageId) => {
  155. history.messages[messageId].edit = false;
  156. history.messages[messageId].editedContent = undefined;
  157. };
  158. const rateMessage = async (messageIdx, rating) => {
  159. messages = messages.map((message, idx) => {
  160. if (messageIdx === idx) {
  161. message.rating = rating;
  162. }
  163. return message;
  164. });
  165. $db.updateChatById(chatId, {
  166. messages: messages,
  167. history: history
  168. });
  169. };
  170. const showPreviousMessage = async (message) => {
  171. if (message.parentId !== null) {
  172. let messageId =
  173. history.messages[message.parentId].childrenIds[
  174. Math.max(history.messages[message.parentId].childrenIds.indexOf(message.id) - 1, 0)
  175. ];
  176. if (message.id !== messageId) {
  177. let messageChildrenIds = history.messages[messageId].childrenIds;
  178. while (messageChildrenIds.length !== 0) {
  179. messageId = messageChildrenIds.at(-1);
  180. messageChildrenIds = history.messages[messageId].childrenIds;
  181. }
  182. history.currentId = messageId;
  183. }
  184. } else {
  185. let childrenIds = Object.values(history.messages)
  186. .filter((message) => message.parentId === null)
  187. .map((message) => message.id);
  188. let messageId = childrenIds[Math.max(childrenIds.indexOf(message.id) - 1, 0)];
  189. if (message.id !== messageId) {
  190. let messageChildrenIds = history.messages[messageId].childrenIds;
  191. while (messageChildrenIds.length !== 0) {
  192. messageId = messageChildrenIds.at(-1);
  193. messageChildrenIds = history.messages[messageId].childrenIds;
  194. }
  195. history.currentId = messageId;
  196. }
  197. }
  198. await tick();
  199. autoScroll = window.innerHeight + window.scrollY >= document.body.offsetHeight - 40;
  200. setTimeout(() => {
  201. window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
  202. }, 100);
  203. };
  204. const showNextMessage = async (message) => {
  205. if (message.parentId !== null) {
  206. let messageId =
  207. history.messages[message.parentId].childrenIds[
  208. Math.min(
  209. history.messages[message.parentId].childrenIds.indexOf(message.id) + 1,
  210. history.messages[message.parentId].childrenIds.length - 1
  211. )
  212. ];
  213. if (message.id !== messageId) {
  214. let messageChildrenIds = history.messages[messageId].childrenIds;
  215. while (messageChildrenIds.length !== 0) {
  216. messageId = messageChildrenIds.at(-1);
  217. messageChildrenIds = history.messages[messageId].childrenIds;
  218. }
  219. history.currentId = messageId;
  220. }
  221. } else {
  222. let childrenIds = Object.values(history.messages)
  223. .filter((message) => message.parentId === null)
  224. .map((message) => message.id);
  225. let messageId =
  226. childrenIds[Math.min(childrenIds.indexOf(message.id) + 1, childrenIds.length - 1)];
  227. if (message.id !== messageId) {
  228. let messageChildrenIds = history.messages[messageId].childrenIds;
  229. while (messageChildrenIds.length !== 0) {
  230. messageId = messageChildrenIds.at(-1);
  231. messageChildrenIds = history.messages[messageId].childrenIds;
  232. }
  233. history.currentId = messageId;
  234. }
  235. }
  236. await tick();
  237. autoScroll = window.innerHeight + window.scrollY >= document.body.offsetHeight - 40;
  238. setTimeout(() => {
  239. window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
  240. }, 100);
  241. };
  242. </script>
  243. {#if messages.length == 0}
  244. <div class="m-auto text-center max-w-md pb-56 px-2">
  245. <div class="flex justify-center mt-8">
  246. <img src="/ollama.png" class=" w-16 invert-[10%] dark:invert-[100%] rounded-full" />
  247. </div>
  248. <div class=" mt-1 text-2xl text-gray-800 dark:text-gray-100 font-semibold">
  249. How can I help you today?
  250. </div>
  251. </div>
  252. {:else}
  253. {#each messages as message, messageIdx}
  254. <div class=" w-full">
  255. <div class="flex justify-between px-5 mb-3 max-w-3xl mx-auto rounded-lg group">
  256. <div class=" flex w-full">
  257. <div class=" mr-4">
  258. {#if message.role === 'user'}
  259. {#if $config === null}
  260. <img
  261. src="{$settings.gravatarUrl ? $settings.gravatarUrl : '/user'}.png"
  262. class=" max-w-[28px] object-cover rounded-full"
  263. alt="User profile"
  264. />
  265. {:else}
  266. <img
  267. src={$user ? $user.profile_image_url : '/user.png'}
  268. class=" max-w-[28px] object-cover rounded-full"
  269. alt="User profile"
  270. />
  271. {/if}
  272. {:else}
  273. <img
  274. src="/favicon.png"
  275. class=" max-w-[28px] object-cover rounded-full"
  276. alt="Ollama profile"
  277. />
  278. {/if}
  279. </div>
  280. <div class="w-full">
  281. <div class=" self-center font-bold mb-0.5">
  282. {#if message.role === 'user'}
  283. You
  284. {:else}
  285. Ollama <span class=" text-gray-500 text-sm font-medium"
  286. >{message.model ? ` ${message.model}` : ''}</span
  287. >
  288. {/if}
  289. </div>
  290. {#if message.role !== 'user' && message.content === ''}
  291. <div class="w-full mt-3">
  292. <div class="animate-pulse flex w-full">
  293. <div class="space-y-2 w-full">
  294. <div class="h-2 bg-gray-200 dark:bg-gray-600 rounded mr-14" />
  295. <div class="grid grid-cols-3 gap-4">
  296. <div class="h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-2" />
  297. <div class="h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-1" />
  298. </div>
  299. <div class="grid grid-cols-4 gap-4">
  300. <div class="h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-1" />
  301. <div class="h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-2" />
  302. <div class="h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-1 mr-4" />
  303. </div>
  304. <div class="h-2 bg-gray-200 dark:bg-gray-600 rounded" />
  305. </div>
  306. </div>
  307. </div>
  308. {:else}
  309. <div
  310. class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:my-0 prose-p:-mb-4 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-6 prose-li:-mb-4 whitespace-pre-line"
  311. >
  312. {#if message.role == 'user'}
  313. {#if message?.edit === true}
  314. <div class=" w-full">
  315. <textarea
  316. class=" bg-transparent outline-none w-full resize-none"
  317. bind:value={history.messages[message.id].editedContent}
  318. on:input={(e) => {
  319. e.target.style.height = '';
  320. e.target.style.height = `${e.target.scrollHeight}px`;
  321. }}
  322. on:focus={(e) => {
  323. e.target.style.height = '';
  324. e.target.style.height = `${e.target.scrollHeight}px`;
  325. }}
  326. />
  327. <div class=" flex justify-end space-x-2 text-sm font-medium">
  328. <button
  329. class="px-4 py-2.5 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded-lg"
  330. on:click={() => {
  331. confirmEditMessage(message.id);
  332. }}
  333. >
  334. Save & Submit
  335. </button>
  336. <button
  337. class=" px-4 py-2.5 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-100 transition outline outline-1 outline-gray-200 dark:outline-gray-600 rounded-lg"
  338. on:click={() => {
  339. cancelEditMessage(message.id);
  340. }}
  341. >
  342. Cancel
  343. </button>
  344. </div>
  345. </div>
  346. {:else}
  347. <div class="w-full">
  348. {message.content}
  349. <div class=" flex justify-start space-x-1">
  350. {#if message.parentId !== null && message.parentId in history.messages && (history.messages[message.parentId]?.childrenIds.length ?? 0) > 1}
  351. <div class="flex self-center">
  352. <button
  353. class="self-center"
  354. on:click={() => {
  355. showPreviousMessage(message);
  356. }}
  357. >
  358. <svg
  359. xmlns="http://www.w3.org/2000/svg"
  360. viewBox="0 0 20 20"
  361. fill="currentColor"
  362. class="w-4 h-4"
  363. >
  364. <path
  365. fill-rule="evenodd"
  366. d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z"
  367. clip-rule="evenodd"
  368. />
  369. </svg>
  370. </button>
  371. <div class="text-xs font-bold self-center">
  372. {history.messages[message.parentId].childrenIds.indexOf(message.id) +
  373. 1} / {history.messages[message.parentId].childrenIds.length}
  374. </div>
  375. <button
  376. class="self-center"
  377. on:click={() => {
  378. showNextMessage(message);
  379. }}
  380. >
  381. <svg
  382. xmlns="http://www.w3.org/2000/svg"
  383. viewBox="0 0 20 20"
  384. fill="currentColor"
  385. class="w-4 h-4"
  386. >
  387. <path
  388. fill-rule="evenodd"
  389. d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
  390. clip-rule="evenodd"
  391. />
  392. </svg>
  393. </button>
  394. </div>
  395. {:else if message.parentId === null && Object.values(history.messages).filter((message) => message.parentId === null).length > 1}
  396. <div class="flex self-center">
  397. <button
  398. class="self-center"
  399. on:click={() => {
  400. showPreviousMessage(message);
  401. }}
  402. >
  403. <svg
  404. xmlns="http://www.w3.org/2000/svg"
  405. viewBox="0 0 20 20"
  406. fill="currentColor"
  407. class="w-4 h-4"
  408. >
  409. <path
  410. fill-rule="evenodd"
  411. d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z"
  412. clip-rule="evenodd"
  413. />
  414. </svg>
  415. </button>
  416. <div class="text-xs font-bold self-center">
  417. {Object.values(history.messages)
  418. .filter((message) => message.parentId === null)
  419. .map((message) => message.id)
  420. .indexOf(message.id) + 1} / {Object.values(history.messages).filter(
  421. (message) => message.parentId === null
  422. ).length}
  423. </div>
  424. <button
  425. class="self-center"
  426. on:click={() => {
  427. showNextMessage(message);
  428. }}
  429. >
  430. <svg
  431. xmlns="http://www.w3.org/2000/svg"
  432. viewBox="0 0 20 20"
  433. fill="currentColor"
  434. class="w-4 h-4"
  435. >
  436. <path
  437. fill-rule="evenodd"
  438. d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
  439. clip-rule="evenodd"
  440. />
  441. </svg>
  442. </button>
  443. </div>
  444. {/if}
  445. <button
  446. class="invisible group-hover:visible p-1 rounded dark:hover:bg-gray-800 transition"
  447. on:click={() => {
  448. editMessageHandler(message.id);
  449. }}
  450. >
  451. <svg
  452. xmlns="http://www.w3.org/2000/svg"
  453. fill="none"
  454. viewBox="0 0 24 24"
  455. stroke-width="1.5"
  456. stroke="currentColor"
  457. class="w-4 h-4"
  458. >
  459. <path
  460. stroke-linecap="round"
  461. stroke-linejoin="round"
  462. 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"
  463. />
  464. </svg>
  465. </button>
  466. </div>
  467. </div>
  468. {/if}
  469. {:else}
  470. <div class="w-full">
  471. {@html marked(message.content.replace('\\\\', '\\\\\\'))}
  472. {#if message.done}
  473. <div class=" flex justify-start space-x-1 -mt-2">
  474. {#if message.parentId !== null && message.parentId in history.messages && (history.messages[message.parentId]?.childrenIds.length ?? 0) > 1}
  475. <div class="flex self-center">
  476. <button
  477. class="self-center"
  478. on:click={() => {
  479. showPreviousMessage(message);
  480. }}
  481. >
  482. <svg
  483. xmlns="http://www.w3.org/2000/svg"
  484. viewBox="0 0 20 20"
  485. fill="currentColor"
  486. class="w-4 h-4"
  487. >
  488. <path
  489. fill-rule="evenodd"
  490. d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z"
  491. clip-rule="evenodd"
  492. />
  493. </svg>
  494. </button>
  495. <div class="text-xs font-bold self-center">
  496. {history.messages[message.parentId].childrenIds.indexOf(message.id) +
  497. 1} / {history.messages[message.parentId].childrenIds.length}
  498. </div>
  499. <button
  500. class="self-center"
  501. on:click={() => {
  502. showNextMessage(message);
  503. }}
  504. >
  505. <svg
  506. xmlns="http://www.w3.org/2000/svg"
  507. viewBox="0 0 20 20"
  508. fill="currentColor"
  509. class="w-4 h-4"
  510. >
  511. <path
  512. fill-rule="evenodd"
  513. d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
  514. clip-rule="evenodd"
  515. />
  516. </svg>
  517. </button>
  518. </div>
  519. {/if}
  520. <button
  521. class="{messageIdx + 1 === messages.length
  522. ? 'visible'
  523. : 'invisible group-hover:visible'} p-1 rounded dark:hover:bg-gray-800 transition"
  524. on:click={() => {
  525. copyToClipboard(message.content);
  526. }}
  527. >
  528. <svg
  529. xmlns="http://www.w3.org/2000/svg"
  530. fill="none"
  531. viewBox="0 0 24 24"
  532. stroke-width="1.5"
  533. stroke="currentColor"
  534. class="w-4 h-4"
  535. >
  536. <path
  537. stroke-linecap="round"
  538. stroke-linejoin="round"
  539. 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"
  540. />
  541. </svg>
  542. </button>
  543. <button
  544. class="{messageIdx + 1 === messages.length
  545. ? 'visible'
  546. : 'invisible group-hover:visible'} p-1 rounded dark:hover:bg-gray-800 transition"
  547. on:click={() => {
  548. rateMessage(messageIdx, 1);
  549. }}
  550. >
  551. <svg
  552. stroke="currentColor"
  553. fill="none"
  554. stroke-width="2"
  555. viewBox="0 0 24 24"
  556. stroke-linecap="round"
  557. stroke-linejoin="round"
  558. class="w-4 h-4"
  559. xmlns="http://www.w3.org/2000/svg"
  560. ><path
  561. 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"
  562. /></svg
  563. >
  564. </button>
  565. <button
  566. class="{messageIdx + 1 === messages.length
  567. ? 'visible'
  568. : 'invisible group-hover:visible'} p-1 rounded dark:hover:bg-gray-800 transition"
  569. on:click={() => {
  570. rateMessage(messageIdx, -1);
  571. }}
  572. >
  573. <svg
  574. stroke="currentColor"
  575. fill="none"
  576. stroke-width="2"
  577. viewBox="0 0 24 24"
  578. stroke-linecap="round"
  579. stroke-linejoin="round"
  580. class="w-4 h-4"
  581. xmlns="http://www.w3.org/2000/svg"
  582. ><path
  583. 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"
  584. /></svg
  585. >
  586. </button>
  587. {#if messageIdx + 1 === messages.length}
  588. <button
  589. type="button"
  590. class="{messageIdx + 1 === messages.length
  591. ? 'visible'
  592. : 'invisible group-hover:visible'} p-1 rounded dark:hover:bg-gray-800 transition"
  593. on:click={regenerateResponse}
  594. >
  595. <svg
  596. xmlns="http://www.w3.org/2000/svg"
  597. fill="none"
  598. viewBox="0 0 24 24"
  599. stroke-width="1.5"
  600. stroke="currentColor"
  601. class="w-4 h-4"
  602. >
  603. <path
  604. stroke-linecap="round"
  605. stroke-linejoin="round"
  606. 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"
  607. />
  608. </svg>
  609. </button>
  610. {/if}
  611. </div>
  612. {/if}
  613. </div>
  614. {/if}
  615. </div>
  616. {/if}
  617. </div>
  618. <!-- {} -->
  619. </div>
  620. </div>
  621. </div>
  622. {/each}
  623. {/if}