Chat.svelte 52 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049
  1. <script lang="ts">
  2. import { v4 as uuidv4 } from 'uuid';
  3. import { toast } from 'svelte-sonner';
  4. import mermaid from 'mermaid';
  5. import { PaneGroup, Pane, PaneResizer } from 'paneforge';
  6. import { getContext, onDestroy, onMount, tick } from 'svelte';
  7. const i18n: Writable<i18nType> = getContext('i18n');
  8. import { goto } from '$app/navigation';
  9. import { page } from '$app/stores';
  10. import { get, type Unsubscriber, type Writable } from 'svelte/store';
  11. import type { i18n as i18nType } from 'i18next';
  12. import { WEBUI_BASE_URL } from '$lib/constants';
  13. import {
  14. chatId,
  15. chats,
  16. config,
  17. type Model,
  18. models,
  19. tags as allTags,
  20. settings,
  21. showSidebar,
  22. WEBUI_NAME,
  23. banners,
  24. user,
  25. socket,
  26. showControls,
  27. showCallOverlay,
  28. currentChatPage,
  29. temporaryChatEnabled,
  30. mobile,
  31. showOverview,
  32. chatTitle,
  33. showArtifacts,
  34. tools
  35. } from '$lib/stores';
  36. import {
  37. convertMessagesToHistory,
  38. copyToClipboard,
  39. getMessageContentParts,
  40. extractSentencesForAudio,
  41. promptTemplate,
  42. splitStream
  43. } from '$lib/utils';
  44. import { generateChatCompletion } from '$lib/apis/ollama';
  45. import {
  46. addTagById,
  47. createNewChat,
  48. deleteTagById,
  49. deleteTagsById,
  50. getAllTags,
  51. getChatById,
  52. getChatList,
  53. getTagsById,
  54. updateChatById
  55. } from '$lib/apis/chats';
  56. import { generateOpenAIChatCompletion } from '$lib/apis/openai';
  57. import { processWeb, processWebSearch, processYoutubeVideo } from '$lib/apis/retrieval';
  58. import { createOpenAITextStream } from '$lib/apis/streaming';
  59. import { queryMemory } from '$lib/apis/memories';
  60. import { getAndUpdateUserLocation, getUserSettings } from '$lib/apis/users';
  61. import {
  62. chatCompleted,
  63. generateQueries,
  64. chatAction,
  65. generateMoACompletion,
  66. stopTask
  67. } from '$lib/apis';
  68. import Banner from '../common/Banner.svelte';
  69. import MessageInput from '$lib/components/chat/MessageInput.svelte';
  70. import Messages from '$lib/components/chat/Messages.svelte';
  71. import Navbar from '$lib/components/layout/Navbar.svelte';
  72. import ChatControls from './ChatControls.svelte';
  73. import EventConfirmDialog from '../common/ConfirmDialog.svelte';
  74. import Placeholder from './Placeholder.svelte';
  75. import { getTools } from '$lib/apis/tools';
  76. import NotificationToast from '../NotificationToast.svelte';
  77. export let chatIdProp = '';
  78. let loaded = false;
  79. const eventTarget = new EventTarget();
  80. let controlPane;
  81. let controlPaneComponent;
  82. let autoScroll = true;
  83. let processing = '';
  84. let messagesContainerElement: HTMLDivElement;
  85. let navbarElement;
  86. let showEventConfirmation = false;
  87. let eventConfirmationTitle = '';
  88. let eventConfirmationMessage = '';
  89. let eventConfirmationInput = false;
  90. let eventConfirmationInputPlaceholder = '';
  91. let eventConfirmationInputValue = '';
  92. let eventCallback = null;
  93. let chatIdUnsubscriber: Unsubscriber | undefined;
  94. let selectedModels = [''];
  95. let atSelectedModel: Model | undefined;
  96. let selectedModelIds = [];
  97. $: selectedModelIds = atSelectedModel !== undefined ? [atSelectedModel.id] : selectedModels;
  98. let selectedToolIds = [];
  99. let webSearchEnabled = false;
  100. let chat = null;
  101. let tags = [];
  102. let history = {
  103. messages: {},
  104. currentId: null
  105. };
  106. let taskId = null;
  107. // Chat Input
  108. let prompt = '';
  109. let chatFiles = [];
  110. let files = [];
  111. let params = {};
  112. $: if (chatIdProp) {
  113. (async () => {
  114. console.log(chatIdProp);
  115. if (chatIdProp && (await loadChat())) {
  116. await tick();
  117. loaded = true;
  118. window.setTimeout(() => scrollToBottom(), 0);
  119. const chatInput = document.getElementById('chat-input');
  120. chatInput?.focus();
  121. } else {
  122. await goto('/');
  123. }
  124. })();
  125. }
  126. $: if (selectedModels && chatIdProp !== '') {
  127. saveSessionSelectedModels();
  128. }
  129. const saveSessionSelectedModels = () => {
  130. if (selectedModels.length === 0 || (selectedModels.length === 1 && selectedModels[0] === '')) {
  131. return;
  132. }
  133. sessionStorage.selectedModels = JSON.stringify(selectedModels);
  134. console.log('saveSessionSelectedModels', selectedModels, sessionStorage.selectedModels);
  135. };
  136. $: if (selectedModels) {
  137. setToolIds();
  138. }
  139. const setToolIds = async () => {
  140. if (!$tools) {
  141. tools.set(await getTools(localStorage.token));
  142. }
  143. if (selectedModels.length !== 1) {
  144. return;
  145. }
  146. const model = $models.find((m) => m.id === selectedModels[0]);
  147. if (model) {
  148. selectedToolIds = (model?.info?.meta?.toolIds ?? []).filter((id) =>
  149. $tools.find((t) => t.id === id)
  150. );
  151. }
  152. };
  153. const showMessage = async (message) => {
  154. const _chatId = JSON.parse(JSON.stringify($chatId));
  155. let _messageId = JSON.parse(JSON.stringify(message.id));
  156. let messageChildrenIds = history.messages[_messageId].childrenIds;
  157. while (messageChildrenIds.length !== 0) {
  158. _messageId = messageChildrenIds.at(-1);
  159. messageChildrenIds = history.messages[_messageId].childrenIds;
  160. }
  161. history.currentId = _messageId;
  162. await tick();
  163. await tick();
  164. await tick();
  165. const messageElement = document.getElementById(`message-${message.id}`);
  166. if (messageElement) {
  167. messageElement.scrollIntoView({ behavior: 'smooth' });
  168. }
  169. await tick();
  170. saveChatHandler(_chatId);
  171. };
  172. const chatEventHandler = async (event, cb) => {
  173. console.log(event);
  174. if (event.chat_id === $chatId) {
  175. await tick();
  176. let message = history.messages[event.message_id];
  177. if (message) {
  178. const type = event?.data?.type ?? null;
  179. const data = event?.data?.data ?? null;
  180. if (type === 'status') {
  181. if (message?.statusHistory) {
  182. message.statusHistory.push(data);
  183. } else {
  184. message.statusHistory = [data];
  185. }
  186. } else if (type === 'source' || type === 'citation') {
  187. if (data?.type === 'code_execution') {
  188. // Code execution; update existing code execution by ID, or add new one.
  189. if (!message?.code_executions) {
  190. message.code_executions = [];
  191. }
  192. const existingCodeExecutionIndex = message.code_executions.findIndex(
  193. (execution) => execution.id === data.id
  194. );
  195. if (existingCodeExecutionIndex !== -1) {
  196. message.code_executions[existingCodeExecutionIndex] = data;
  197. } else {
  198. message.code_executions.push(data);
  199. }
  200. message.code_executions = message.code_executions;
  201. } else {
  202. // Regular source.
  203. if (message?.sources) {
  204. message.sources.push(data);
  205. } else {
  206. message.sources = [data];
  207. }
  208. }
  209. } else if (type === 'chat:completion') {
  210. chatCompletionEventHandler(data, message, event.chat_id);
  211. } else if (type === 'chat:title') {
  212. chatTitle.set(data);
  213. currentChatPage.set(1);
  214. await chats.set(await getChatList(localStorage.token, $currentChatPage));
  215. } else if (type === 'chat:tags') {
  216. chat = await getChatById(localStorage.token, $chatId);
  217. allTags.set(await getAllTags(localStorage.token));
  218. } else if (type === 'message') {
  219. message.content += data.content;
  220. } else if (type === 'replace') {
  221. message.content = data.content;
  222. } else if (type === 'action') {
  223. if (data.action === 'continue') {
  224. const continueButton = document.getElementById('continue-response-button');
  225. if (continueButton) {
  226. continueButton.click();
  227. }
  228. }
  229. } else if (type === 'confirmation') {
  230. eventCallback = cb;
  231. eventConfirmationInput = false;
  232. showEventConfirmation = true;
  233. eventConfirmationTitle = data.title;
  234. eventConfirmationMessage = data.message;
  235. } else if (type === 'execute') {
  236. eventCallback = cb;
  237. try {
  238. // Use Function constructor to evaluate code in a safer way
  239. const asyncFunction = new Function(`return (async () => { ${data.code} })()`);
  240. const result = await asyncFunction(); // Await the result of the async function
  241. if (cb) {
  242. cb(result);
  243. }
  244. } catch (error) {
  245. console.error('Error executing code:', error);
  246. }
  247. } else if (type === 'input') {
  248. eventCallback = cb;
  249. eventConfirmationInput = true;
  250. showEventConfirmation = true;
  251. eventConfirmationTitle = data.title;
  252. eventConfirmationMessage = data.message;
  253. eventConfirmationInputPlaceholder = data.placeholder;
  254. eventConfirmationInputValue = data?.value ?? '';
  255. } else {
  256. console.log('Unknown message type', data);
  257. }
  258. history.messages[event.message_id] = message;
  259. }
  260. } else {
  261. await tick();
  262. const type = event?.data?.type ?? null;
  263. const data = event?.data?.data ?? null;
  264. if (type === 'chat:completion') {
  265. const { done, content, title } = data;
  266. if (done) {
  267. toast.custom(NotificationToast, {
  268. componentProps: {
  269. onClick: () => {
  270. goto(`/c/${event.chat_id}`);
  271. },
  272. content: content,
  273. title: title
  274. },
  275. duration: 15000,
  276. unstyled: true
  277. });
  278. }
  279. } else if (type === 'chat:title') {
  280. currentChatPage.set(1);
  281. await chats.set(await getChatList(localStorage.token, $currentChatPage));
  282. } else if (type === 'chat:tags') {
  283. allTags.set(await getAllTags(localStorage.token));
  284. }
  285. }
  286. };
  287. const onMessageHandler = async (event: {
  288. origin: string;
  289. data: { type: string; text: string };
  290. }) => {
  291. if (event.origin !== window.origin) {
  292. return;
  293. }
  294. // Replace with your iframe's origin
  295. if (event.data.type === 'input:prompt') {
  296. console.debug(event.data.text);
  297. const inputElement = document.getElementById('chat-input');
  298. if (inputElement) {
  299. prompt = event.data.text;
  300. inputElement.focus();
  301. }
  302. }
  303. if (event.data.type === 'action:submit') {
  304. console.debug(event.data.text);
  305. if (prompt !== '') {
  306. await tick();
  307. submitPrompt(prompt);
  308. }
  309. }
  310. if (event.data.type === 'input:prompt:submit') {
  311. console.debug(event.data.text);
  312. if (prompt !== '') {
  313. await tick();
  314. submitPrompt(event.data.text);
  315. }
  316. }
  317. };
  318. onMount(async () => {
  319. console.log('mounted');
  320. window.addEventListener('message', onMessageHandler);
  321. $socket?.on('chat-events', chatEventHandler);
  322. if (!$chatId) {
  323. chatIdUnsubscriber = chatId.subscribe(async (value) => {
  324. if (!value) {
  325. await initNewChat();
  326. }
  327. });
  328. } else {
  329. if ($temporaryChatEnabled) {
  330. await goto('/');
  331. }
  332. }
  333. showControls.subscribe(async (value) => {
  334. if (controlPane && !$mobile) {
  335. try {
  336. if (value) {
  337. controlPaneComponent.openPane();
  338. } else {
  339. controlPane.collapse();
  340. }
  341. } catch (e) {
  342. // ignore
  343. }
  344. }
  345. if (!value) {
  346. showCallOverlay.set(false);
  347. showOverview.set(false);
  348. showArtifacts.set(false);
  349. }
  350. });
  351. const chatInput = document.getElementById('chat-input');
  352. chatInput?.focus();
  353. chats.subscribe(() => {});
  354. });
  355. onDestroy(() => {
  356. chatIdUnsubscriber?.();
  357. window.removeEventListener('message', onMessageHandler);
  358. $socket?.off('chat-events');
  359. });
  360. // File upload functions
  361. const uploadGoogleDriveFile = async (fileData) => {
  362. console.log('Starting uploadGoogleDriveFile with:', {
  363. id: fileData.id,
  364. name: fileData.name,
  365. url: fileData.url,
  366. headers: {
  367. Authorization: `Bearer ${token}`
  368. }
  369. });
  370. // Validate input
  371. if (!fileData?.id || !fileData?.name || !fileData?.url || !fileData?.headers?.Authorization) {
  372. throw new Error('Invalid file data provided');
  373. }
  374. const tempItemId = uuidv4();
  375. const fileItem = {
  376. type: 'file',
  377. file: '',
  378. id: null,
  379. url: fileData.url,
  380. name: fileData.name,
  381. collection_name: '',
  382. status: 'uploading',
  383. error: '',
  384. itemId: tempItemId,
  385. size: 0
  386. };
  387. try {
  388. files = [...files, fileItem];
  389. console.log('Processing web file with URL:', fileData.url);
  390. // Configure fetch options with proper headers
  391. const fetchOptions = {
  392. headers: {
  393. Authorization: fileData.headers.Authorization,
  394. Accept: '*/*'
  395. },
  396. method: 'GET'
  397. };
  398. // Attempt to fetch the file
  399. console.log('Fetching file content from Google Drive...');
  400. const fileResponse = await fetch(fileData.url, fetchOptions);
  401. if (!fileResponse.ok) {
  402. const errorText = await fileResponse.text();
  403. throw new Error(`Failed to fetch file (${fileResponse.status}): ${errorText}`);
  404. }
  405. // Get content type from response
  406. const contentType = fileResponse.headers.get('content-type') || 'application/octet-stream';
  407. console.log('Response received with content-type:', contentType);
  408. // Convert response to blob
  409. console.log('Converting response to blob...');
  410. const fileBlob = await fileResponse.blob();
  411. if (fileBlob.size === 0) {
  412. throw new Error('Retrieved file is empty');
  413. }
  414. console.log('Blob created:', {
  415. size: fileBlob.size,
  416. type: fileBlob.type || contentType
  417. });
  418. // Create File object with proper MIME type
  419. const file = new File([fileBlob], fileData.name, {
  420. type: fileBlob.type || contentType
  421. });
  422. console.log('File object created:', {
  423. name: file.name,
  424. size: file.size,
  425. type: file.type
  426. });
  427. if (file.size === 0) {
  428. throw new Error('Created file is empty');
  429. }
  430. // Upload file to server
  431. console.log('Uploading file to server...');
  432. const uploadedFile = await uploadFile(localStorage.token, file);
  433. if (!uploadedFile) {
  434. throw new Error('Server returned null response for file upload');
  435. }
  436. console.log('File uploaded successfully:', uploadedFile);
  437. // Update file item with upload results
  438. fileItem.status = 'uploaded';
  439. fileItem.file = uploadedFile;
  440. fileItem.id = uploadedFile.id;
  441. fileItem.size = file.size;
  442. fileItem.collection_name = uploadedFile?.meta?.collection_name;
  443. fileItem.url = `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`;
  444. files = files;
  445. toast.success($i18n.t('File uploaded successfully'));
  446. } catch (e) {
  447. console.error('Error uploading file:', e);
  448. files = files.filter((f) => f.itemId !== tempItemId);
  449. toast.error(
  450. $i18n.t('Error uploading file: {{error}}', {
  451. error: e.message || 'Unknown error'
  452. })
  453. );
  454. }
  455. };
  456. const uploadWeb = async (url) => {
  457. console.log(url);
  458. const fileItem = {
  459. type: 'doc',
  460. name: url,
  461. collection_name: '',
  462. status: 'uploading',
  463. url: url,
  464. error: ''
  465. };
  466. try {
  467. files = [...files, fileItem];
  468. const res = await processWeb(localStorage.token, '', url);
  469. if (res) {
  470. fileItem.status = 'uploaded';
  471. fileItem.collection_name = res.collection_name;
  472. fileItem.file = {
  473. ...res.file,
  474. ...fileItem.file
  475. };
  476. files = files;
  477. }
  478. } catch (e) {
  479. // Remove the failed doc from the files array
  480. files = files.filter((f) => f.name !== url);
  481. toast.error(JSON.stringify(e));
  482. }
  483. };
  484. const uploadYoutubeTranscription = async (url) => {
  485. console.log(url);
  486. const fileItem = {
  487. type: 'doc',
  488. name: url,
  489. collection_name: '',
  490. status: 'uploading',
  491. context: 'full',
  492. url: url,
  493. error: ''
  494. };
  495. try {
  496. files = [...files, fileItem];
  497. const res = await processYoutubeVideo(localStorage.token, url);
  498. if (res) {
  499. fileItem.status = 'uploaded';
  500. fileItem.collection_name = res.collection_name;
  501. fileItem.file = {
  502. ...res.file,
  503. ...fileItem.file
  504. };
  505. files = files;
  506. }
  507. } catch (e) {
  508. // Remove the failed doc from the files array
  509. files = files.filter((f) => f.name !== url);
  510. toast.error(e);
  511. }
  512. };
  513. //////////////////////////
  514. // Web functions
  515. //////////////////////////
  516. const initNewChat = async () => {
  517. if ($page.url.searchParams.get('models')) {
  518. selectedModels = $page.url.searchParams.get('models')?.split(',');
  519. } else if ($page.url.searchParams.get('model')) {
  520. const urlModels = $page.url.searchParams.get('model')?.split(',');
  521. if (urlModels.length === 1) {
  522. const m = $models.find((m) => m.id === urlModels[0]);
  523. if (!m) {
  524. const modelSelectorButton = document.getElementById('model-selector-0-button');
  525. if (modelSelectorButton) {
  526. modelSelectorButton.click();
  527. await tick();
  528. const modelSelectorInput = document.getElementById('model-search-input');
  529. if (modelSelectorInput) {
  530. modelSelectorInput.focus();
  531. modelSelectorInput.value = urlModels[0];
  532. modelSelectorInput.dispatchEvent(new Event('input'));
  533. }
  534. }
  535. } else {
  536. selectedModels = urlModels;
  537. }
  538. } else {
  539. selectedModels = urlModels;
  540. }
  541. } else {
  542. if (sessionStorage.selectedModels) {
  543. selectedModels = JSON.parse(sessionStorage.selectedModels);
  544. sessionStorage.removeItem('selectedModels');
  545. } else {
  546. if ($settings?.models) {
  547. selectedModels = $settings?.models;
  548. } else if ($config?.default_models) {
  549. console.log($config?.default_models.split(',') ?? '');
  550. selectedModels = $config?.default_models.split(',');
  551. }
  552. }
  553. }
  554. selectedModels = selectedModels.filter((modelId) => $models.map((m) => m.id).includes(modelId));
  555. if (selectedModels.length === 0 || (selectedModels.length === 1 && selectedModels[0] === '')) {
  556. if ($models.length > 0) {
  557. selectedModels = [$models[0].id];
  558. } else {
  559. selectedModels = [''];
  560. }
  561. }
  562. await showControls.set(false);
  563. await showCallOverlay.set(false);
  564. await showOverview.set(false);
  565. await showArtifacts.set(false);
  566. if ($page.url.pathname.includes('/c/')) {
  567. window.history.replaceState(history.state, '', `/`);
  568. }
  569. autoScroll = true;
  570. await chatId.set('');
  571. await chatTitle.set('');
  572. history = {
  573. messages: {},
  574. currentId: null
  575. };
  576. chatFiles = [];
  577. params = {};
  578. if ($page.url.searchParams.get('youtube')) {
  579. uploadYoutubeTranscription(
  580. `https://www.youtube.com/watch?v=${$page.url.searchParams.get('youtube')}`
  581. );
  582. }
  583. if ($page.url.searchParams.get('web-search') === 'true') {
  584. webSearchEnabled = true;
  585. }
  586. if ($page.url.searchParams.get('tools')) {
  587. selectedToolIds = $page.url.searchParams
  588. .get('tools')
  589. ?.split(',')
  590. .map((id) => id.trim())
  591. .filter((id) => id);
  592. } else if ($page.url.searchParams.get('tool-ids')) {
  593. selectedToolIds = $page.url.searchParams
  594. .get('tool-ids')
  595. ?.split(',')
  596. .map((id) => id.trim())
  597. .filter((id) => id);
  598. }
  599. if ($page.url.searchParams.get('call') === 'true') {
  600. showCallOverlay.set(true);
  601. showControls.set(true);
  602. }
  603. if ($page.url.searchParams.get('q')) {
  604. prompt = $page.url.searchParams.get('q') ?? '';
  605. if (prompt) {
  606. await tick();
  607. submitPrompt(prompt);
  608. }
  609. }
  610. selectedModels = selectedModels.map((modelId) =>
  611. $models.map((m) => m.id).includes(modelId) ? modelId : ''
  612. );
  613. const userSettings = await getUserSettings(localStorage.token);
  614. if (userSettings) {
  615. settings.set(userSettings.ui);
  616. } else {
  617. settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
  618. }
  619. const chatInput = document.getElementById('chat-input');
  620. setTimeout(() => chatInput?.focus(), 0);
  621. };
  622. const loadChat = async () => {
  623. chatId.set(chatIdProp);
  624. chat = await getChatById(localStorage.token, $chatId).catch(async (error) => {
  625. await goto('/');
  626. return null;
  627. });
  628. if (chat) {
  629. tags = await getTagsById(localStorage.token, $chatId).catch(async (error) => {
  630. return [];
  631. });
  632. const chatContent = chat.chat;
  633. if (chatContent) {
  634. console.log(chatContent);
  635. selectedModels =
  636. (chatContent?.models ?? undefined) !== undefined
  637. ? chatContent.models
  638. : [chatContent.models ?? ''];
  639. history =
  640. (chatContent?.history ?? undefined) !== undefined
  641. ? chatContent.history
  642. : convertMessagesToHistory(chatContent.messages);
  643. chatTitle.set(chatContent.title);
  644. const userSettings = await getUserSettings(localStorage.token);
  645. if (userSettings) {
  646. await settings.set(userSettings.ui);
  647. } else {
  648. await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
  649. }
  650. params = chatContent?.params ?? {};
  651. chatFiles = chatContent?.files ?? [];
  652. autoScroll = true;
  653. await tick();
  654. if (history.currentId) {
  655. history.messages[history.currentId].done = true;
  656. }
  657. await tick();
  658. return true;
  659. } else {
  660. return null;
  661. }
  662. }
  663. };
  664. const scrollToBottom = async () => {
  665. await tick();
  666. if (messagesContainerElement) {
  667. messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
  668. }
  669. };
  670. const createMessagesList = (responseMessageId) => {
  671. if (responseMessageId === null) {
  672. return [];
  673. }
  674. const message = history.messages[responseMessageId];
  675. if (message?.parentId) {
  676. return [...createMessagesList(message.parentId), message];
  677. } else {
  678. return [message];
  679. }
  680. };
  681. const chatCompletedHandler = async (chatId, modelId, responseMessageId, messages) => {
  682. await mermaid.run({
  683. querySelector: '.mermaid'
  684. });
  685. const res = await chatCompleted(localStorage.token, {
  686. model: modelId,
  687. messages: messages.map((m) => ({
  688. id: m.id,
  689. role: m.role,
  690. content: m.content,
  691. info: m.info ? m.info : undefined,
  692. timestamp: m.timestamp,
  693. ...(m.sources ? { sources: m.sources } : {})
  694. })),
  695. chat_id: chatId,
  696. session_id: $socket?.id,
  697. id: responseMessageId
  698. }).catch((error) => {
  699. toast.error(error);
  700. messages.at(-1).error = { content: error };
  701. return null;
  702. });
  703. if (res !== null) {
  704. // Update chat history with the new messages
  705. for (const message of res.messages) {
  706. history.messages[message.id] = {
  707. ...history.messages[message.id],
  708. ...(history.messages[message.id].content !== message.content
  709. ? { originalContent: history.messages[message.id].content }
  710. : {}),
  711. ...message
  712. };
  713. }
  714. }
  715. await tick();
  716. if ($chatId == chatId) {
  717. if (!$temporaryChatEnabled) {
  718. chat = await updateChatById(localStorage.token, chatId, {
  719. models: selectedModels,
  720. messages: messages,
  721. history: history,
  722. params: params,
  723. files: chatFiles
  724. });
  725. currentChatPage.set(1);
  726. await chats.set(await getChatList(localStorage.token, $currentChatPage));
  727. }
  728. }
  729. };
  730. const chatActionHandler = async (chatId, actionId, modelId, responseMessageId, event = null) => {
  731. const messages = createMessagesList(responseMessageId);
  732. const res = await chatAction(localStorage.token, actionId, {
  733. model: modelId,
  734. messages: messages.map((m) => ({
  735. id: m.id,
  736. role: m.role,
  737. content: m.content,
  738. info: m.info ? m.info : undefined,
  739. timestamp: m.timestamp,
  740. ...(m.sources ? { sources: m.sources } : {})
  741. })),
  742. ...(event ? { event: event } : {}),
  743. chat_id: chatId,
  744. session_id: $socket?.id,
  745. id: responseMessageId
  746. }).catch((error) => {
  747. toast.error(error);
  748. messages.at(-1).error = { content: error };
  749. return null;
  750. });
  751. if (res !== null) {
  752. // Update chat history with the new messages
  753. for (const message of res.messages) {
  754. history.messages[message.id] = {
  755. ...history.messages[message.id],
  756. ...(history.messages[message.id].content !== message.content
  757. ? { originalContent: history.messages[message.id].content }
  758. : {}),
  759. ...message
  760. };
  761. }
  762. }
  763. if ($chatId == chatId) {
  764. if (!$temporaryChatEnabled) {
  765. chat = await updateChatById(localStorage.token, chatId, {
  766. models: selectedModels,
  767. messages: messages,
  768. history: history,
  769. params: params,
  770. files: chatFiles
  771. });
  772. currentChatPage.set(1);
  773. await chats.set(await getChatList(localStorage.token, $currentChatPage));
  774. }
  775. }
  776. };
  777. const getChatEventEmitter = async (modelId: string, chatId: string = '') => {
  778. return setInterval(() => {
  779. $socket?.emit('usage', {
  780. action: 'chat',
  781. model: modelId,
  782. chat_id: chatId
  783. });
  784. }, 1000);
  785. };
  786. const createMessagePair = async (userPrompt) => {
  787. prompt = '';
  788. if (selectedModels.length === 0) {
  789. toast.error($i18n.t('Model not selected'));
  790. } else {
  791. const modelId = selectedModels[0];
  792. const model = $models.filter((m) => m.id === modelId).at(0);
  793. const messages = createMessagesList(history.currentId);
  794. const parentMessage = messages.length !== 0 ? messages.at(-1) : null;
  795. const userMessageId = uuidv4();
  796. const responseMessageId = uuidv4();
  797. const userMessage = {
  798. id: userMessageId,
  799. parentId: parentMessage ? parentMessage.id : null,
  800. childrenIds: [responseMessageId],
  801. role: 'user',
  802. content: userPrompt ? userPrompt : `[PROMPT] ${userMessageId}`,
  803. timestamp: Math.floor(Date.now() / 1000)
  804. };
  805. const responseMessage = {
  806. id: responseMessageId,
  807. parentId: userMessageId,
  808. childrenIds: [],
  809. role: 'assistant',
  810. content: `[RESPONSE] ${responseMessageId}`,
  811. done: true,
  812. model: modelId,
  813. modelName: model.name ?? model.id,
  814. modelIdx: 0,
  815. timestamp: Math.floor(Date.now() / 1000)
  816. };
  817. if (parentMessage) {
  818. parentMessage.childrenIds.push(userMessageId);
  819. history.messages[parentMessage.id] = parentMessage;
  820. }
  821. history.messages[userMessageId] = userMessage;
  822. history.messages[responseMessageId] = responseMessage;
  823. history.currentId = responseMessageId;
  824. await tick();
  825. if (autoScroll) {
  826. scrollToBottom();
  827. }
  828. if (messages.length === 0) {
  829. await initChatHandler();
  830. } else {
  831. await saveChatHandler($chatId);
  832. }
  833. }
  834. };
  835. const chatCompletionEventHandler = async (data, message, chatId) => {
  836. const { id, done, choices, sources, selectedModelId, error, usage } = data;
  837. if (error) {
  838. await handleOpenAIError(error, message);
  839. }
  840. if (sources) {
  841. message.sources = sources;
  842. // Only remove status if it was initially set
  843. if (model?.info?.meta?.knowledge ?? false) {
  844. message.statusHistory = message.statusHistory.filter(
  845. (status) => status.action !== 'knowledge_search'
  846. );
  847. }
  848. }
  849. if (choices) {
  850. const value = choices[0]?.delta?.content ?? '';
  851. if (message.content == '' && value == '\n') {
  852. console.log('Empty response');
  853. } else {
  854. message.content += value;
  855. if (navigator.vibrate && ($settings?.hapticFeedback ?? false)) {
  856. navigator.vibrate(5);
  857. }
  858. // Emit chat event for TTS
  859. const messageContentParts = getMessageContentParts(
  860. message.content,
  861. $config?.audio?.tts?.split_on ?? 'punctuation'
  862. );
  863. messageContentParts.pop();
  864. // dispatch only last sentence and make sure it hasn't been dispatched before
  865. if (
  866. messageContentParts.length > 0 &&
  867. messageContentParts[messageContentParts.length - 1] !== message.lastSentence
  868. ) {
  869. message.lastSentence = messageContentParts[messageContentParts.length - 1];
  870. eventTarget.dispatchEvent(
  871. new CustomEvent('chat', {
  872. detail: {
  873. id: message.id,
  874. content: messageContentParts[messageContentParts.length - 1]
  875. }
  876. })
  877. );
  878. }
  879. }
  880. }
  881. if (selectedModelId) {
  882. message.selectedModelId = selectedModelId;
  883. message.arena = true;
  884. }
  885. if (usage) {
  886. message.usage = usage;
  887. }
  888. if (done) {
  889. message.done = true;
  890. if ($settings.notificationEnabled && !document.hasFocus()) {
  891. new Notification(`${message.model}`, {
  892. body: message.content,
  893. icon: `${WEBUI_BASE_URL}/static/favicon.png`
  894. });
  895. }
  896. if ($settings.responseAutoCopy) {
  897. copyToClipboard(message.content);
  898. }
  899. if ($settings.responseAutoPlayback && !$showCallOverlay) {
  900. await tick();
  901. document.getElementById(`speak-button-${message.id}`)?.click();
  902. }
  903. // Emit chat event for TTS
  904. let lastMessageContentPart =
  905. getMessageContentParts(message.content, $config?.audio?.tts?.split_on ?? 'punctuation')?.at(
  906. -1
  907. ) ?? '';
  908. if (lastMessageContentPart) {
  909. eventTarget.dispatchEvent(
  910. new CustomEvent('chat', {
  911. detail: { id: message.id, content: lastMessageContentPart }
  912. })
  913. );
  914. }
  915. eventTarget.dispatchEvent(
  916. new CustomEvent('chat:finish', {
  917. detail: {
  918. id: message.id,
  919. content: message.content
  920. }
  921. })
  922. );
  923. history.messages[message.id] = message;
  924. await chatCompletedHandler(chatId, message.model, message.id, createMessagesList(message.id));
  925. }
  926. history.messages[message.id] = message;
  927. console.log(data);
  928. if (autoScroll) {
  929. scrollToBottom();
  930. }
  931. };
  932. //////////////////////////
  933. // Chat functions
  934. //////////////////////////
  935. const submitPrompt = async (userPrompt, { _raw = false } = {}) => {
  936. console.log('submitPrompt', userPrompt, $chatId);
  937. const messages = createMessagesList(history.currentId);
  938. const _selectedModels = selectedModels.map((modelId) =>
  939. $models.map((m) => m.id).includes(modelId) ? modelId : ''
  940. );
  941. if (JSON.stringify(selectedModels) !== JSON.stringify(_selectedModels)) {
  942. selectedModels = _selectedModels;
  943. }
  944. if (userPrompt === '') {
  945. toast.error($i18n.t('Please enter a prompt'));
  946. return;
  947. }
  948. if (selectedModels.includes('')) {
  949. toast.error($i18n.t('Model not selected'));
  950. return;
  951. }
  952. if (messages.length != 0 && messages.at(-1).done != true) {
  953. // Response not done
  954. return;
  955. }
  956. if (messages.length != 0 && messages.at(-1).error) {
  957. // Error in response
  958. toast.error($i18n.t(`Oops! There was an error in the previous response.`));
  959. return;
  960. }
  961. if (
  962. files.length > 0 &&
  963. files.filter((file) => file.type !== 'image' && file.status === 'uploading').length > 0
  964. ) {
  965. toast.error(
  966. $i18n.t(`Oops! There are files still uploading. Please wait for the upload to complete.`)
  967. );
  968. return;
  969. }
  970. if (
  971. ($config?.file?.max_count ?? null) !== null &&
  972. files.length + chatFiles.length > $config?.file?.max_count
  973. ) {
  974. toast.error(
  975. $i18n.t(`You can only chat with a maximum of {{maxCount}} file(s) at a time.`, {
  976. maxCount: $config?.file?.max_count
  977. })
  978. );
  979. return;
  980. }
  981. prompt = '';
  982. await tick();
  983. // Reset chat input textarea
  984. const chatInputElement = document.getElementById('chat-input');
  985. if (chatInputElement) {
  986. chatInputElement.style.height = '';
  987. }
  988. const _files = JSON.parse(JSON.stringify(files));
  989. chatFiles.push(..._files.filter((item) => ['doc', 'file', 'collection'].includes(item.type)));
  990. chatFiles = chatFiles.filter(
  991. // Remove duplicates
  992. (item, index, array) =>
  993. array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
  994. );
  995. files = [];
  996. prompt = '';
  997. // Create user message
  998. let userMessageId = uuidv4();
  999. let userMessage = {
  1000. id: userMessageId,
  1001. parentId: messages.length !== 0 ? messages.at(-1).id : null,
  1002. childrenIds: [],
  1003. role: 'user',
  1004. content: userPrompt,
  1005. files: _files.length > 0 ? _files : undefined,
  1006. timestamp: Math.floor(Date.now() / 1000), // Unix epoch
  1007. models: selectedModels
  1008. };
  1009. // Add message to history and Set currentId to messageId
  1010. history.messages[userMessageId] = userMessage;
  1011. history.currentId = userMessageId;
  1012. // Append messageId to childrenIds of parent message
  1013. if (messages.length !== 0) {
  1014. history.messages[messages.at(-1).id].childrenIds.push(userMessageId);
  1015. }
  1016. // Wait until history/message have been updated
  1017. await tick();
  1018. // focus on chat input
  1019. const chatInput = document.getElementById('chat-input');
  1020. chatInput?.focus();
  1021. saveSessionSelectedModels();
  1022. await sendPrompt(userPrompt, userMessageId, { newChat: true });
  1023. };
  1024. const sendPrompt = async (
  1025. prompt: string,
  1026. parentId: string,
  1027. { modelId = null, modelIdx = null, newChat = false } = {}
  1028. ) => {
  1029. // Create new chat if newChat is true and first user message
  1030. if (
  1031. newChat &&
  1032. history.messages[history.currentId].parentId === null &&
  1033. history.messages[history.currentId].role === 'user'
  1034. ) {
  1035. await initChatHandler();
  1036. } else {
  1037. await saveChatHandler($chatId);
  1038. }
  1039. // If modelId is provided, use it, else use selected model
  1040. let selectedModelIds = modelId
  1041. ? [modelId]
  1042. : atSelectedModel !== undefined
  1043. ? [atSelectedModel.id]
  1044. : selectedModels;
  1045. // Create response messages for each selected model
  1046. const responseMessageIds: Record<PropertyKey, string> = {};
  1047. for (const [_modelIdx, modelId] of selectedModelIds.entries()) {
  1048. const model = $models.filter((m) => m.id === modelId).at(0);
  1049. if (model) {
  1050. let responseMessageId = uuidv4();
  1051. let responseMessage = {
  1052. parentId: parentId,
  1053. id: responseMessageId,
  1054. childrenIds: [],
  1055. role: 'assistant',
  1056. content: '',
  1057. model: model.id,
  1058. modelName: model.name ?? model.id,
  1059. modelIdx: modelIdx ? modelIdx : _modelIdx,
  1060. userContext: null,
  1061. timestamp: Math.floor(Date.now() / 1000) // Unix epoch
  1062. };
  1063. // Add message to history and Set currentId to messageId
  1064. history.messages[responseMessageId] = responseMessage;
  1065. history.currentId = responseMessageId;
  1066. // Append messageId to childrenIds of parent message
  1067. if (parentId !== null) {
  1068. history.messages[parentId].childrenIds = [
  1069. ...history.messages[parentId].childrenIds,
  1070. responseMessageId
  1071. ];
  1072. }
  1073. responseMessageIds[`${modelId}-${modelIdx ? modelIdx : _modelIdx}`] = responseMessageId;
  1074. }
  1075. }
  1076. await tick();
  1077. // Save chat after all messages have been created
  1078. await saveChatHandler($chatId);
  1079. const _chatId = JSON.parse(JSON.stringify($chatId));
  1080. await Promise.all(
  1081. selectedModelIds.map(async (modelId, _modelIdx) => {
  1082. console.log('modelId', modelId);
  1083. const model = $models.filter((m) => m.id === modelId).at(0);
  1084. if (model) {
  1085. const messages = createMessagesList(parentId);
  1086. // If there are image files, check if model is vision capable
  1087. const hasImages = messages.some((message) =>
  1088. message.files?.some((file) => file.type === 'image')
  1089. );
  1090. if (hasImages && !(model.info?.meta?.capabilities?.vision ?? true)) {
  1091. toast.error(
  1092. $i18n.t('Model {{modelName}} is not vision capable', {
  1093. modelName: model.name ?? model.id
  1094. })
  1095. );
  1096. }
  1097. let responseMessageId =
  1098. responseMessageIds[`${modelId}-${modelIdx ? modelIdx : _modelIdx}`];
  1099. let responseMessage = history.messages[responseMessageId];
  1100. let userContext = null;
  1101. if ($settings?.memory ?? false) {
  1102. if (userContext === null) {
  1103. const res = await queryMemory(localStorage.token, prompt).catch((error) => {
  1104. toast.error(error);
  1105. return null;
  1106. });
  1107. if (res) {
  1108. if (res.documents[0].length > 0) {
  1109. userContext = res.documents[0].reduce((acc, doc, index) => {
  1110. const createdAtTimestamp = res.metadatas[0][index].created_at;
  1111. const createdAtDate = new Date(createdAtTimestamp * 1000)
  1112. .toISOString()
  1113. .split('T')[0];
  1114. return `${acc}${index + 1}. [${createdAtDate}]. ${doc}\n`;
  1115. }, '');
  1116. }
  1117. console.log(userContext);
  1118. }
  1119. }
  1120. }
  1121. responseMessage.userContext = userContext;
  1122. const chatEventEmitter = await getChatEventEmitter(model.id, _chatId);
  1123. scrollToBottom();
  1124. if (webSearchEnabled) {
  1125. await getWebSearchResults(model.id, parentId, responseMessageId);
  1126. }
  1127. await sendPromptSocket(model, responseMessageId, _chatId);
  1128. if (chatEventEmitter) clearInterval(chatEventEmitter);
  1129. } else {
  1130. toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
  1131. }
  1132. })
  1133. );
  1134. currentChatPage.set(1);
  1135. chats.set(await getChatList(localStorage.token, $currentChatPage));
  1136. };
  1137. const sendPromptSocket = async (model, responseMessageId, _chatId) => {
  1138. const responseMessage = history.messages[responseMessageId];
  1139. const userMessage = history.messages[responseMessage.parentId];
  1140. let files = JSON.parse(JSON.stringify(chatFiles));
  1141. if (model?.info?.meta?.knowledge ?? false) {
  1142. // Only initialize and add status if knowledge exists
  1143. responseMessage.statusHistory = [
  1144. {
  1145. action: 'knowledge_search',
  1146. description: $i18n.t(`Searching Knowledge for "{{searchQuery}}"`, {
  1147. searchQuery: userMessage.content
  1148. }),
  1149. done: false
  1150. }
  1151. ];
  1152. files.push(
  1153. ...model.info.meta.knowledge.map((item) => {
  1154. if (item?.collection_name) {
  1155. return {
  1156. id: item.collection_name,
  1157. name: item.name,
  1158. legacy: true
  1159. };
  1160. } else if (item?.collection_names) {
  1161. return {
  1162. name: item.name,
  1163. type: 'collection',
  1164. collection_names: item.collection_names,
  1165. legacy: true
  1166. };
  1167. } else {
  1168. return item;
  1169. }
  1170. })
  1171. );
  1172. history.messages[responseMessageId] = responseMessage;
  1173. }
  1174. files.push(
  1175. ...(userMessage?.files ?? []).filter((item) =>
  1176. ['doc', 'file', 'collection'].includes(item.type)
  1177. ),
  1178. ...(responseMessage?.files ?? []).filter((item) => ['web_search_results'].includes(item.type))
  1179. );
  1180. // Remove duplicates
  1181. files = files.filter(
  1182. (item, index, array) =>
  1183. array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
  1184. );
  1185. scrollToBottom();
  1186. eventTarget.dispatchEvent(
  1187. new CustomEvent('chat:start', {
  1188. detail: {
  1189. id: responseMessageId
  1190. }
  1191. })
  1192. );
  1193. await tick();
  1194. const stream =
  1195. model?.info?.params?.stream_response ??
  1196. $settings?.params?.stream_response ??
  1197. params?.stream_response ??
  1198. true;
  1199. const messages = [
  1200. params?.system || $settings.system || (responseMessage?.userContext ?? null)
  1201. ? {
  1202. role: 'system',
  1203. content: `${promptTemplate(
  1204. params?.system ?? $settings?.system ?? '',
  1205. $user.name,
  1206. $settings?.userLocation
  1207. ? await getAndUpdateUserLocation(localStorage.token)
  1208. : undefined
  1209. )}${
  1210. (responseMessage?.userContext ?? null)
  1211. ? `\n\nUser Context:\n${responseMessage?.userContext ?? ''}`
  1212. : ''
  1213. }`
  1214. }
  1215. : undefined,
  1216. ...createMessagesList(responseMessageId)
  1217. ]
  1218. .filter((message) => message?.content?.trim())
  1219. .map((message, idx, arr) => ({
  1220. role: message.role,
  1221. ...((message.files?.filter((file) => file.type === 'image').length > 0 ?? false) &&
  1222. message.role === 'user'
  1223. ? {
  1224. content: [
  1225. {
  1226. type: 'text',
  1227. text: message?.merged?.content ?? message.content
  1228. },
  1229. ...message.files
  1230. .filter((file) => file.type === 'image')
  1231. .map((file) => ({
  1232. type: 'image_url',
  1233. image_url: {
  1234. url: file.url
  1235. }
  1236. }))
  1237. ]
  1238. }
  1239. : {
  1240. content: message?.merged?.content ?? message.content
  1241. })
  1242. }));
  1243. const res = await generateOpenAIChatCompletion(
  1244. localStorage.token,
  1245. {
  1246. stream: stream,
  1247. model: model.id,
  1248. messages: messages,
  1249. params: {
  1250. ...$settings?.params,
  1251. ...params,
  1252. format: $settings.requestFormat ?? undefined,
  1253. keep_alive: $settings.keepAlive ?? undefined,
  1254. stop:
  1255. (params?.stop ?? $settings?.params?.stop ?? undefined)
  1256. ? (params?.stop.split(',').map((token) => token.trim()) ?? $settings.params.stop).map(
  1257. (str) => decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
  1258. )
  1259. : undefined
  1260. },
  1261. tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
  1262. files: files.length > 0 ? files : undefined,
  1263. session_id: $socket?.id,
  1264. chat_id: $chatId,
  1265. id: responseMessageId,
  1266. ...(!$temporaryChatEnabled && messages.length == 1 && selectedModels[0] === model.id
  1267. ? {
  1268. background_tasks: {
  1269. title_generation: $settings?.title?.auto ?? true,
  1270. tags_generation: $settings?.autoTags ?? true
  1271. }
  1272. }
  1273. : {}),
  1274. ...(stream && (model.info?.meta?.capabilities?.usage ?? false)
  1275. ? {
  1276. stream_options: {
  1277. include_usage: true
  1278. }
  1279. }
  1280. : {})
  1281. },
  1282. `${WEBUI_BASE_URL}/api`
  1283. ).catch((error) => {
  1284. responseMessage.error = {
  1285. content: error
  1286. };
  1287. responseMessage.done = true;
  1288. return null;
  1289. });
  1290. console.log(res);
  1291. if (res) {
  1292. taskId = res.task_id;
  1293. }
  1294. await tick();
  1295. scrollToBottom();
  1296. };
  1297. const handleOpenAIError = async (error, responseMessage) => {
  1298. let errorMessage = '';
  1299. let innerError;
  1300. if (error) {
  1301. innerError = error;
  1302. }
  1303. console.error(innerError);
  1304. if ('detail' in innerError) {
  1305. toast.error(innerError.detail);
  1306. errorMessage = innerError.detail;
  1307. } else if ('error' in innerError) {
  1308. if ('message' in innerError.error) {
  1309. toast.error(innerError.error.message);
  1310. errorMessage = innerError.error.message;
  1311. } else {
  1312. toast.error(innerError.error);
  1313. errorMessage = innerError.error;
  1314. }
  1315. } else if ('message' in innerError) {
  1316. toast.error(innerError.message);
  1317. errorMessage = innerError.message;
  1318. }
  1319. responseMessage.error = {
  1320. content: $i18n.t(`Uh-oh! There was an issue with the response.`) + '\n' + errorMessage
  1321. };
  1322. responseMessage.done = true;
  1323. if (responseMessage.statusHistory) {
  1324. responseMessage.statusHistory = responseMessage.statusHistory.filter(
  1325. (status) => status.action !== 'knowledge_search'
  1326. );
  1327. }
  1328. history.messages[responseMessage.id] = responseMessage;
  1329. };
  1330. const stopResponse = () => {
  1331. if (taskId) {
  1332. const res = stopTask(localStorage.token, taskId).catch((error) => {
  1333. return null;
  1334. });
  1335. if (res) {
  1336. taskId = null;
  1337. const responseMessage = history.messages[history.currentId];
  1338. responseMessage.done = true;
  1339. history.messages[history.currentId] = responseMessage;
  1340. if (autoScroll) {
  1341. scrollToBottom();
  1342. }
  1343. }
  1344. }
  1345. };
  1346. const submitMessage = async (parentId, prompt) => {
  1347. let userPrompt = prompt;
  1348. let userMessageId = uuidv4();
  1349. let userMessage = {
  1350. id: userMessageId,
  1351. parentId: parentId,
  1352. childrenIds: [],
  1353. role: 'user',
  1354. content: userPrompt,
  1355. models: selectedModels
  1356. };
  1357. if (parentId !== null) {
  1358. history.messages[parentId].childrenIds = [
  1359. ...history.messages[parentId].childrenIds,
  1360. userMessageId
  1361. ];
  1362. }
  1363. history.messages[userMessageId] = userMessage;
  1364. history.currentId = userMessageId;
  1365. await tick();
  1366. await sendPrompt(userPrompt, userMessageId);
  1367. };
  1368. const regenerateResponse = async (message) => {
  1369. console.log('regenerateResponse');
  1370. if (history.currentId) {
  1371. let userMessage = history.messages[message.parentId];
  1372. let userPrompt = userMessage.content;
  1373. if ((userMessage?.models ?? [...selectedModels]).length == 1) {
  1374. // If user message has only one model selected, sendPrompt automatically selects it for regeneration
  1375. await sendPrompt(userPrompt, userMessage.id);
  1376. } else {
  1377. // If there are multiple models selected, use the model of the response message for regeneration
  1378. // e.g. many model chat
  1379. await sendPrompt(userPrompt, userMessage.id, {
  1380. modelId: message.model,
  1381. modelIdx: message.modelIdx
  1382. });
  1383. }
  1384. }
  1385. };
  1386. const continueResponse = async () => {
  1387. console.log('continueResponse');
  1388. const _chatId = JSON.parse(JSON.stringify($chatId));
  1389. if (history.currentId && history.messages[history.currentId].done == true) {
  1390. const responseMessage = history.messages[history.currentId];
  1391. responseMessage.done = false;
  1392. await tick();
  1393. const model = $models
  1394. .filter((m) => m.id === (responseMessage?.selectedModelId ?? responseMessage.model))
  1395. .at(0);
  1396. if (model) {
  1397. await sendPromptSocket(model, responseMessage.id, _chatId);
  1398. }
  1399. }
  1400. };
  1401. const mergeResponses = async (messageId, responses, _chatId) => {
  1402. console.log('mergeResponses', messageId, responses);
  1403. const message = history.messages[messageId];
  1404. const mergedResponse = {
  1405. status: true,
  1406. content: ''
  1407. };
  1408. message.merged = mergedResponse;
  1409. history.messages[messageId] = message;
  1410. try {
  1411. const [res, controller] = await generateMoACompletion(
  1412. localStorage.token,
  1413. message.model,
  1414. history.messages[message.parentId].content,
  1415. responses
  1416. );
  1417. if (res && res.ok && res.body) {
  1418. const textStream = await createOpenAITextStream(res.body, $settings.splitLargeChunks);
  1419. for await (const update of textStream) {
  1420. const { value, done, sources, error, usage } = update;
  1421. if (error || done) {
  1422. break;
  1423. }
  1424. if (mergedResponse.content == '' && value == '\n') {
  1425. continue;
  1426. } else {
  1427. mergedResponse.content += value;
  1428. history.messages[messageId] = message;
  1429. }
  1430. if (autoScroll) {
  1431. scrollToBottom();
  1432. }
  1433. }
  1434. await saveChatHandler(_chatId);
  1435. } else {
  1436. console.error(res);
  1437. }
  1438. } catch (e) {
  1439. console.error(e);
  1440. }
  1441. };
  1442. const getWebSearchResults = async (
  1443. model: string,
  1444. parentId: string,
  1445. responseMessageId: string
  1446. ) => {
  1447. // TODO: move this to the backend
  1448. const responseMessage = history.messages[responseMessageId];
  1449. const userMessage = history.messages[parentId];
  1450. const messages = createMessagesList(history.currentId);
  1451. responseMessage.statusHistory = [
  1452. {
  1453. done: false,
  1454. action: 'web_search',
  1455. description: $i18n.t('Generating search query')
  1456. }
  1457. ];
  1458. history.messages[responseMessageId] = responseMessage;
  1459. const prompt = userMessage.content;
  1460. let queries = await generateQueries(
  1461. localStorage.token,
  1462. model,
  1463. messages.filter((message) => message?.content?.trim()),
  1464. prompt
  1465. ).catch((error) => {
  1466. console.log(error);
  1467. return [prompt];
  1468. });
  1469. if (queries.length === 0) {
  1470. responseMessage.statusHistory.push({
  1471. done: true,
  1472. error: true,
  1473. action: 'web_search',
  1474. description: $i18n.t('No search query generated')
  1475. });
  1476. history.messages[responseMessageId] = responseMessage;
  1477. return;
  1478. }
  1479. const searchQuery = queries[0];
  1480. responseMessage.statusHistory.push({
  1481. done: false,
  1482. action: 'web_search',
  1483. description: $i18n.t(`Searching "{{searchQuery}}"`, { searchQuery })
  1484. });
  1485. history.messages[responseMessageId] = responseMessage;
  1486. const results = await processWebSearch(localStorage.token, searchQuery).catch((error) => {
  1487. console.log(error);
  1488. toast.error(error);
  1489. return null;
  1490. });
  1491. if (results) {
  1492. responseMessage.statusHistory.push({
  1493. done: true,
  1494. action: 'web_search',
  1495. description: $i18n.t('Searched {{count}} sites', { count: results.filenames.length }),
  1496. query: searchQuery,
  1497. urls: results.filenames
  1498. });
  1499. if (responseMessage?.files ?? undefined === undefined) {
  1500. responseMessage.files = [];
  1501. }
  1502. responseMessage.files.push({
  1503. collection_name: results.collection_name,
  1504. name: searchQuery,
  1505. type: 'web_search_results',
  1506. urls: results.filenames
  1507. });
  1508. history.messages[responseMessageId] = responseMessage;
  1509. } else {
  1510. responseMessage.statusHistory.push({
  1511. done: true,
  1512. error: true,
  1513. action: 'web_search',
  1514. description: 'No search results found'
  1515. });
  1516. history.messages[responseMessageId] = responseMessage;
  1517. }
  1518. };
  1519. const initChatHandler = async () => {
  1520. if (!$temporaryChatEnabled) {
  1521. chat = await createNewChat(localStorage.token, {
  1522. id: $chatId,
  1523. title: $i18n.t('New Chat'),
  1524. models: selectedModels,
  1525. system: $settings.system ?? undefined,
  1526. params: params,
  1527. history: history,
  1528. messages: createMessagesList(history.currentId),
  1529. tags: [],
  1530. timestamp: Date.now()
  1531. });
  1532. currentChatPage.set(1);
  1533. await chats.set(await getChatList(localStorage.token, $currentChatPage));
  1534. await chatId.set(chat.id);
  1535. window.history.replaceState(history.state, '', `/c/${chat.id}`);
  1536. } else {
  1537. await chatId.set('local');
  1538. }
  1539. await tick();
  1540. };
  1541. const saveChatHandler = async (_chatId) => {
  1542. if ($chatId == _chatId) {
  1543. if (!$temporaryChatEnabled) {
  1544. chat = await updateChatById(localStorage.token, _chatId, {
  1545. models: selectedModels,
  1546. history: history,
  1547. messages: createMessagesList(history.currentId),
  1548. params: params,
  1549. files: chatFiles
  1550. });
  1551. currentChatPage.set(1);
  1552. await chats.set(await getChatList(localStorage.token, $currentChatPage));
  1553. }
  1554. }
  1555. };
  1556. </script>
  1557. <svelte:head>
  1558. <title>
  1559. {$chatTitle
  1560. ? `${$chatTitle.length > 30 ? `${$chatTitle.slice(0, 30)}...` : $chatTitle} | ${$WEBUI_NAME}`
  1561. : `${$WEBUI_NAME}`}
  1562. </title>
  1563. </svelte:head>
  1564. <audio id="audioElement" src="" style="display: none;" />
  1565. <EventConfirmDialog
  1566. bind:show={showEventConfirmation}
  1567. title={eventConfirmationTitle}
  1568. message={eventConfirmationMessage}
  1569. input={eventConfirmationInput}
  1570. inputPlaceholder={eventConfirmationInputPlaceholder}
  1571. inputValue={eventConfirmationInputValue}
  1572. on:confirm={(e) => {
  1573. if (e.detail) {
  1574. eventCallback(e.detail);
  1575. } else {
  1576. eventCallback(true);
  1577. }
  1578. }}
  1579. on:cancel={() => {
  1580. eventCallback(false);
  1581. }}
  1582. />
  1583. {#if !chatIdProp || (loaded && chatIdProp)}
  1584. <div
  1585. class="h-screen max-h-[100dvh] {$showSidebar
  1586. ? 'md:max-w-[calc(100%-260px)]'
  1587. : ''} w-full max-w-full flex flex-col"
  1588. id="chat-container"
  1589. >
  1590. {#if $settings?.backgroundImageUrl ?? null}
  1591. <div
  1592. class="absolute {$showSidebar
  1593. ? 'md:max-w-[calc(100%-260px)] md:translate-x-[260px]'
  1594. : ''} top-0 left-0 w-full h-full bg-cover bg-center bg-no-repeat"
  1595. style="background-image: url({$settings.backgroundImageUrl}) "
  1596. />
  1597. <div
  1598. class="absolute top-0 left-0 w-full h-full bg-gradient-to-t from-white to-white/85 dark:from-gray-900 dark:to-[#171717]/90 z-0"
  1599. />
  1600. {/if}
  1601. <Navbar
  1602. bind:this={navbarElement}
  1603. chat={{
  1604. id: $chatId,
  1605. chat: {
  1606. title: $chatTitle,
  1607. models: selectedModels,
  1608. system: $settings.system ?? undefined,
  1609. params: params,
  1610. history: history,
  1611. timestamp: Date.now()
  1612. }
  1613. }}
  1614. title={$chatTitle}
  1615. bind:selectedModels
  1616. shareEnabled={!!history.currentId}
  1617. {initNewChat}
  1618. />
  1619. <PaneGroup direction="horizontal" class="w-full h-full">
  1620. <Pane defaultSize={50} class="h-full flex w-full relative">
  1621. {#if $banners.length > 0 && !history.currentId && !$chatId && selectedModels.length <= 1}
  1622. <div class="absolute top-12 left-0 right-0 w-full z-30">
  1623. <div class=" flex flex-col gap-1 w-full">
  1624. {#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner}
  1625. <Banner
  1626. {banner}
  1627. on:dismiss={(e) => {
  1628. const bannerId = e.detail;
  1629. localStorage.setItem(
  1630. 'dismissedBannerIds',
  1631. JSON.stringify(
  1632. [
  1633. bannerId,
  1634. ...JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]')
  1635. ].filter((id) => $banners.find((b) => b.id === id))
  1636. )
  1637. );
  1638. }}
  1639. />
  1640. {/each}
  1641. </div>
  1642. </div>
  1643. {/if}
  1644. <div class="flex flex-col flex-auto z-10 w-full">
  1645. {#if $settings?.landingPageMode === 'chat' || createMessagesList(history.currentId).length > 0}
  1646. <div
  1647. class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0 max-w-full z-10 scrollbar-hidden"
  1648. id="messages-container"
  1649. bind:this={messagesContainerElement}
  1650. on:scroll={(e) => {
  1651. autoScroll =
  1652. messagesContainerElement.scrollHeight - messagesContainerElement.scrollTop <=
  1653. messagesContainerElement.clientHeight + 5;
  1654. }}
  1655. >
  1656. <div class=" h-full w-full flex flex-col">
  1657. <Messages
  1658. chatId={$chatId}
  1659. bind:history
  1660. bind:autoScroll
  1661. bind:prompt
  1662. {selectedModels}
  1663. {sendPrompt}
  1664. {showMessage}
  1665. {submitMessage}
  1666. {continueResponse}
  1667. {regenerateResponse}
  1668. {mergeResponses}
  1669. {chatActionHandler}
  1670. bottomPadding={files.length > 0}
  1671. />
  1672. </div>
  1673. </div>
  1674. <div class=" pb-[1rem]">
  1675. <MessageInput
  1676. {history}
  1677. {selectedModels}
  1678. bind:files
  1679. bind:prompt
  1680. bind:autoScroll
  1681. bind:selectedToolIds
  1682. bind:webSearchEnabled
  1683. bind:atSelectedModel
  1684. transparentBackground={$settings?.backgroundImageUrl ?? false}
  1685. {stopResponse}
  1686. {createMessagePair}
  1687. on:upload={async (e) => {
  1688. const { type, data } = e.detail;
  1689. if (type === 'web') {
  1690. await uploadWeb(data);
  1691. } else if (type === 'youtube') {
  1692. await uploadYoutubeTranscription(data);
  1693. } else if (type === 'google-drive') {
  1694. await uploadGoogleDriveFile(data);
  1695. }
  1696. }}
  1697. on:submit={async (e) => {
  1698. if (e.detail) {
  1699. await tick();
  1700. submitPrompt(
  1701. ($settings?.richTextInput ?? true)
  1702. ? e.detail.replaceAll('\n\n', '\n')
  1703. : e.detail
  1704. );
  1705. }
  1706. }}
  1707. />
  1708. <div
  1709. class="absolute bottom-1 text-xs text-gray-500 text-center line-clamp-1 right-0 left-0"
  1710. >
  1711. <!-- {$i18n.t('LLMs can make mistakes. Verify important information.')} -->
  1712. </div>
  1713. </div>
  1714. {:else}
  1715. <div class="overflow-auto w-full h-full flex items-center">
  1716. <Placeholder
  1717. {history}
  1718. {selectedModels}
  1719. bind:files
  1720. bind:prompt
  1721. bind:autoScroll
  1722. bind:selectedToolIds
  1723. bind:webSearchEnabled
  1724. bind:atSelectedModel
  1725. transparentBackground={$settings?.backgroundImageUrl ?? false}
  1726. {stopResponse}
  1727. {createMessagePair}
  1728. on:upload={async (e) => {
  1729. const { type, data } = e.detail;
  1730. if (type === 'web') {
  1731. await uploadWeb(data);
  1732. } else if (type === 'youtube') {
  1733. await uploadYoutubeTranscription(data);
  1734. }
  1735. }}
  1736. on:submit={async (e) => {
  1737. if (e.detail) {
  1738. await tick();
  1739. submitPrompt(
  1740. ($settings?.richTextInput ?? true)
  1741. ? e.detail.replaceAll('\n\n', '\n')
  1742. : e.detail
  1743. );
  1744. }
  1745. }}
  1746. />
  1747. </div>
  1748. {/if}
  1749. </div>
  1750. </Pane>
  1751. <ChatControls
  1752. bind:this={controlPaneComponent}
  1753. bind:history
  1754. bind:chatFiles
  1755. bind:params
  1756. bind:files
  1757. bind:pane={controlPane}
  1758. chatId={$chatId}
  1759. modelId={selectedModelIds?.at(0) ?? null}
  1760. models={selectedModelIds.reduce((a, e, i, arr) => {
  1761. const model = $models.find((m) => m.id === e);
  1762. if (model) {
  1763. return [...a, model];
  1764. }
  1765. return a;
  1766. }, [])}
  1767. {submitPrompt}
  1768. {stopResponse}
  1769. {showMessage}
  1770. {eventTarget}
  1771. />
  1772. </PaneGroup>
  1773. </div>
  1774. {/if}