Sidebar.svelte 26 KB

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