MultiResponseMessages.svelte 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. <script lang="ts">
  2. import dayjs from 'dayjs';
  3. import { onMount, tick, getContext } from 'svelte';
  4. import { createEventDispatcher } from 'svelte';
  5. import { mobile, models, settings } from '$lib/stores';
  6. import { generateMoACompletion } from '$lib/apis';
  7. import { updateChatById } from '$lib/apis/chats';
  8. import { createOpenAITextStream } from '$lib/apis/streaming';
  9. import ResponseMessage from './ResponseMessage.svelte';
  10. import Tooltip from '$lib/components/common/Tooltip.svelte';
  11. import Merge from '$lib/components/icons/Merge.svelte';
  12. import Markdown from './Markdown.svelte';
  13. import Name from './Name.svelte';
  14. import Skeleton from './Skeleton.svelte';
  15. import localizedFormat from 'dayjs/plugin/localizedFormat';
  16. import ProfileImage from './ProfileImage.svelte';
  17. import { WEBUI_BASE_URL } from '$lib/constants';
  18. const i18n = getContext('i18n');
  19. dayjs.extend(localizedFormat);
  20. export let chatId;
  21. export let history;
  22. export let messageId;
  23. export let selectedModels = [];
  24. export let isLastMessage;
  25. export let readOnly = false;
  26. export let editCodeBlock = true;
  27. export let setInputText: Function = () => {};
  28. export let updateChat: Function;
  29. export let editMessage: Function;
  30. export let saveMessage: Function;
  31. export let rateMessage: Function;
  32. export let actionMessage: Function;
  33. export let submitMessage: Function;
  34. export let deleteMessage: Function;
  35. export let continueResponse: Function;
  36. export let regenerateResponse: Function;
  37. export let mergeResponses: Function;
  38. export let addMessages: Function;
  39. export let triggerScroll: Function;
  40. export let topPadding = false;
  41. const dispatch = createEventDispatcher();
  42. let currentMessageId;
  43. let parentMessage;
  44. let groupedMessageIds = {};
  45. let groupedMessageIdsIdx = {};
  46. let selectedModelIdx = null;
  47. let message = JSON.parse(JSON.stringify(history.messages[messageId]));
  48. $: if (history.messages) {
  49. if (JSON.stringify(message) !== JSON.stringify(history.messages[messageId])) {
  50. message = JSON.parse(JSON.stringify(history.messages[messageId]));
  51. }
  52. }
  53. const gotoMessage = async (modelIdx, messageIdx) => {
  54. // Clamp messageIdx to ensure it's within valid range
  55. groupedMessageIdsIdx[modelIdx] = Math.max(
  56. 0,
  57. Math.min(messageIdx, groupedMessageIds[modelIdx].messageIds.length - 1)
  58. );
  59. // Get the messageId at the specified index
  60. let messageId = groupedMessageIds[modelIdx].messageIds[groupedMessageIdsIdx[modelIdx]];
  61. console.log(messageId);
  62. // Traverse the branch to find the deepest child message
  63. let messageChildrenIds = history.messages[messageId].childrenIds;
  64. while (messageChildrenIds.length !== 0) {
  65. messageId = messageChildrenIds.at(-1);
  66. messageChildrenIds = history.messages[messageId].childrenIds;
  67. }
  68. // Update the current message ID in history
  69. history.currentId = messageId;
  70. // Await UI updates
  71. await tick();
  72. await updateChat();
  73. // Trigger scrolling after navigation
  74. triggerScroll();
  75. };
  76. const showPreviousMessage = async (modelIdx) => {
  77. groupedMessageIdsIdx[modelIdx] = Math.max(0, groupedMessageIdsIdx[modelIdx] - 1);
  78. let messageId = groupedMessageIds[modelIdx].messageIds[groupedMessageIdsIdx[modelIdx]];
  79. console.log(messageId);
  80. let messageChildrenIds = history.messages[messageId].childrenIds;
  81. while (messageChildrenIds.length !== 0) {
  82. messageId = messageChildrenIds.at(-1);
  83. messageChildrenIds = history.messages[messageId].childrenIds;
  84. }
  85. history.currentId = messageId;
  86. await tick();
  87. await updateChat();
  88. triggerScroll();
  89. };
  90. const showNextMessage = async (modelIdx) => {
  91. groupedMessageIdsIdx[modelIdx] = Math.min(
  92. groupedMessageIds[modelIdx].messageIds.length - 1,
  93. groupedMessageIdsIdx[modelIdx] + 1
  94. );
  95. let messageId = groupedMessageIds[modelIdx].messageIds[groupedMessageIdsIdx[modelIdx]];
  96. console.log(messageId);
  97. let messageChildrenIds = history.messages[messageId].childrenIds;
  98. while (messageChildrenIds.length !== 0) {
  99. messageId = messageChildrenIds.at(-1);
  100. messageChildrenIds = history.messages[messageId].childrenIds;
  101. }
  102. history.currentId = messageId;
  103. await tick();
  104. await updateChat();
  105. triggerScroll();
  106. };
  107. const initHandler = async () => {
  108. console.log('multiresponse:initHandler');
  109. await tick();
  110. currentMessageId = messageId;
  111. parentMessage = history.messages[messageId].parentId
  112. ? history.messages[history.messages[messageId].parentId]
  113. : null;
  114. groupedMessageIds = parentMessage?.models.reduce((a, model, modelIdx) => {
  115. // Find all messages that are children of the parent message and have the same model
  116. let modelMessageIds = parentMessage?.childrenIds
  117. .map((id) => history.messages[id])
  118. .filter((m) => m?.modelIdx === modelIdx)
  119. .map((m) => m.id);
  120. // Legacy support for messages that don't have a modelIdx
  121. // Find all messages that are children of the parent message and have the same model
  122. if (modelMessageIds.length === 0) {
  123. let modelMessages = parentMessage?.childrenIds
  124. .map((id) => history.messages[id])
  125. .filter((m) => m?.model === model);
  126. modelMessages.forEach((m) => {
  127. m.modelIdx = modelIdx;
  128. });
  129. modelMessageIds = modelMessages.map((m) => m.id);
  130. }
  131. return {
  132. ...a,
  133. [modelIdx]: { messageIds: modelMessageIds }
  134. };
  135. }, {});
  136. groupedMessageIdsIdx = parentMessage?.models.reduce((a, model, modelIdx) => {
  137. const idx = groupedMessageIds[modelIdx].messageIds.findIndex((id) => id === messageId);
  138. if (idx !== -1) {
  139. return {
  140. ...a,
  141. [modelIdx]: idx
  142. };
  143. } else {
  144. return {
  145. ...a,
  146. [modelIdx]: groupedMessageIds[modelIdx].messageIds.length - 1
  147. };
  148. }
  149. }, {});
  150. selectedModelIdx = history.messages[messageId]?.modelIdx;
  151. console.log(groupedMessageIds, groupedMessageIdsIdx);
  152. await tick();
  153. };
  154. const onGroupClick = async (_messageId, modelIdx) => {
  155. if (messageId != _messageId) {
  156. let currentMessageId = _messageId;
  157. let messageChildrenIds = history.messages[currentMessageId].childrenIds;
  158. while (messageChildrenIds.length !== 0) {
  159. currentMessageId = messageChildrenIds.at(-1);
  160. messageChildrenIds = history.messages[currentMessageId].childrenIds;
  161. }
  162. history.currentId = currentMessageId;
  163. selectedModelIdx = modelIdx;
  164. // await tick();
  165. // await updateChat();
  166. // triggerScroll();
  167. }
  168. };
  169. const mergeResponsesHandler = async () => {
  170. const responses = Object.keys(groupedMessageIds).map((modelIdx) => {
  171. const { messageIds } = groupedMessageIds[modelIdx];
  172. const messageId = messageIds[groupedMessageIdsIdx[modelIdx]];
  173. return history.messages[messageId].content;
  174. });
  175. mergeResponses(messageId, responses, chatId);
  176. };
  177. onMount(async () => {
  178. await initHandler();
  179. await tick();
  180. if ($settings?.scrollOnBranchChange ?? true) {
  181. const messageElement = document.getElementById(`message-${messageId}`);
  182. if (messageElement) {
  183. messageElement.scrollIntoView({ block: 'start' });
  184. }
  185. }
  186. });
  187. </script>
  188. {#if parentMessage}
  189. <div>
  190. <div
  191. class="flex snap-x snap-mandatory overflow-x-auto scrollbar-hidden"
  192. id="responses-container-{chatId}-{parentMessage.id}"
  193. >
  194. {#if $settings?.displayMultiModelResponsesInTabs ?? false}
  195. <div class="w-full">
  196. <div class=" flex w-full mb-4.5 border-b border-gray-200 dark:border-gray-850">
  197. <div
  198. class="flex gap-2 scrollbar-none overflow-x-auto w-fit text-center font-medium bg-transparent pt-1 text-sm"
  199. >
  200. {#each Object.keys(groupedMessageIds) as modelIdx}
  201. {#if groupedMessageIdsIdx[modelIdx] !== undefined && groupedMessageIds[modelIdx].messageIds.length > 0}
  202. <!-- svelte-ignore a11y-no-static-element-interactions -->
  203. <!-- svelte-ignore a11y-click-events-have-key-events -->
  204. {@const _messageId =
  205. groupedMessageIds[modelIdx].messageIds[groupedMessageIdsIdx[modelIdx]]}
  206. {@const model = $models.find((m) => m.id === history.messages[_messageId]?.model)}
  207. <button
  208. class="min-w-fit {selectedModelIdx == modelIdx
  209. ? ' dark:border-gray-300 '
  210. : ' opacity-35 border-transparent'} pb-1.5 px-2.5 transition border-b-2"
  211. on:click={async () => {
  212. if (selectedModelIdx != modelIdx) {
  213. selectedModelIdx = modelIdx;
  214. }
  215. onGroupClick(_messageId, modelIdx);
  216. }}
  217. >
  218. <div class="flex items-center gap-1.5">
  219. <!-- <ProfileImage
  220. src={model?.info?.meta?.profile_image_url ??
  221. ($i18n.language === 'dg-DG'
  222. ? `${WEBUI_BASE_URL}/doge.png`
  223. : `${WEBUI_BASE_URL}/favicon.png`)}
  224. className={'size-5 assistant-message-profile-image'}
  225. /> -->
  226. <div class="-translate-y-[1px]">
  227. {model ? `${model.name}` : history.messages[_messageId]?.model}
  228. </div>
  229. </div>
  230. </button>
  231. {/if}
  232. {/each}
  233. </div>
  234. </div>
  235. {#if selectedModelIdx !== null}
  236. {@const _messageId =
  237. groupedMessageIds[selectedModelIdx].messageIds[
  238. groupedMessageIdsIdx[selectedModelIdx]
  239. ]}
  240. {#key history.currentId}
  241. {#if message}
  242. <ResponseMessage
  243. {chatId}
  244. {history}
  245. messageId={_messageId}
  246. {selectedModels}
  247. isLastMessage={true}
  248. siblings={groupedMessageIds[selectedModelIdx].messageIds}
  249. gotoMessage={(message, messageIdx) => gotoMessage(selectedModelIdx, messageIdx)}
  250. showPreviousMessage={() => showPreviousMessage(selectedModelIdx)}
  251. showNextMessage={() => showNextMessage(selectedModelIdx)}
  252. {setInputText}
  253. {updateChat}
  254. {editMessage}
  255. {saveMessage}
  256. {rateMessage}
  257. {deleteMessage}
  258. {actionMessage}
  259. {submitMessage}
  260. {continueResponse}
  261. regenerateResponse={async (message, prompt = null) => {
  262. regenerateResponse(message, prompt);
  263. await tick();
  264. groupedMessageIdsIdx[selectedModelIdx] =
  265. groupedMessageIds[selectedModelIdx].messageIds.length - 1;
  266. }}
  267. {addMessages}
  268. {readOnly}
  269. {topPadding}
  270. />
  271. {/if}
  272. {/key}
  273. {/if}
  274. </div>
  275. {:else}
  276. {#each Object.keys(groupedMessageIds) as modelIdx}
  277. {#if groupedMessageIdsIdx[modelIdx] !== undefined && groupedMessageIds[modelIdx].messageIds.length > 0}
  278. <!-- svelte-ignore a11y-no-static-element-interactions -->
  279. <!-- svelte-ignore a11y-click-events-have-key-events -->
  280. {@const _messageId =
  281. groupedMessageIds[modelIdx].messageIds[groupedMessageIdsIdx[modelIdx]]}
  282. <div
  283. class=" snap-center w-full max-w-full m-1 border {history.messages[messageId]
  284. ?.modelIdx == modelIdx
  285. ? `bg-gray-50 dark:bg-gray-850 border-gray-100 dark:border-gray-800 border-2 ${
  286. $mobile ? 'min-w-full' : 'min-w-80'
  287. }`
  288. : `border-gray-100 dark:border-gray-850 border-dashed ${
  289. $mobile ? 'min-w-full' : 'min-w-80'
  290. }`} transition-all p-5 rounded-2xl"
  291. on:click={async () => {
  292. onGroupClick(_messageId, modelIdx);
  293. }}
  294. >
  295. {#key history.currentId}
  296. {#if message}
  297. <ResponseMessage
  298. {chatId}
  299. {history}
  300. messageId={_messageId}
  301. {selectedModels}
  302. isLastMessage={true}
  303. siblings={groupedMessageIds[modelIdx].messageIds}
  304. gotoMessage={(message, messageIdx) => gotoMessage(modelIdx, messageIdx)}
  305. showPreviousMessage={() => showPreviousMessage(modelIdx)}
  306. showNextMessage={() => showNextMessage(modelIdx)}
  307. {setInputText}
  308. {updateChat}
  309. {editMessage}
  310. {saveMessage}
  311. {rateMessage}
  312. {deleteMessage}
  313. {actionMessage}
  314. {submitMessage}
  315. {continueResponse}
  316. regenerateResponse={async (message, prompt = null) => {
  317. regenerateResponse(message, prompt);
  318. await tick();
  319. groupedMessageIdsIdx[modelIdx] =
  320. groupedMessageIds[modelIdx].messageIds.length - 1;
  321. }}
  322. {addMessages}
  323. {readOnly}
  324. {editCodeBlock}
  325. {topPadding}
  326. />
  327. {/if}
  328. {/key}
  329. </div>
  330. {/if}
  331. {/each}
  332. {/if}
  333. </div>
  334. {#if !readOnly}
  335. {#if !Object.keys(groupedMessageIds).find((modelIdx) => {
  336. const { messageIds } = groupedMessageIds[modelIdx];
  337. const _messageId = messageIds[groupedMessageIdsIdx[modelIdx]];
  338. return !history.messages[_messageId]?.done ?? false;
  339. })}
  340. <div class="flex justify-end">
  341. <div class="w-full">
  342. {#if history.messages[messageId]?.merged?.status}
  343. {@const message = history.messages[messageId]?.merged}
  344. <div class="w-full rounded-xl pl-5 pr-2 py-2 mt-2">
  345. <Name>
  346. {$i18n.t('Merged Response')}
  347. {#if message.timestamp}
  348. <span
  349. class=" self-center invisible group-hover:visible text-gray-400 text-xs font-medium uppercase ml-0.5 -mt-0.5"
  350. >
  351. {dayjs(message.timestamp * 1000).format('LT')}
  352. </span>
  353. {/if}
  354. </Name>
  355. <div class="mt-1 markdown-prose w-full min-w-full">
  356. {#if (message?.content ?? '') === ''}
  357. <Skeleton />
  358. {:else}
  359. <Markdown id={`merged`} content={message.content ?? ''} />
  360. {/if}
  361. </div>
  362. </div>
  363. {/if}
  364. </div>
  365. {#if isLastMessage}
  366. <div class=" shrink-0 text-gray-600 dark:text-gray-500 mt-1">
  367. <Tooltip content={$i18n.t('Merge Responses')} placement="bottom">
  368. <button
  369. type="button"
  370. id="merge-response-button"
  371. class="{true
  372. ? 'visible'
  373. : 'invisible group-hover:visible'} p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
  374. on:click={() => {
  375. mergeResponsesHandler();
  376. }}
  377. >
  378. <Merge className=" size-5 " />
  379. </button>
  380. </Tooltip>
  381. </div>
  382. {/if}
  383. </div>
  384. {/if}
  385. {/if}
  386. </div>
  387. {/if}