Sidebar.svelte 31 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148
  1. <script lang="ts">
  2. import { toast } from 'svelte-sonner';
  3. import { v4 as uuidv4 } from 'uuid';
  4. import { goto } from '$app/navigation';
  5. import {
  6. user,
  7. chats,
  8. settings,
  9. showSettings,
  10. chatId,
  11. tags,
  12. showSidebar,
  13. showSearch,
  14. mobile,
  15. showArchivedChats,
  16. pinnedChats,
  17. scrollPaginationEnabled,
  18. currentChatPage,
  19. temporaryChatEnabled,
  20. channels,
  21. socket,
  22. config,
  23. isApp,
  24. models,
  25. selectedFolder,
  26. WEBUI_NAME
  27. } from '$lib/stores';
  28. import { onMount, getContext, tick, onDestroy } from 'svelte';
  29. const i18n = getContext('i18n');
  30. import {
  31. getChatList,
  32. getAllTags,
  33. getPinnedChatList,
  34. toggleChatPinnedStatusById,
  35. getChatById,
  36. updateChatFolderIdById,
  37. importChat
  38. } from '$lib/apis/chats';
  39. import { createNewFolder, getFolders, updateFolderParentIdById } from '$lib/apis/folders';
  40. import { WEBUI_BASE_URL } from '$lib/constants';
  41. import ArchivedChatsModal from './ArchivedChatsModal.svelte';
  42. import UserMenu from './Sidebar/UserMenu.svelte';
  43. import ChatItem from './Sidebar/ChatItem.svelte';
  44. import Spinner from '../common/Spinner.svelte';
  45. import Loader from '../common/Loader.svelte';
  46. import Folder from '../common/Folder.svelte';
  47. import Tooltip from '../common/Tooltip.svelte';
  48. import Folders from './Sidebar/Folders.svelte';
  49. import { getChannels, createNewChannel } from '$lib/apis/channels';
  50. import ChannelModal from './Sidebar/ChannelModal.svelte';
  51. import ChannelItem from './Sidebar/ChannelItem.svelte';
  52. import PencilSquare from '../icons/PencilSquare.svelte';
  53. import Search from '../icons/Search.svelte';
  54. import SearchModal from './SearchModal.svelte';
  55. import FolderModal from './Sidebar/Folders/FolderModal.svelte';
  56. import Sidebar from '../icons/Sidebar.svelte';
  57. import PinnedModelList from './Sidebar/PinnedModelList.svelte';
  58. import Note from '../icons/Note.svelte';
  59. import { slide } from 'svelte/transition';
  60. const BREAKPOINT = 768;
  61. let navElement;
  62. let shiftKey = false;
  63. let selectedChatId = null;
  64. let showPinnedChat = true;
  65. let showCreateChannel = false;
  66. // Pagination variables
  67. let chatListLoading = false;
  68. let allChatsLoaded = false;
  69. let showCreateFolderModal = false;
  70. let folders = {};
  71. let newFolderId = null;
  72. const initFolders = async () => {
  73. const folderList = await getFolders(localStorage.token).catch((error) => {
  74. toast.error(`${error}`);
  75. return [];
  76. });
  77. folders = {};
  78. // First pass: Initialize all folder entries
  79. for (const folder of folderList) {
  80. // Ensure folder is added to folders with its data
  81. folders[folder.id] = { ...(folders[folder.id] || {}), ...folder };
  82. if (newFolderId && folder.id === newFolderId) {
  83. folders[folder.id].new = true;
  84. newFolderId = null;
  85. }
  86. }
  87. // Second pass: Tie child folders to their parents
  88. for (const folder of folderList) {
  89. if (folder.parent_id) {
  90. // Ensure the parent folder is initialized if it doesn't exist
  91. if (!folders[folder.parent_id]) {
  92. folders[folder.parent_id] = {}; // Create a placeholder if not already present
  93. }
  94. // Initialize childrenIds array if it doesn't exist and add the current folder id
  95. folders[folder.parent_id].childrenIds = folders[folder.parent_id].childrenIds
  96. ? [...folders[folder.parent_id].childrenIds, folder.id]
  97. : [folder.id];
  98. // Sort the children by updated_at field
  99. folders[folder.parent_id].childrenIds.sort((a, b) => {
  100. return folders[b].updated_at - folders[a].updated_at;
  101. });
  102. }
  103. }
  104. };
  105. const createFolder = async ({ name, data }) => {
  106. if (name === '') {
  107. toast.error($i18n.t('Folder name cannot be empty.'));
  108. return;
  109. }
  110. const rootFolders = Object.values(folders).filter((folder) => folder.parent_id === null);
  111. if (rootFolders.find((folder) => folder.name.toLowerCase() === name.toLowerCase())) {
  112. // If a folder with the same name already exists, append a number to the name
  113. let i = 1;
  114. while (
  115. rootFolders.find((folder) => folder.name.toLowerCase() === `${name} ${i}`.toLowerCase())
  116. ) {
  117. i++;
  118. }
  119. name = `${name} ${i}`;
  120. }
  121. // Add a dummy folder to the list to show the user that the folder is being created
  122. const tempId = uuidv4();
  123. folders = {
  124. ...folders,
  125. tempId: {
  126. id: tempId,
  127. name: name,
  128. created_at: Date.now(),
  129. updated_at: Date.now()
  130. }
  131. };
  132. const res = await createNewFolder(localStorage.token, {
  133. name,
  134. data
  135. }).catch((error) => {
  136. toast.error(`${error}`);
  137. return null;
  138. });
  139. if (res) {
  140. // newFolderId = res.id;
  141. await initFolders();
  142. }
  143. };
  144. const initChannels = async () => {
  145. await channels.set(await getChannels(localStorage.token));
  146. };
  147. const initChatList = async () => {
  148. // Reset pagination variables
  149. tags.set(await getAllTags(localStorage.token));
  150. pinnedChats.set(await getPinnedChatList(localStorage.token));
  151. initFolders();
  152. currentChatPage.set(1);
  153. allChatsLoaded = false;
  154. await chats.set(await getChatList(localStorage.token, $currentChatPage));
  155. // Enable pagination
  156. scrollPaginationEnabled.set(true);
  157. };
  158. const loadMoreChats = async () => {
  159. chatListLoading = true;
  160. currentChatPage.set($currentChatPage + 1);
  161. let newChatList = [];
  162. newChatList = await getChatList(localStorage.token, $currentChatPage);
  163. // once the bottom of the list has been reached (no results) there is no need to continue querying
  164. allChatsLoaded = newChatList.length === 0;
  165. await chats.set([...($chats ? $chats : []), ...newChatList]);
  166. chatListLoading = false;
  167. };
  168. const importChatHandler = async (items, pinned = false, folderId = null) => {
  169. console.log('importChatHandler', items, pinned, folderId);
  170. for (const item of items) {
  171. console.log(item);
  172. if (item.chat) {
  173. await importChat(
  174. localStorage.token,
  175. item.chat,
  176. item?.meta ?? {},
  177. pinned,
  178. folderId,
  179. item?.created_at ?? null,
  180. item?.updated_at ?? null
  181. );
  182. }
  183. }
  184. initChatList();
  185. };
  186. const inputFilesHandler = async (files) => {
  187. console.log(files);
  188. for (const file of files) {
  189. const reader = new FileReader();
  190. reader.onload = async (e) => {
  191. const content = e.target.result;
  192. try {
  193. const chatItems = JSON.parse(content);
  194. importChatHandler(chatItems);
  195. } catch {
  196. toast.error($i18n.t(`Invalid file format.`));
  197. }
  198. };
  199. reader.readAsText(file);
  200. }
  201. };
  202. const tagEventHandler = async (type, tagName, chatId) => {
  203. console.log(type, tagName, chatId);
  204. if (type === 'delete') {
  205. initChatList();
  206. } else if (type === 'add') {
  207. initChatList();
  208. }
  209. };
  210. let draggedOver = false;
  211. const onDragOver = (e) => {
  212. e.preventDefault();
  213. // Check if a file is being draggedOver.
  214. if (e.dataTransfer?.types?.includes('Files')) {
  215. draggedOver = true;
  216. } else {
  217. draggedOver = false;
  218. }
  219. };
  220. const onDragLeave = () => {
  221. draggedOver = false;
  222. };
  223. const onDrop = async (e) => {
  224. e.preventDefault();
  225. console.log(e); // Log the drop event
  226. // Perform file drop check and handle it accordingly
  227. if (e.dataTransfer?.files) {
  228. const inputFiles = Array.from(e.dataTransfer?.files);
  229. if (inputFiles && inputFiles.length > 0) {
  230. console.log(inputFiles); // Log the dropped files
  231. inputFilesHandler(inputFiles); // Handle the dropped files
  232. }
  233. }
  234. draggedOver = false; // Reset draggedOver status after drop
  235. };
  236. let touchstart;
  237. let touchend;
  238. function checkDirection() {
  239. const screenWidth = window.innerWidth;
  240. const swipeDistance = Math.abs(touchend.screenX - touchstart.screenX);
  241. if (touchstart.clientX < 40 && swipeDistance >= screenWidth / 8) {
  242. if (touchend.screenX < touchstart.screenX) {
  243. showSidebar.set(false);
  244. }
  245. if (touchend.screenX > touchstart.screenX) {
  246. showSidebar.set(true);
  247. }
  248. }
  249. }
  250. const onTouchStart = (e) => {
  251. touchstart = e.changedTouches[0];
  252. console.log(touchstart.clientX);
  253. };
  254. const onTouchEnd = (e) => {
  255. touchend = e.changedTouches[0];
  256. checkDirection();
  257. };
  258. const onKeyDown = (e) => {
  259. if (e.key === 'Shift') {
  260. shiftKey = true;
  261. }
  262. };
  263. const onKeyUp = (e) => {
  264. if (e.key === 'Shift') {
  265. shiftKey = false;
  266. }
  267. };
  268. const onFocus = () => {};
  269. const onBlur = () => {
  270. shiftKey = false;
  271. selectedChatId = null;
  272. };
  273. onMount(async () => {
  274. showPinnedChat = localStorage?.showPinnedChat ? localStorage.showPinnedChat === 'true' : true;
  275. mobile.subscribe((value) => {
  276. if ($showSidebar && value) {
  277. showSidebar.set(false);
  278. }
  279. if ($showSidebar && !value) {
  280. const navElement = document.getElementsByTagName('nav')[0];
  281. if (navElement) {
  282. navElement.style['-webkit-app-region'] = 'drag';
  283. }
  284. }
  285. if (!$showSidebar && !value) {
  286. showSidebar.set(true);
  287. }
  288. });
  289. showSidebar.set(!$mobile ? localStorage.sidebar === 'true' : false);
  290. showSidebar.subscribe((value) => {
  291. localStorage.sidebar = value;
  292. // nav element is not available on the first render
  293. const navElement = document.getElementsByTagName('nav')[0];
  294. if (navElement) {
  295. if ($mobile) {
  296. if (!value) {
  297. navElement.style['-webkit-app-region'] = 'drag';
  298. } else {
  299. navElement.style['-webkit-app-region'] = 'no-drag';
  300. }
  301. } else {
  302. navElement.style['-webkit-app-region'] = 'drag';
  303. }
  304. }
  305. });
  306. chats.subscribe((value) => {
  307. initFolders();
  308. });
  309. await initChannels();
  310. await initChatList();
  311. window.addEventListener('keydown', onKeyDown);
  312. window.addEventListener('keyup', onKeyUp);
  313. window.addEventListener('touchstart', onTouchStart);
  314. window.addEventListener('touchend', onTouchEnd);
  315. window.addEventListener('focus', onFocus);
  316. window.addEventListener('blur', onBlur);
  317. const dropZone = document.getElementById('sidebar');
  318. dropZone?.addEventListener('dragover', onDragOver);
  319. dropZone?.addEventListener('drop', onDrop);
  320. dropZone?.addEventListener('dragleave', onDragLeave);
  321. });
  322. onDestroy(() => {
  323. window.removeEventListener('keydown', onKeyDown);
  324. window.removeEventListener('keyup', onKeyUp);
  325. window.removeEventListener('touchstart', onTouchStart);
  326. window.removeEventListener('touchend', onTouchEnd);
  327. window.removeEventListener('focus', onFocus);
  328. window.removeEventListener('blur', onBlur);
  329. const dropZone = document.getElementById('sidebar');
  330. dropZone?.removeEventListener('dragover', onDragOver);
  331. dropZone?.removeEventListener('drop', onDrop);
  332. dropZone?.removeEventListener('dragleave', onDragLeave);
  333. });
  334. const newChatHandler = async () => {
  335. selectedChatId = null;
  336. selectedFolder.set(null);
  337. if ($user?.role !== 'admin' && $user?.permissions?.chat?.temporary_enforced) {
  338. await temporaryChatEnabled.set(true);
  339. } else {
  340. await temporaryChatEnabled.set(false);
  341. }
  342. setTimeout(() => {
  343. if ($mobile) {
  344. showSidebar.set(false);
  345. }
  346. }, 0);
  347. };
  348. const itemClickHandler = async () => {
  349. selectedChatId = null;
  350. chatId.set('');
  351. if ($mobile) {
  352. showSidebar.set(false);
  353. }
  354. await tick();
  355. };
  356. </script>
  357. <ArchivedChatsModal
  358. bind:show={$showArchivedChats}
  359. onUpdate={async () => {
  360. await initChatList();
  361. }}
  362. />
  363. <ChannelModal
  364. bind:show={showCreateChannel}
  365. onSubmit={async ({ name, access_control }) => {
  366. const res = await createNewChannel(localStorage.token, {
  367. name: name,
  368. access_control: access_control
  369. }).catch((error) => {
  370. toast.error(`${error}`);
  371. return null;
  372. });
  373. if (res) {
  374. $socket.emit('join-channels', { auth: { token: $user?.token } });
  375. await initChannels();
  376. showCreateChannel = false;
  377. }
  378. }}
  379. />
  380. <FolderModal
  381. bind:show={showCreateFolderModal}
  382. onSubmit={async (folder) => {
  383. await createFolder(folder);
  384. showCreateFolderModal = false;
  385. }}
  386. />
  387. <!-- svelte-ignore a11y-no-static-element-interactions -->
  388. {#if $showSidebar}
  389. <div
  390. class=" {$isApp
  391. ? ' ml-[4.5rem] md:ml-0'
  392. : ''} fixed md:hidden z-40 top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center overflow-hidden overscroll-contain"
  393. on:mousedown={() => {
  394. showSidebar.set(!$showSidebar);
  395. }}
  396. />
  397. {/if}
  398. <SearchModal
  399. bind:show={$showSearch}
  400. onClose={() => {
  401. if ($mobile) {
  402. showSidebar.set(false);
  403. }
  404. }}
  405. />
  406. <button
  407. id="sidebar-new-chat-button"
  408. class="hidden"
  409. on:click={() => {
  410. goto('/');
  411. newChatHandler();
  412. }}
  413. />
  414. {#if !$mobile && !$showSidebar}
  415. <div
  416. class=" py-2 px-1.5 flex flex-col justify-between text-black dark:text-white h-full border-e border-gray-50 dark:border-gray-850 z-10"
  417. id="sidebar"
  418. >
  419. <button
  420. class="flex flex-col flex-1 cursor-[e-resize]"
  421. on:click={() => {
  422. showSidebar.set(!$showSidebar);
  423. }}
  424. >
  425. <div class="pb-1.5">
  426. <Tooltip
  427. content={$showSidebar ? $i18n.t('Close Sidebar') : $i18n.t('Open Sidebar')}
  428. placement="right"
  429. >
  430. <button
  431. class=" flex rounded-lg hover:bg-gray-100 dark:hover:bg-gray-850 transition group cursor-[e-resize]"
  432. >
  433. <div class=" self-center flex items-center justify-center size-9">
  434. <img
  435. crossorigin="anonymous"
  436. src="{WEBUI_BASE_URL}/static/favicon.png"
  437. class="sidebar-new-chat-icon size-6 rounded-full group-hover:hidden"
  438. alt=""
  439. />
  440. <Sidebar className="size-5 hidden group-hover:flex" />
  441. </div>
  442. </button>
  443. </Tooltip>
  444. </div>
  445. <div>
  446. <div class="">
  447. <Tooltip content={$i18n.t('New Chat')} placement="right">
  448. <a
  449. class=" cursor-pointer flex rounded-lg hover:bg-gray-100 dark:hover:bg-gray-850 transition group"
  450. href="/"
  451. draggable="false"
  452. on:click={async (e) => {
  453. e.stopImmediatePropagation();
  454. e.preventDefault();
  455. goto('/');
  456. newChatHandler();
  457. }}
  458. >
  459. <div class=" self-center flex items-center justify-center size-9">
  460. <PencilSquare className="size-4.5" />
  461. </div>
  462. </a>
  463. </Tooltip>
  464. </div>
  465. <div class="">
  466. <Tooltip content={$i18n.t('Search')} placement="right">
  467. <button
  468. class=" cursor-pointer flex rounded-lg hover:bg-gray-100 dark:hover:bg-gray-850 transition group"
  469. on:click={(e) => {
  470. e.stopImmediatePropagation();
  471. e.preventDefault();
  472. showSearch.set(true);
  473. }}
  474. draggable="false"
  475. >
  476. <div class=" self-center flex items-center justify-center size-9">
  477. <Search className="size-4.5" />
  478. </div>
  479. </button>
  480. </Tooltip>
  481. </div>
  482. {#if ($config?.features?.enable_notes ?? false) && ($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true))}
  483. <div class="">
  484. <Tooltip content={$i18n.t('Notes')} placement="right">
  485. <a
  486. class=" cursor-pointer flex rounded-lg hover:bg-gray-100 dark:hover:bg-gray-850 transition group"
  487. href="/notes"
  488. on:click={async (e) => {
  489. e.stopImmediatePropagation();
  490. e.preventDefault();
  491. goto('/notes');
  492. itemClickHandler();
  493. }}
  494. draggable="false"
  495. >
  496. <div class=" self-center flex items-center justify-center size-9">
  497. <Note className="size-4.5" />
  498. </div>
  499. </a>
  500. </Tooltip>
  501. </div>
  502. {/if}
  503. {#if $user?.role === 'admin' || $user?.permissions?.workspace?.models || $user?.permissions?.workspace?.knowledge || $user?.permissions?.workspace?.prompts || $user?.permissions?.workspace?.tools}
  504. <div class="">
  505. <Tooltip content={$i18n.t('Workspace')} placement="right">
  506. <a
  507. class=" cursor-pointer flex rounded-lg hover:bg-gray-100 dark:hover:bg-gray-850 transition group"
  508. href="/workspace"
  509. on:click={async (e) => {
  510. e.stopImmediatePropagation();
  511. e.preventDefault();
  512. goto('/workspace');
  513. itemClickHandler();
  514. }}
  515. draggable="false"
  516. >
  517. <div class=" self-center flex items-center justify-center size-9">
  518. <svg
  519. xmlns="http://www.w3.org/2000/svg"
  520. fill="none"
  521. viewBox="0 0 24 24"
  522. stroke-width="1.5"
  523. stroke="currentColor"
  524. class="size-4.5"
  525. >
  526. <path
  527. stroke-linecap="round"
  528. stroke-linejoin="round"
  529. d="M13.5 16.875h3.375m0 0h3.375m-3.375 0V13.5m0 3.375v3.375M6 10.5h2.25a2.25 2.25 0 0 0 2.25-2.25V6a2.25 2.25 0 0 0-2.25-2.25H6A2.25 2.25 0 0 0 3.75 6v2.25A2.25 2.25 0 0 0 6 10.5Zm0 9.75h2.25A2.25 2.25 0 0 0 10.5 18v-2.25a2.25 2.25 0 0 0-2.25-2.25H6a2.25 2.25 0 0 0-2.25 2.25V18A2.25 2.25 0 0 0 6 20.25Zm9.75-9.75H18a2.25 2.25 0 0 0 2.25-2.25V6A2.25 2.25 0 0 0 18 3.75h-2.25A2.25 2.25 0 0 0 13.5 6v2.25a2.25 2.25 0 0 0 2.25 2.25Z"
  530. />
  531. </svg>
  532. </div>
  533. </a>
  534. </Tooltip>
  535. </div>
  536. {/if}
  537. </div>
  538. </button>
  539. <div>
  540. <div>
  541. <div class=" py-0.5">
  542. {#if $user !== undefined && $user !== null}
  543. <UserMenu
  544. role={$user?.role}
  545. on:show={(e) => {
  546. if (e.detail === 'archived-chat') {
  547. showArchivedChats.set(true);
  548. }
  549. }}
  550. >
  551. <div
  552. class=" cursor-pointer flex rounded-lg hover:bg-gray-100 dark:hover:bg-gray-850 transition group"
  553. >
  554. <div class=" self-center flex items-center justify-center size-9">
  555. <img
  556. src={$user?.profile_image_url}
  557. class=" size-6 object-cover rounded-full"
  558. alt={$i18n.t('Open User Profile Menu')}
  559. aria-label={$i18n.t('Open User Profile Menu')}
  560. />
  561. </div>
  562. </div>
  563. </UserMenu>
  564. {/if}
  565. </div>
  566. </div>
  567. </div>
  568. </div>
  569. {/if}
  570. {#if $showSidebar}
  571. <div
  572. bind:this={navElement}
  573. id="sidebar"
  574. class="h-screen max-h-[100dvh] min-h-screen select-none {$showSidebar
  575. ? 'bg-gray-50 dark:bg-gray-950 z-50'
  576. : ' bg-transparent z-0 '} {$isApp
  577. ? `ml-[4.5rem] md:ml-0 `
  578. : ' transition-all duration-300 '} shrink-0 text-gray-900 dark:text-gray-200 text-sm fixed top-0 left-0 overflow-x-hidden
  579. "
  580. transition:slide={{ duration: 200, axis: 'x' }}
  581. data-state={$showSidebar}
  582. >
  583. <div
  584. class=" my-auto flex flex-col justify-between h-screen max-h-[100dvh] w-[260px] overflow-x-hidden scrollbar-hidden z-50 {$showSidebar
  585. ? ''
  586. : 'invisible'}"
  587. >
  588. <div
  589. class="sidebar px-1.5 pt-2 pb-1.5 flex justify-between space-x-1 text-gray-600 dark:text-gray-400 sticky top-0 z-10 bg-gray-50 dark:bg-gray-950"
  590. >
  591. <a
  592. class="flex items-center rounded-lg p-1.5 h-full justify-center hover:bg-gray-100 dark:hover:bg-gray-850 transition no-drag-region"
  593. href="/"
  594. draggable="false"
  595. on:click={newChatHandler}
  596. >
  597. <img
  598. crossorigin="anonymous"
  599. src="{WEBUI_BASE_URL}/static/favicon.png"
  600. class="sidebar-new-chat-icon size-6 rounded-full"
  601. alt=""
  602. />
  603. </a>
  604. <a href="/" class="flex flex-1 px-1.5" on:click={newChatHandler}>
  605. <div class=" self-center font-medium text-gray-850 dark:text-white font-primary">
  606. {$WEBUI_NAME}
  607. </div>
  608. </a>
  609. <Tooltip
  610. content={$showSidebar ? $i18n.t('Close Sidebar') : $i18n.t('Open Sidebar')}
  611. placement="bottom"
  612. >
  613. <button
  614. class=" flex rounded-lg hover:bg-gray-100 dark:hover:bg-gray-850 transition cursor-[w-resize]"
  615. on:click={() => {
  616. showSidebar.set(!$showSidebar);
  617. }}
  618. >
  619. <div class=" self-center p-1.5">
  620. <Sidebar />
  621. </div>
  622. </button>
  623. </Tooltip>
  624. </div>
  625. <div class="pb-1.5">
  626. <div class="px-[7px] flex justify-center text-gray-800 dark:text-gray-200">
  627. <a
  628. id="sidebar-new-chat-button"
  629. class="grow flex items-center space-x-3 rounded-lg px-2 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition outline-none"
  630. href="/"
  631. draggable="false"
  632. on:click={newChatHandler}
  633. >
  634. <div class="self-center">
  635. <PencilSquare className=" size-4.5" strokeWidth="2" />
  636. </div>
  637. <div class="flex self-center translate-y-[0.5px]">
  638. <div class=" self-center text-sm font-primary">{$i18n.t('New Chat')}</div>
  639. </div>
  640. </a>
  641. </div>
  642. <div class="px-[7px] flex justify-center text-gray-800 dark:text-gray-200">
  643. <button
  644. class="grow flex items-center space-x-3 rounded-lg px-2 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition outline-none"
  645. on:click={() => {
  646. showSearch.set(true);
  647. }}
  648. draggable="false"
  649. >
  650. <div class="self-center">
  651. <Search strokeWidth="2" className="size-4.5" />
  652. </div>
  653. <div class="flex self-center translate-y-[0.5px]">
  654. <div class=" self-center text-sm font-primary">{$i18n.t('Search')}</div>
  655. </div>
  656. </button>
  657. </div>
  658. {#if ($config?.features?.enable_notes ?? false) && ($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true))}
  659. <div class="px-[7px] flex justify-center text-gray-800 dark:text-gray-200">
  660. <a
  661. class="grow flex items-center space-x-3 rounded-lg px-2 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
  662. href="/notes"
  663. on:click={itemClickHandler}
  664. draggable="false"
  665. >
  666. <div class="self-center">
  667. <Note className="size-4.5" strokeWidth="2" />
  668. </div>
  669. <div class="flex self-center translate-y-[0.5px]">
  670. <div class=" self-center text-sm font-primary">{$i18n.t('Notes')}</div>
  671. </div>
  672. </a>
  673. </div>
  674. {/if}
  675. {#if $user?.role === 'admin' || $user?.permissions?.workspace?.models || $user?.permissions?.workspace?.knowledge || $user?.permissions?.workspace?.prompts || $user?.permissions?.workspace?.tools}
  676. <div class="px-[7px] flex justify-center text-gray-800 dark:text-gray-200">
  677. <a
  678. class="grow flex items-center space-x-3 rounded-lg px-2 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
  679. href="/workspace"
  680. on:click={itemClickHandler}
  681. draggable="false"
  682. >
  683. <div class="self-center">
  684. <svg
  685. xmlns="http://www.w3.org/2000/svg"
  686. fill="none"
  687. viewBox="0 0 24 24"
  688. stroke-width="2"
  689. stroke="currentColor"
  690. class="size-4.5"
  691. >
  692. <path
  693. stroke-linecap="round"
  694. stroke-linejoin="round"
  695. d="M13.5 16.875h3.375m0 0h3.375m-3.375 0V13.5m0 3.375v3.375M6 10.5h2.25a2.25 2.25 0 0 0 2.25-2.25V6a2.25 2.25 0 0 0-2.25-2.25H6A2.25 2.25 0 0 0 3.75 6v2.25A2.25 2.25 0 0 0 6 10.5Zm0 9.75h2.25A2.25 2.25 0 0 0 10.5 18v-2.25a2.25 2.25 0 0 0-2.25-2.25H6a2.25 2.25 0 0 0-2.25 2.25V18A2.25 2.25 0 0 0 6 20.25Zm9.75-9.75H18a2.25 2.25 0 0 0 2.25-2.25V6A2.25 2.25 0 0 0 18 3.75h-2.25A2.25 2.25 0 0 0 13.5 6v2.25a2.25 2.25 0 0 0 2.25 2.25Z"
  696. />
  697. </svg>
  698. </div>
  699. <div class="flex self-center translate-y-[0.5px]">
  700. <div class=" self-center text-sm font-primary">{$i18n.t('Workspace')}</div>
  701. </div>
  702. </a>
  703. </div>
  704. {/if}
  705. </div>
  706. <div class="relative flex flex-col flex-1">
  707. {#if ($models ?? []).length > 0 && ($settings?.pinnedModels ?? []).length > 0}
  708. <PinnedModelList bind:selectedChatId {shiftKey} />
  709. {/if}
  710. {#if $config?.features?.enable_channels && ($user?.role === 'admin' || $channels.length > 0)}
  711. <Folder
  712. className="px-2 mt-0.5"
  713. name={$i18n.t('Channels')}
  714. dragAndDrop={false}
  715. onAdd={async () => {
  716. if ($user?.role === 'admin') {
  717. await tick();
  718. setTimeout(() => {
  719. showCreateChannel = true;
  720. }, 0);
  721. }
  722. }}
  723. onAddLabel={$i18n.t('Create Channel')}
  724. >
  725. {#each $channels as channel}
  726. <ChannelItem
  727. {channel}
  728. onUpdate={async () => {
  729. await initChannels();
  730. }}
  731. />
  732. {/each}
  733. </Folder>
  734. {/if}
  735. <Folder
  736. className="px-2 mt-0.5"
  737. name={$i18n.t('Chats')}
  738. onAdd={() => {
  739. showCreateFolderModal = true;
  740. }}
  741. onAddLabel={$i18n.t('New Folder')}
  742. on:change={async (e) => {
  743. selectedFolder.set(null);
  744. await goto('/');
  745. }}
  746. on:import={(e) => {
  747. importChatHandler(e.detail);
  748. }}
  749. on:drop={async (e) => {
  750. const { type, id, item } = e.detail;
  751. if (type === 'chat') {
  752. let chat = await getChatById(localStorage.token, id).catch((error) => {
  753. return null;
  754. });
  755. if (!chat && item) {
  756. chat = await importChat(
  757. localStorage.token,
  758. item.chat,
  759. item?.meta ?? {},
  760. false,
  761. null,
  762. item?.created_at ?? null,
  763. item?.updated_at ?? null
  764. );
  765. }
  766. if (chat) {
  767. console.log(chat);
  768. if (chat.folder_id) {
  769. const res = await updateChatFolderIdById(localStorage.token, chat.id, null).catch(
  770. (error) => {
  771. toast.error(`${error}`);
  772. return null;
  773. }
  774. );
  775. }
  776. if (chat.pinned) {
  777. const res = await toggleChatPinnedStatusById(localStorage.token, chat.id);
  778. }
  779. initChatList();
  780. }
  781. } else if (type === 'folder') {
  782. if (folders[id].parent_id === null) {
  783. return;
  784. }
  785. const res = await updateFolderParentIdById(localStorage.token, id, null).catch(
  786. (error) => {
  787. toast.error(`${error}`);
  788. return null;
  789. }
  790. );
  791. if (res) {
  792. await initFolders();
  793. }
  794. }
  795. }}
  796. >
  797. {#if $pinnedChats.length > 0}
  798. <div class="flex flex-col space-y-1 rounded-xl">
  799. <Folder
  800. className=""
  801. bind:open={showPinnedChat}
  802. on:change={(e) => {
  803. localStorage.setItem('showPinnedChat', e.detail);
  804. console.log(e.detail);
  805. }}
  806. on:import={(e) => {
  807. importChatHandler(e.detail, true);
  808. }}
  809. on:drop={async (e) => {
  810. const { type, id, item } = e.detail;
  811. if (type === 'chat') {
  812. let chat = await getChatById(localStorage.token, id).catch((error) => {
  813. return null;
  814. });
  815. if (!chat && item) {
  816. chat = await importChat(
  817. localStorage.token,
  818. item.chat,
  819. item?.meta ?? {},
  820. false,
  821. null,
  822. item?.created_at ?? null,
  823. item?.updated_at ?? null
  824. );
  825. }
  826. if (chat) {
  827. console.log(chat);
  828. if (chat.folder_id) {
  829. const res = await updateChatFolderIdById(
  830. localStorage.token,
  831. chat.id,
  832. null
  833. ).catch((error) => {
  834. toast.error(`${error}`);
  835. return null;
  836. });
  837. }
  838. if (!chat.pinned) {
  839. const res = await toggleChatPinnedStatusById(localStorage.token, chat.id);
  840. }
  841. initChatList();
  842. }
  843. }
  844. }}
  845. name={$i18n.t('Pinned')}
  846. >
  847. <div
  848. class="ml-3 pl-1 mt-[1px] flex flex-col overflow-y-auto scrollbar-hidden border-s border-gray-100 dark:border-gray-900"
  849. >
  850. {#each $pinnedChats as chat, idx (`pinned-chat-${chat?.id ?? idx}`)}
  851. <ChatItem
  852. className=""
  853. id={chat.id}
  854. title={chat.title}
  855. {shiftKey}
  856. selected={selectedChatId === chat.id}
  857. on:select={() => {
  858. selectedChatId = chat.id;
  859. }}
  860. on:unselect={() => {
  861. selectedChatId = null;
  862. }}
  863. on:change={async () => {
  864. initChatList();
  865. }}
  866. on:tag={(e) => {
  867. const { type, name } = e.detail;
  868. tagEventHandler(type, name, chat.id);
  869. }}
  870. />
  871. {/each}
  872. </div>
  873. </Folder>
  874. </div>
  875. {/if}
  876. {#if folders}
  877. <Folders
  878. {folders}
  879. {shiftKey}
  880. onDelete={(folderId) => {
  881. selectedFolder.set(null);
  882. initChatList();
  883. }}
  884. on:update={() => {
  885. initChatList();
  886. }}
  887. on:import={(e) => {
  888. const { folderId, items } = e.detail;
  889. importChatHandler(items, false, folderId);
  890. }}
  891. on:change={async () => {
  892. initChatList();
  893. }}
  894. />
  895. {/if}
  896. <div class=" flex-1 flex flex-col overflow-y-auto scrollbar-hidden">
  897. <div class="pt-1.5">
  898. {#if $chats}
  899. {#each $chats as chat, idx (`chat-${chat?.id ?? idx}`)}
  900. {#if idx === 0 || (idx > 0 && chat.time_range !== $chats[idx - 1].time_range)}
  901. <div
  902. class="w-full pl-2.5 text-xs text-gray-500 dark:text-gray-500 font-medium {idx ===
  903. 0
  904. ? ''
  905. : 'pt-5'} pb-1.5"
  906. >
  907. {$i18n.t(chat.time_range)}
  908. <!-- localisation keys for time_range to be recognized from the i18next parser (so they don't get automatically removed):
  909. {$i18n.t('Today')}
  910. {$i18n.t('Yesterday')}
  911. {$i18n.t('Previous 7 days')}
  912. {$i18n.t('Previous 30 days')}
  913. {$i18n.t('January')}
  914. {$i18n.t('February')}
  915. {$i18n.t('March')}
  916. {$i18n.t('April')}
  917. {$i18n.t('May')}
  918. {$i18n.t('June')}
  919. {$i18n.t('July')}
  920. {$i18n.t('August')}
  921. {$i18n.t('September')}
  922. {$i18n.t('October')}
  923. {$i18n.t('November')}
  924. {$i18n.t('December')}
  925. -->
  926. </div>
  927. {/if}
  928. <ChatItem
  929. className=""
  930. id={chat.id}
  931. title={chat.title}
  932. {shiftKey}
  933. selected={selectedChatId === chat.id}
  934. on:select={() => {
  935. selectedChatId = chat.id;
  936. }}
  937. on:unselect={() => {
  938. selectedChatId = null;
  939. }}
  940. on:change={async () => {
  941. initChatList();
  942. }}
  943. on:tag={(e) => {
  944. const { type, name } = e.detail;
  945. tagEventHandler(type, name, chat.id);
  946. }}
  947. />
  948. {/each}
  949. {#if $scrollPaginationEnabled && !allChatsLoaded}
  950. <Loader
  951. on:visible={(e) => {
  952. if (!chatListLoading) {
  953. loadMoreChats();
  954. }
  955. }}
  956. >
  957. <div
  958. class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2"
  959. >
  960. <Spinner className=" size-4" />
  961. <div class=" ">Loading...</div>
  962. </div>
  963. </Loader>
  964. {/if}
  965. {:else}
  966. <div
  967. class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2"
  968. >
  969. <Spinner className=" size-4" />
  970. <div class=" ">Loading...</div>
  971. </div>
  972. {/if}
  973. </div>
  974. </div>
  975. </Folder>
  976. </div>
  977. <div class="px-1.5 pt-1.5 pb-2 sticky bottom-0 z-10 bg-gray-50 dark:bg-gray-950 sidebar">
  978. <div class="flex flex-col font-primary">
  979. {#if $user !== undefined && $user !== null}
  980. <UserMenu
  981. role={$user?.role}
  982. on:show={(e) => {
  983. if (e.detail === 'archived-chat') {
  984. showArchivedChats.set(true);
  985. }
  986. }}
  987. >
  988. <div
  989. class=" flex items-center rounded-xl py-2 px-1.5 w-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
  990. >
  991. <div class=" self-center mr-3">
  992. <img
  993. src={$user?.profile_image_url}
  994. class=" size-6 object-cover rounded-full"
  995. alt={$i18n.t('Open User Profile Menu')}
  996. aria-label={$i18n.t('Open User Profile Menu')}
  997. />
  998. </div>
  999. <div class=" self-center font-medium">{$user?.name}</div>
  1000. </div>
  1001. </UserMenu>
  1002. {/if}
  1003. </div>
  1004. </div>
  1005. </div>
  1006. </div>
  1007. {/if}
  1008. <style>
  1009. .scrollbar-hidden:active::-webkit-scrollbar-thumb,
  1010. .scrollbar-hidden:focus::-webkit-scrollbar-thumb,
  1011. .scrollbar-hidden:hover::-webkit-scrollbar-thumb {
  1012. visibility: visible;
  1013. }
  1014. .scrollbar-hidden::-webkit-scrollbar-thumb {
  1015. visibility: hidden;
  1016. }
  1017. </style>