Messages.svelte 25 KB

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