Messages.svelte 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. <script lang="ts">
  2. import { v4 as uuidv4 } from 'uuid';
  3. import { chats, config, settings, user as _user, mobile, currentChatPage } from '$lib/stores';
  4. import { tick, getContext, onMount } from 'svelte';
  5. import { toast } from 'svelte-sonner';
  6. import { getChatList, updateChatById } from '$lib/apis/chats';
  7. import { copyToClipboard, findWordIndices } from '$lib/utils';
  8. import UserMessage from './Messages/UserMessage.svelte';
  9. import ResponseMessage from './Messages/ResponseMessage.svelte';
  10. import Placeholder from './Messages/Placeholder.svelte';
  11. import MultiResponseMessages from './Messages/MultiResponseMessages.svelte';
  12. const i18n = getContext('i18n');
  13. export let chatId = '';
  14. export let readOnly = false;
  15. export let sendPrompt: Function;
  16. export let continueGeneration: Function;
  17. export let regenerateResponse: Function;
  18. export let chatActionHandler: Function;
  19. export let user = $_user;
  20. export let prompt;
  21. export let processing = '';
  22. export let bottomPadding = false;
  23. export let autoScroll;
  24. export let history = {};
  25. export let messages = [];
  26. export let selectedModels;
  27. $: if (autoScroll && bottomPadding) {
  28. (async () => {
  29. await tick();
  30. scrollToBottom();
  31. })();
  32. }
  33. const scrollToBottom = () => {
  34. const element = document.getElementById('messages-container');
  35. element.scrollTop = element.scrollHeight;
  36. };
  37. const copyToClipboardWithToast = async (text) => {
  38. const res = await copyToClipboard(text);
  39. if (res) {
  40. toast.success($i18n.t('Copying to clipboard was successful!'));
  41. }
  42. };
  43. const confirmEditMessage = async (messageId, content) => {
  44. let userPrompt = content;
  45. let userMessageId = uuidv4();
  46. let userMessage = {
  47. id: userMessageId,
  48. parentId: history.messages[messageId].parentId,
  49. childrenIds: [],
  50. role: 'user',
  51. content: userPrompt,
  52. ...(history.messages[messageId].files && { files: history.messages[messageId].files }),
  53. models: selectedModels
  54. };
  55. let messageParentId = history.messages[messageId].parentId;
  56. if (messageParentId !== null) {
  57. history.messages[messageParentId].childrenIds = [
  58. ...history.messages[messageParentId].childrenIds,
  59. userMessageId
  60. ];
  61. }
  62. history.messages[userMessageId] = userMessage;
  63. history.currentId = userMessageId;
  64. await tick();
  65. await sendPrompt(userPrompt, userMessageId);
  66. };
  67. const updateChatMessages = async () => {
  68. await tick();
  69. await updateChatById(localStorage.token, chatId, {
  70. messages: messages,
  71. history: history
  72. });
  73. currentChatPage.set(1);
  74. await chats.set(await getChatList(localStorage.token, $currentChatPage));
  75. };
  76. const confirmEditResponseMessage = async (messageId, content) => {
  77. history.messages[messageId].originalContent = history.messages[messageId].content;
  78. history.messages[messageId].content = content;
  79. await updateChatMessages();
  80. };
  81. const rateMessage = async (messageId, rating) => {
  82. history.messages[messageId].annotation = {
  83. ...history.messages[messageId].annotation,
  84. rating: rating
  85. };
  86. await updateChatMessages();
  87. };
  88. const showPreviousMessage = async (message) => {
  89. if (message.parentId !== null) {
  90. let messageId =
  91. history.messages[message.parentId].childrenIds[
  92. Math.max(history.messages[message.parentId].childrenIds.indexOf(message.id) - 1, 0)
  93. ];
  94. if (message.id !== messageId) {
  95. let messageChildrenIds = history.messages[messageId].childrenIds;
  96. while (messageChildrenIds.length !== 0) {
  97. messageId = messageChildrenIds.at(-1);
  98. messageChildrenIds = history.messages[messageId].childrenIds;
  99. }
  100. history.currentId = messageId;
  101. }
  102. } else {
  103. let childrenIds = Object.values(history.messages)
  104. .filter((message) => message.parentId === null)
  105. .map((message) => message.id);
  106. let messageId = childrenIds[Math.max(childrenIds.indexOf(message.id) - 1, 0)];
  107. if (message.id !== messageId) {
  108. let messageChildrenIds = history.messages[messageId].childrenIds;
  109. while (messageChildrenIds.length !== 0) {
  110. messageId = messageChildrenIds.at(-1);
  111. messageChildrenIds = history.messages[messageId].childrenIds;
  112. }
  113. history.currentId = messageId;
  114. }
  115. }
  116. await tick();
  117. if ($settings?.scrollOnBranchChange ?? true) {
  118. const element = document.getElementById('messages-container');
  119. autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
  120. setTimeout(() => {
  121. scrollToBottom();
  122. }, 100);
  123. }
  124. };
  125. const showNextMessage = async (message) => {
  126. if (message.parentId !== null) {
  127. let messageId =
  128. history.messages[message.parentId].childrenIds[
  129. Math.min(
  130. history.messages[message.parentId].childrenIds.indexOf(message.id) + 1,
  131. history.messages[message.parentId].childrenIds.length - 1
  132. )
  133. ];
  134. if (message.id !== messageId) {
  135. let messageChildrenIds = history.messages[messageId].childrenIds;
  136. while (messageChildrenIds.length !== 0) {
  137. messageId = messageChildrenIds.at(-1);
  138. messageChildrenIds = history.messages[messageId].childrenIds;
  139. }
  140. history.currentId = messageId;
  141. }
  142. } else {
  143. let childrenIds = Object.values(history.messages)
  144. .filter((message) => message.parentId === null)
  145. .map((message) => message.id);
  146. let messageId =
  147. childrenIds[Math.min(childrenIds.indexOf(message.id) + 1, childrenIds.length - 1)];
  148. if (message.id !== messageId) {
  149. let messageChildrenIds = history.messages[messageId].childrenIds;
  150. while (messageChildrenIds.length !== 0) {
  151. messageId = messageChildrenIds.at(-1);
  152. messageChildrenIds = history.messages[messageId].childrenIds;
  153. }
  154. history.currentId = messageId;
  155. }
  156. }
  157. await tick();
  158. if ($settings?.scrollOnBranchChange ?? true) {
  159. const element = document.getElementById('messages-container');
  160. autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
  161. setTimeout(() => {
  162. scrollToBottom();
  163. }, 100);
  164. }
  165. };
  166. const deleteMessageHandler = async (messageId) => {
  167. const messageToDelete = history.messages[messageId];
  168. const parentMessageId = messageToDelete.parentId;
  169. const childMessageIds = messageToDelete.childrenIds ?? [];
  170. const hasDescendantMessages = childMessageIds.some(
  171. (childId) => history.messages[childId]?.childrenIds?.length > 0
  172. );
  173. history.currentId = parentMessageId;
  174. await tick();
  175. // Remove the message itself from the parent message's children array
  176. history.messages[parentMessageId].childrenIds = history.messages[
  177. parentMessageId
  178. ].childrenIds.filter((id) => id !== messageId);
  179. await tick();
  180. childMessageIds.forEach((childId) => {
  181. const childMessage = history.messages[childId];
  182. if (childMessage && childMessage.childrenIds) {
  183. if (childMessage.childrenIds.length === 0 && !hasDescendantMessages) {
  184. // If there are no other responses/prompts
  185. history.messages[parentMessageId].childrenIds = [];
  186. } else {
  187. childMessage.childrenIds.forEach((grandChildId) => {
  188. if (history.messages[grandChildId]) {
  189. history.messages[grandChildId].parentId = parentMessageId;
  190. history.messages[parentMessageId].childrenIds.push(grandChildId);
  191. }
  192. });
  193. }
  194. }
  195. // Remove child message id from the parent message's children array
  196. history.messages[parentMessageId].childrenIds = history.messages[
  197. parentMessageId
  198. ].childrenIds.filter((id) => id !== childId);
  199. });
  200. await tick();
  201. await updateChatById(localStorage.token, chatId, {
  202. messages: messages,
  203. history: history
  204. });
  205. };
  206. </script>
  207. <div class="h-full flex">
  208. {#if messages.length == 0}
  209. <Placeholder
  210. modelIds={selectedModels}
  211. submitPrompt={async (p) => {
  212. let text = p;
  213. if (p.includes('{{CLIPBOARD}}')) {
  214. const clipboardText = await navigator.clipboard.readText().catch((err) => {
  215. toast.error($i18n.t('Failed to read clipboard contents'));
  216. return '{{CLIPBOARD}}';
  217. });
  218. text = p.replaceAll('{{CLIPBOARD}}', clipboardText);
  219. }
  220. prompt = text;
  221. await tick();
  222. const chatInputElement = document.getElementById('chat-textarea');
  223. if (chatInputElement) {
  224. prompt = p;
  225. chatInputElement.style.height = '';
  226. chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 200) + 'px';
  227. chatInputElement.focus();
  228. const words = findWordIndices(prompt);
  229. if (words.length > 0) {
  230. const word = words.at(0);
  231. chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
  232. }
  233. }
  234. await tick();
  235. }}
  236. />
  237. {:else}
  238. <div class="w-full pt-2">
  239. {#key chatId}
  240. {#each messages as message, messageIdx}
  241. <div class=" w-full {messageIdx === messages.length - 1 ? ' pb-12' : ''}">
  242. <div
  243. class="flex flex-col justify-between px-5 mb-3 {($settings?.widescreenMode ?? null)
  244. ? 'max-w-full'
  245. : 'max-w-5xl'} mx-auto rounded-lg group"
  246. >
  247. {#if message.role === 'user'}
  248. <UserMessage
  249. on:delete={() => deleteMessageHandler(message.id)}
  250. {user}
  251. {readOnly}
  252. {message}
  253. isFirstMessage={messageIdx === 0}
  254. siblings={message.parentId !== null
  255. ? (history.messages[message.parentId]?.childrenIds ?? [])
  256. : (Object.values(history.messages)
  257. .filter((message) => message.parentId === null)
  258. .map((message) => message.id) ?? [])}
  259. {confirmEditMessage}
  260. {showPreviousMessage}
  261. {showNextMessage}
  262. copyToClipboard={copyToClipboardWithToast}
  263. />
  264. {:else if $mobile || (history.messages[message.parentId]?.models?.length ?? 1) === 1}
  265. {#key message.id && history.currentId}
  266. <ResponseMessage
  267. {message}
  268. siblings={history.messages[message.parentId]?.childrenIds ?? []}
  269. isLastMessage={messageIdx + 1 === messages.length}
  270. {readOnly}
  271. {updateChatMessages}
  272. {confirmEditResponseMessage}
  273. {showPreviousMessage}
  274. {showNextMessage}
  275. {rateMessage}
  276. copyToClipboard={copyToClipboardWithToast}
  277. {continueGeneration}
  278. {regenerateResponse}
  279. on:action={async (e) => {
  280. console.log('action', e);
  281. if (typeof e.detail === 'string') {
  282. await chatActionHandler(chatId, e.detail, message.model, message.id);
  283. } else {
  284. const { id, event } = e.detail;
  285. await chatActionHandler(chatId, id, message.model, message.id, event);
  286. }
  287. }}
  288. on:save={async (e) => {
  289. console.log('save', e);
  290. const message = e.detail;
  291. history.messages[message.id] = message;
  292. await updateChatById(localStorage.token, chatId, {
  293. messages: messages,
  294. history: history
  295. });
  296. }}
  297. />
  298. {/key}
  299. {:else}
  300. {#key message.parentId}
  301. <MultiResponseMessages
  302. bind:history
  303. {messages}
  304. {readOnly}
  305. {chatId}
  306. parentMessage={history.messages[message.parentId]}
  307. {messageIdx}
  308. {updateChatMessages}
  309. {confirmEditResponseMessage}
  310. {rateMessage}
  311. copyToClipboard={copyToClipboardWithToast}
  312. {continueGeneration}
  313. {regenerateResponse}
  314. on:change={async () => {
  315. await updateChatById(localStorage.token, chatId, {
  316. messages: messages,
  317. history: history
  318. });
  319. if (autoScroll) {
  320. const element = document.getElementById('messages-container');
  321. autoScroll =
  322. element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
  323. setTimeout(() => {
  324. scrollToBottom();
  325. }, 100);
  326. }
  327. }}
  328. />
  329. {/key}
  330. {/if}
  331. </div>
  332. </div>
  333. {/each}
  334. {#if bottomPadding}
  335. <div class=" pb-6" />
  336. {/if}
  337. {/key}
  338. </div>
  339. {/if}
  340. </div>