12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148 |
- <script lang="ts">
- import { toast } from 'svelte-sonner';
- import { v4 as uuidv4 } from 'uuid';
- import { goto } from '$app/navigation';
- import {
- user,
- chats,
- settings,
- showSettings,
- chatId,
- tags,
- showSidebar,
- showSearch,
- mobile,
- showArchivedChats,
- pinnedChats,
- scrollPaginationEnabled,
- currentChatPage,
- temporaryChatEnabled,
- channels,
- socket,
- config,
- isApp,
- models,
- selectedFolder,
- WEBUI_NAME
- } from '$lib/stores';
- import { onMount, getContext, tick, onDestroy } from 'svelte';
- const i18n = getContext('i18n');
- import {
- getChatList,
- getAllTags,
- getPinnedChatList,
- toggleChatPinnedStatusById,
- getChatById,
- updateChatFolderIdById,
- importChat
- } from '$lib/apis/chats';
- import { createNewFolder, getFolders, updateFolderParentIdById } from '$lib/apis/folders';
- import { WEBUI_BASE_URL } from '$lib/constants';
- import ArchivedChatsModal from './ArchivedChatsModal.svelte';
- import UserMenu from './Sidebar/UserMenu.svelte';
- import ChatItem from './Sidebar/ChatItem.svelte';
- import Spinner from '../common/Spinner.svelte';
- import Loader from '../common/Loader.svelte';
- import Folder from '../common/Folder.svelte';
- import Tooltip from '../common/Tooltip.svelte';
- import Folders from './Sidebar/Folders.svelte';
- import { getChannels, createNewChannel } from '$lib/apis/channels';
- import ChannelModal from './Sidebar/ChannelModal.svelte';
- import ChannelItem from './Sidebar/ChannelItem.svelte';
- import PencilSquare from '../icons/PencilSquare.svelte';
- import Search from '../icons/Search.svelte';
- import SearchModal from './SearchModal.svelte';
- import FolderModal from './Sidebar/Folders/FolderModal.svelte';
- import Sidebar from '../icons/Sidebar.svelte';
- import PinnedModelList from './Sidebar/PinnedModelList.svelte';
- import Note from '../icons/Note.svelte';
- import { slide } from 'svelte/transition';
- const BREAKPOINT = 768;
- let navElement;
- let shiftKey = false;
- let selectedChatId = null;
- let showPinnedChat = true;
- let showCreateChannel = false;
- // Pagination variables
- let chatListLoading = false;
- let allChatsLoaded = false;
- let showCreateFolderModal = false;
- let folders = {};
- let newFolderId = null;
- const initFolders = async () => {
- const folderList = await getFolders(localStorage.token).catch((error) => {
- toast.error(`${error}`);
- return [];
- });
- folders = {};
- // First pass: Initialize all folder entries
- for (const folder of folderList) {
- // Ensure folder is added to folders with its data
- folders[folder.id] = { ...(folders[folder.id] || {}), ...folder };
- if (newFolderId && folder.id === newFolderId) {
- folders[folder.id].new = true;
- newFolderId = null;
- }
- }
- // Second pass: Tie child folders to their parents
- for (const folder of folderList) {
- if (folder.parent_id) {
- // Ensure the parent folder is initialized if it doesn't exist
- if (!folders[folder.parent_id]) {
- folders[folder.parent_id] = {}; // Create a placeholder if not already present
- }
- // Initialize childrenIds array if it doesn't exist and add the current folder id
- folders[folder.parent_id].childrenIds = folders[folder.parent_id].childrenIds
- ? [...folders[folder.parent_id].childrenIds, folder.id]
- : [folder.id];
- // Sort the children by updated_at field
- folders[folder.parent_id].childrenIds.sort((a, b) => {
- return folders[b].updated_at - folders[a].updated_at;
- });
- }
- }
- };
- const createFolder = async ({ name, data }) => {
- if (name === '') {
- toast.error($i18n.t('Folder name cannot be empty.'));
- return;
- }
- const rootFolders = Object.values(folders).filter((folder) => folder.parent_id === null);
- if (rootFolders.find((folder) => folder.name.toLowerCase() === name.toLowerCase())) {
- // If a folder with the same name already exists, append a number to the name
- let i = 1;
- while (
- rootFolders.find((folder) => folder.name.toLowerCase() === `${name} ${i}`.toLowerCase())
- ) {
- i++;
- }
- name = `${name} ${i}`;
- }
- // Add a dummy folder to the list to show the user that the folder is being created
- const tempId = uuidv4();
- folders = {
- ...folders,
- tempId: {
- id: tempId,
- name: name,
- created_at: Date.now(),
- updated_at: Date.now()
- }
- };
- const res = await createNewFolder(localStorage.token, {
- name,
- data
- }).catch((error) => {
- toast.error(`${error}`);
- return null;
- });
- if (res) {
- // newFolderId = res.id;
- await initFolders();
- }
- };
- const initChannels = async () => {
- await channels.set(await getChannels(localStorage.token));
- };
- const initChatList = async () => {
- // Reset pagination variables
- tags.set(await getAllTags(localStorage.token));
- pinnedChats.set(await getPinnedChatList(localStorage.token));
- initFolders();
- currentChatPage.set(1);
- allChatsLoaded = false;
- await chats.set(await getChatList(localStorage.token, $currentChatPage));
- // Enable pagination
- scrollPaginationEnabled.set(true);
- };
- const loadMoreChats = async () => {
- chatListLoading = true;
- currentChatPage.set($currentChatPage + 1);
- let newChatList = [];
- newChatList = await getChatList(localStorage.token, $currentChatPage);
- // once the bottom of the list has been reached (no results) there is no need to continue querying
- allChatsLoaded = newChatList.length === 0;
- await chats.set([...($chats ? $chats : []), ...newChatList]);
- chatListLoading = false;
- };
- const importChatHandler = async (items, pinned = false, folderId = null) => {
- console.log('importChatHandler', items, pinned, folderId);
- for (const item of items) {
- console.log(item);
- if (item.chat) {
- await importChat(
- localStorage.token,
- item.chat,
- item?.meta ?? {},
- pinned,
- folderId,
- item?.created_at ?? null,
- item?.updated_at ?? null
- );
- }
- }
- initChatList();
- };
- const inputFilesHandler = async (files) => {
- console.log(files);
- for (const file of files) {
- const reader = new FileReader();
- reader.onload = async (e) => {
- const content = e.target.result;
- try {
- const chatItems = JSON.parse(content);
- importChatHandler(chatItems);
- } catch {
- toast.error($i18n.t(`Invalid file format.`));
- }
- };
- reader.readAsText(file);
- }
- };
- const tagEventHandler = async (type, tagName, chatId) => {
- console.log(type, tagName, chatId);
- if (type === 'delete') {
- initChatList();
- } else if (type === 'add') {
- initChatList();
- }
- };
- let draggedOver = false;
- const onDragOver = (e) => {
- e.preventDefault();
- // Check if a file is being draggedOver.
- if (e.dataTransfer?.types?.includes('Files')) {
- draggedOver = true;
- } else {
- draggedOver = false;
- }
- };
- const onDragLeave = () => {
- draggedOver = false;
- };
- const onDrop = async (e) => {
- e.preventDefault();
- console.log(e); // Log the drop event
- // Perform file drop check and handle it accordingly
- if (e.dataTransfer?.files) {
- const inputFiles = Array.from(e.dataTransfer?.files);
- if (inputFiles && inputFiles.length > 0) {
- console.log(inputFiles); // Log the dropped files
- inputFilesHandler(inputFiles); // Handle the dropped files
- }
- }
- draggedOver = false; // Reset draggedOver status after drop
- };
- let touchstart;
- let touchend;
- function checkDirection() {
- const screenWidth = window.innerWidth;
- const swipeDistance = Math.abs(touchend.screenX - touchstart.screenX);
- if (touchstart.clientX < 40 && swipeDistance >= screenWidth / 8) {
- if (touchend.screenX < touchstart.screenX) {
- showSidebar.set(false);
- }
- if (touchend.screenX > touchstart.screenX) {
- showSidebar.set(true);
- }
- }
- }
- const onTouchStart = (e) => {
- touchstart = e.changedTouches[0];
- console.log(touchstart.clientX);
- };
- const onTouchEnd = (e) => {
- touchend = e.changedTouches[0];
- checkDirection();
- };
- const onKeyDown = (e) => {
- if (e.key === 'Shift') {
- shiftKey = true;
- }
- };
- const onKeyUp = (e) => {
- if (e.key === 'Shift') {
- shiftKey = false;
- }
- };
- const onFocus = () => {};
- const onBlur = () => {
- shiftKey = false;
- selectedChatId = null;
- };
- onMount(async () => {
- showPinnedChat = localStorage?.showPinnedChat ? localStorage.showPinnedChat === 'true' : true;
- mobile.subscribe((value) => {
- if ($showSidebar && value) {
- showSidebar.set(false);
- }
- if ($showSidebar && !value) {
- const navElement = document.getElementsByTagName('nav')[0];
- if (navElement) {
- navElement.style['-webkit-app-region'] = 'drag';
- }
- }
- if (!$showSidebar && !value) {
- showSidebar.set(true);
- }
- });
- showSidebar.set(!$mobile ? localStorage.sidebar === 'true' : false);
- showSidebar.subscribe((value) => {
- localStorage.sidebar = value;
- // nav element is not available on the first render
- const navElement = document.getElementsByTagName('nav')[0];
- if (navElement) {
- if ($mobile) {
- if (!value) {
- navElement.style['-webkit-app-region'] = 'drag';
- } else {
- navElement.style['-webkit-app-region'] = 'no-drag';
- }
- } else {
- navElement.style['-webkit-app-region'] = 'drag';
- }
- }
- });
- chats.subscribe((value) => {
- initFolders();
- });
- await initChannels();
- await initChatList();
- window.addEventListener('keydown', onKeyDown);
- window.addEventListener('keyup', onKeyUp);
- window.addEventListener('touchstart', onTouchStart);
- window.addEventListener('touchend', onTouchEnd);
- window.addEventListener('focus', onFocus);
- window.addEventListener('blur', onBlur);
- const dropZone = document.getElementById('sidebar');
- dropZone?.addEventListener('dragover', onDragOver);
- dropZone?.addEventListener('drop', onDrop);
- dropZone?.addEventListener('dragleave', onDragLeave);
- });
- onDestroy(() => {
- window.removeEventListener('keydown', onKeyDown);
- window.removeEventListener('keyup', onKeyUp);
- window.removeEventListener('touchstart', onTouchStart);
- window.removeEventListener('touchend', onTouchEnd);
- window.removeEventListener('focus', onFocus);
- window.removeEventListener('blur', onBlur);
- const dropZone = document.getElementById('sidebar');
- dropZone?.removeEventListener('dragover', onDragOver);
- dropZone?.removeEventListener('drop', onDrop);
- dropZone?.removeEventListener('dragleave', onDragLeave);
- });
- const newChatHandler = async () => {
- selectedChatId = null;
- selectedFolder.set(null);
- if ($user?.role !== 'admin' && $user?.permissions?.chat?.temporary_enforced) {
- await temporaryChatEnabled.set(true);
- } else {
- await temporaryChatEnabled.set(false);
- }
- setTimeout(() => {
- if ($mobile) {
- showSidebar.set(false);
- }
- }, 0);
- };
- const itemClickHandler = async () => {
- selectedChatId = null;
- chatId.set('');
- if ($mobile) {
- showSidebar.set(false);
- }
- await tick();
- };
- </script>
- <ArchivedChatsModal
- bind:show={$showArchivedChats}
- onUpdate={async () => {
- await initChatList();
- }}
- />
- <ChannelModal
- bind:show={showCreateChannel}
- onSubmit={async ({ name, access_control }) => {
- const res = await createNewChannel(localStorage.token, {
- name: name,
- access_control: access_control
- }).catch((error) => {
- toast.error(`${error}`);
- return null;
- });
- if (res) {
- $socket.emit('join-channels', { auth: { token: $user?.token } });
- await initChannels();
- showCreateChannel = false;
- }
- }}
- />
- <FolderModal
- bind:show={showCreateFolderModal}
- onSubmit={async (folder) => {
- await createFolder(folder);
- showCreateFolderModal = false;
- }}
- />
- <!-- svelte-ignore a11y-no-static-element-interactions -->
- {#if $showSidebar}
- <div
- class=" {$isApp
- ? ' ml-[4.5rem] md:ml-0'
- : ''} 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"
- on:mousedown={() => {
- showSidebar.set(!$showSidebar);
- }}
- />
- {/if}
- <SearchModal
- bind:show={$showSearch}
- onClose={() => {
- if ($mobile) {
- showSidebar.set(false);
- }
- }}
- />
- <button
- id="sidebar-new-chat-button"
- class="hidden"
- on:click={() => {
- goto('/');
- newChatHandler();
- }}
- />
- {#if !$mobile && !$showSidebar}
- <div
- 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"
- id="sidebar"
- >
- <button
- class="flex flex-col flex-1 cursor-[e-resize]"
- on:click={() => {
- showSidebar.set(!$showSidebar);
- }}
- >
- <div class="pb-1.5">
- <Tooltip
- content={$showSidebar ? $i18n.t('Close Sidebar') : $i18n.t('Open Sidebar')}
- placement="right"
- >
- <button
- class=" flex rounded-lg hover:bg-gray-100 dark:hover:bg-gray-850 transition group cursor-[e-resize]"
- >
- <div class=" self-center flex items-center justify-center size-9">
- <img
- crossorigin="anonymous"
- src="{WEBUI_BASE_URL}/static/favicon.png"
- class="sidebar-new-chat-icon size-6 rounded-full group-hover:hidden"
- alt=""
- />
- <Sidebar className="size-5 hidden group-hover:flex" />
- </div>
- </button>
- </Tooltip>
- </div>
- <div>
- <div class="">
- <Tooltip content={$i18n.t('New Chat')} placement="right">
- <a
- class=" cursor-pointer flex rounded-lg hover:bg-gray-100 dark:hover:bg-gray-850 transition group"
- href="/"
- draggable="false"
- on:click={async (e) => {
- e.stopImmediatePropagation();
- e.preventDefault();
- goto('/');
- newChatHandler();
- }}
- >
- <div class=" self-center flex items-center justify-center size-9">
- <PencilSquare className="size-4.5" />
- </div>
- </a>
- </Tooltip>
- </div>
- <div class="">
- <Tooltip content={$i18n.t('Search')} placement="right">
- <button
- class=" cursor-pointer flex rounded-lg hover:bg-gray-100 dark:hover:bg-gray-850 transition group"
- on:click={(e) => {
- e.stopImmediatePropagation();
- e.preventDefault();
- showSearch.set(true);
- }}
- draggable="false"
- >
- <div class=" self-center flex items-center justify-center size-9">
- <Search className="size-4.5" />
- </div>
- </button>
- </Tooltip>
- </div>
- {#if ($config?.features?.enable_notes ?? false) && ($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true))}
- <div class="">
- <Tooltip content={$i18n.t('Notes')} placement="right">
- <a
- class=" cursor-pointer flex rounded-lg hover:bg-gray-100 dark:hover:bg-gray-850 transition group"
- href="/notes"
- on:click={async (e) => {
- e.stopImmediatePropagation();
- e.preventDefault();
- goto('/notes');
- itemClickHandler();
- }}
- draggable="false"
- >
- <div class=" self-center flex items-center justify-center size-9">
- <Note className="size-4.5" />
- </div>
- </a>
- </Tooltip>
- </div>
- {/if}
- {#if $user?.role === 'admin' || $user?.permissions?.workspace?.models || $user?.permissions?.workspace?.knowledge || $user?.permissions?.workspace?.prompts || $user?.permissions?.workspace?.tools}
- <div class="">
- <Tooltip content={$i18n.t('Workspace')} placement="right">
- <a
- class=" cursor-pointer flex rounded-lg hover:bg-gray-100 dark:hover:bg-gray-850 transition group"
- href="/workspace"
- on:click={async (e) => {
- e.stopImmediatePropagation();
- e.preventDefault();
- goto('/workspace');
- itemClickHandler();
- }}
- draggable="false"
- >
- <div class=" self-center flex items-center justify-center size-9">
- <svg
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- stroke-width="1.5"
- stroke="currentColor"
- class="size-4.5"
- >
- <path
- stroke-linecap="round"
- stroke-linejoin="round"
- 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"
- />
- </svg>
- </div>
- </a>
- </Tooltip>
- </div>
- {/if}
- </div>
- </button>
- <div>
- <div>
- <div class=" py-0.5">
- {#if $user !== undefined && $user !== null}
- <UserMenu
- role={$user?.role}
- on:show={(e) => {
- if (e.detail === 'archived-chat') {
- showArchivedChats.set(true);
- }
- }}
- >
- <div
- class=" cursor-pointer flex rounded-lg hover:bg-gray-100 dark:hover:bg-gray-850 transition group"
- >
- <div class=" self-center flex items-center justify-center size-9">
- <img
- src={$user?.profile_image_url}
- class=" size-6 object-cover rounded-full"
- alt={$i18n.t('Open User Profile Menu')}
- aria-label={$i18n.t('Open User Profile Menu')}
- />
- </div>
- </div>
- </UserMenu>
- {/if}
- </div>
- </div>
- </div>
- </div>
- {/if}
- {#if $showSidebar}
- <div
- bind:this={navElement}
- id="sidebar"
- class="h-screen max-h-[100dvh] min-h-screen select-none {$showSidebar
- ? 'bg-gray-50 dark:bg-gray-950 z-50'
- : ' bg-transparent z-0 '} {$isApp
- ? `ml-[4.5rem] md:ml-0 `
- : ' transition-all duration-300 '} shrink-0 text-gray-900 dark:text-gray-200 text-sm fixed top-0 left-0 overflow-x-hidden
- "
- transition:slide={{ duration: 200, axis: 'x' }}
- data-state={$showSidebar}
- >
- <div
- class=" my-auto flex flex-col justify-between h-screen max-h-[100dvh] w-[260px] overflow-x-hidden scrollbar-hidden z-50 {$showSidebar
- ? ''
- : 'invisible'}"
- >
- <div
- 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"
- >
- <a
- 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"
- href="/"
- draggable="false"
- on:click={newChatHandler}
- >
- <img
- crossorigin="anonymous"
- src="{WEBUI_BASE_URL}/static/favicon.png"
- class="sidebar-new-chat-icon size-6 rounded-full"
- alt=""
- />
- </a>
- <a href="/" class="flex flex-1 px-1.5" on:click={newChatHandler}>
- <div class=" self-center font-medium text-gray-850 dark:text-white font-primary">
- {$WEBUI_NAME}
- </div>
- </a>
- <Tooltip
- content={$showSidebar ? $i18n.t('Close Sidebar') : $i18n.t('Open Sidebar')}
- placement="bottom"
- >
- <button
- class=" flex rounded-lg hover:bg-gray-100 dark:hover:bg-gray-850 transition cursor-[w-resize]"
- on:click={() => {
- showSidebar.set(!$showSidebar);
- }}
- >
- <div class=" self-center p-1.5">
- <Sidebar />
- </div>
- </button>
- </Tooltip>
- </div>
- <div class="pb-1.5">
- <div class="px-[7px] flex justify-center text-gray-800 dark:text-gray-200">
- <a
- id="sidebar-new-chat-button"
- 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"
- href="/"
- draggable="false"
- on:click={newChatHandler}
- >
- <div class="self-center">
- <PencilSquare className=" size-4.5" strokeWidth="2" />
- </div>
- <div class="flex self-center translate-y-[0.5px]">
- <div class=" self-center text-sm font-primary">{$i18n.t('New Chat')}</div>
- </div>
- </a>
- </div>
- <div class="px-[7px] flex justify-center text-gray-800 dark:text-gray-200">
- <button
- 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"
- on:click={() => {
- showSearch.set(true);
- }}
- draggable="false"
- >
- <div class="self-center">
- <Search strokeWidth="2" className="size-4.5" />
- </div>
- <div class="flex self-center translate-y-[0.5px]">
- <div class=" self-center text-sm font-primary">{$i18n.t('Search')}</div>
- </div>
- </button>
- </div>
- {#if ($config?.features?.enable_notes ?? false) && ($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true))}
- <div class="px-[7px] flex justify-center text-gray-800 dark:text-gray-200">
- <a
- class="grow flex items-center space-x-3 rounded-lg px-2 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
- href="/notes"
- on:click={itemClickHandler}
- draggable="false"
- >
- <div class="self-center">
- <Note className="size-4.5" strokeWidth="2" />
- </div>
- <div class="flex self-center translate-y-[0.5px]">
- <div class=" self-center text-sm font-primary">{$i18n.t('Notes')}</div>
- </div>
- </a>
- </div>
- {/if}
- {#if $user?.role === 'admin' || $user?.permissions?.workspace?.models || $user?.permissions?.workspace?.knowledge || $user?.permissions?.workspace?.prompts || $user?.permissions?.workspace?.tools}
- <div class="px-[7px] flex justify-center text-gray-800 dark:text-gray-200">
- <a
- class="grow flex items-center space-x-3 rounded-lg px-2 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
- href="/workspace"
- on:click={itemClickHandler}
- draggable="false"
- >
- <div class="self-center">
- <svg
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- stroke-width="2"
- stroke="currentColor"
- class="size-4.5"
- >
- <path
- stroke-linecap="round"
- stroke-linejoin="round"
- 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"
- />
- </svg>
- </div>
- <div class="flex self-center translate-y-[0.5px]">
- <div class=" self-center text-sm font-primary">{$i18n.t('Workspace')}</div>
- </div>
- </a>
- </div>
- {/if}
- </div>
- <div class="relative flex flex-col flex-1">
- {#if ($models ?? []).length > 0 && ($settings?.pinnedModels ?? []).length > 0}
- <PinnedModelList bind:selectedChatId {shiftKey} />
- {/if}
- {#if $config?.features?.enable_channels && ($user?.role === 'admin' || $channels.length > 0)}
- <Folder
- className="px-2 mt-0.5"
- name={$i18n.t('Channels')}
- dragAndDrop={false}
- onAdd={async () => {
- if ($user?.role === 'admin') {
- await tick();
- setTimeout(() => {
- showCreateChannel = true;
- }, 0);
- }
- }}
- onAddLabel={$i18n.t('Create Channel')}
- >
- {#each $channels as channel}
- <ChannelItem
- {channel}
- onUpdate={async () => {
- await initChannels();
- }}
- />
- {/each}
- </Folder>
- {/if}
- <Folder
- className="px-2 mt-0.5"
- name={$i18n.t('Chats')}
- onAdd={() => {
- showCreateFolderModal = true;
- }}
- onAddLabel={$i18n.t('New Folder')}
- on:change={async (e) => {
- selectedFolder.set(null);
- await goto('/');
- }}
- on:import={(e) => {
- importChatHandler(e.detail);
- }}
- on:drop={async (e) => {
- const { type, id, item } = e.detail;
- if (type === 'chat') {
- let chat = await getChatById(localStorage.token, id).catch((error) => {
- return null;
- });
- if (!chat && item) {
- chat = await importChat(
- localStorage.token,
- item.chat,
- item?.meta ?? {},
- false,
- null,
- item?.created_at ?? null,
- item?.updated_at ?? null
- );
- }
- if (chat) {
- console.log(chat);
- if (chat.folder_id) {
- const res = await updateChatFolderIdById(localStorage.token, chat.id, null).catch(
- (error) => {
- toast.error(`${error}`);
- return null;
- }
- );
- }
- if (chat.pinned) {
- const res = await toggleChatPinnedStatusById(localStorage.token, chat.id);
- }
- initChatList();
- }
- } else if (type === 'folder') {
- if (folders[id].parent_id === null) {
- return;
- }
- const res = await updateFolderParentIdById(localStorage.token, id, null).catch(
- (error) => {
- toast.error(`${error}`);
- return null;
- }
- );
- if (res) {
- await initFolders();
- }
- }
- }}
- >
- {#if $pinnedChats.length > 0}
- <div class="flex flex-col space-y-1 rounded-xl">
- <Folder
- className=""
- bind:open={showPinnedChat}
- on:change={(e) => {
- localStorage.setItem('showPinnedChat', e.detail);
- console.log(e.detail);
- }}
- on:import={(e) => {
- importChatHandler(e.detail, true);
- }}
- on:drop={async (e) => {
- const { type, id, item } = e.detail;
- if (type === 'chat') {
- let chat = await getChatById(localStorage.token, id).catch((error) => {
- return null;
- });
- if (!chat && item) {
- chat = await importChat(
- localStorage.token,
- item.chat,
- item?.meta ?? {},
- false,
- null,
- item?.created_at ?? null,
- item?.updated_at ?? null
- );
- }
- if (chat) {
- console.log(chat);
- if (chat.folder_id) {
- const res = await updateChatFolderIdById(
- localStorage.token,
- chat.id,
- null
- ).catch((error) => {
- toast.error(`${error}`);
- return null;
- });
- }
- if (!chat.pinned) {
- const res = await toggleChatPinnedStatusById(localStorage.token, chat.id);
- }
- initChatList();
- }
- }
- }}
- name={$i18n.t('Pinned')}
- >
- <div
- class="ml-3 pl-1 mt-[1px] flex flex-col overflow-y-auto scrollbar-hidden border-s border-gray-100 dark:border-gray-900"
- >
- {#each $pinnedChats as chat, idx (`pinned-chat-${chat?.id ?? idx}`)}
- <ChatItem
- className=""
- id={chat.id}
- title={chat.title}
- {shiftKey}
- selected={selectedChatId === chat.id}
- on:select={() => {
- selectedChatId = chat.id;
- }}
- on:unselect={() => {
- selectedChatId = null;
- }}
- on:change={async () => {
- initChatList();
- }}
- on:tag={(e) => {
- const { type, name } = e.detail;
- tagEventHandler(type, name, chat.id);
- }}
- />
- {/each}
- </div>
- </Folder>
- </div>
- {/if}
- {#if folders}
- <Folders
- {folders}
- {shiftKey}
- onDelete={(folderId) => {
- selectedFolder.set(null);
- initChatList();
- }}
- on:update={() => {
- initChatList();
- }}
- on:import={(e) => {
- const { folderId, items } = e.detail;
- importChatHandler(items, false, folderId);
- }}
- on:change={async () => {
- initChatList();
- }}
- />
- {/if}
- <div class=" flex-1 flex flex-col overflow-y-auto scrollbar-hidden">
- <div class="pt-1.5">
- {#if $chats}
- {#each $chats as chat, idx (`chat-${chat?.id ?? idx}`)}
- {#if idx === 0 || (idx > 0 && chat.time_range !== $chats[idx - 1].time_range)}
- <div
- class="w-full pl-2.5 text-xs text-gray-500 dark:text-gray-500 font-medium {idx ===
- 0
- ? ''
- : 'pt-5'} pb-1.5"
- >
- {$i18n.t(chat.time_range)}
- <!-- localisation keys for time_range to be recognized from the i18next parser (so they don't get automatically removed):
- {$i18n.t('Today')}
- {$i18n.t('Yesterday')}
- {$i18n.t('Previous 7 days')}
- {$i18n.t('Previous 30 days')}
- {$i18n.t('January')}
- {$i18n.t('February')}
- {$i18n.t('March')}
- {$i18n.t('April')}
- {$i18n.t('May')}
- {$i18n.t('June')}
- {$i18n.t('July')}
- {$i18n.t('August')}
- {$i18n.t('September')}
- {$i18n.t('October')}
- {$i18n.t('November')}
- {$i18n.t('December')}
- -->
- </div>
- {/if}
- <ChatItem
- className=""
- id={chat.id}
- title={chat.title}
- {shiftKey}
- selected={selectedChatId === chat.id}
- on:select={() => {
- selectedChatId = chat.id;
- }}
- on:unselect={() => {
- selectedChatId = null;
- }}
- on:change={async () => {
- initChatList();
- }}
- on:tag={(e) => {
- const { type, name } = e.detail;
- tagEventHandler(type, name, chat.id);
- }}
- />
- {/each}
- {#if $scrollPaginationEnabled && !allChatsLoaded}
- <Loader
- on:visible={(e) => {
- if (!chatListLoading) {
- loadMoreChats();
- }
- }}
- >
- <div
- class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2"
- >
- <Spinner className=" size-4" />
- <div class=" ">Loading...</div>
- </div>
- </Loader>
- {/if}
- {:else}
- <div
- class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2"
- >
- <Spinner className=" size-4" />
- <div class=" ">Loading...</div>
- </div>
- {/if}
- </div>
- </div>
- </Folder>
- </div>
- <div class="px-1.5 pt-1.5 pb-2 sticky bottom-0 z-10 bg-gray-50 dark:bg-gray-950 sidebar">
- <div class="flex flex-col font-primary">
- {#if $user !== undefined && $user !== null}
- <UserMenu
- role={$user?.role}
- on:show={(e) => {
- if (e.detail === 'archived-chat') {
- showArchivedChats.set(true);
- }
- }}
- >
- <div
- class=" flex items-center rounded-xl py-2 px-1.5 w-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
- >
- <div class=" self-center mr-3">
- <img
- src={$user?.profile_image_url}
- class=" size-6 object-cover rounded-full"
- alt={$i18n.t('Open User Profile Menu')}
- aria-label={$i18n.t('Open User Profile Menu')}
- />
- </div>
- <div class=" self-center font-medium">{$user?.name}</div>
- </div>
- </UserMenu>
- {/if}
- </div>
- </div>
- </div>
- </div>
- {/if}
- <style>
- .scrollbar-hidden:active::-webkit-scrollbar-thumb,
- .scrollbar-hidden:focus::-webkit-scrollbar-thumb,
- .scrollbar-hidden:hover::-webkit-scrollbar-thumb {
- visibility: visible;
- }
- .scrollbar-hidden::-webkit-scrollbar-thumb {
- visibility: hidden;
- }
- </style>
|