1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219 |
- <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,
- folders as _folders,
- 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 scrollTop = 0;
- 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 folderRegistry = {};
- let newFolderId = null;
- const initFolders = async () => {
- const folderList = await getFolders(localStorage.token).catch((error) => {
- toast.error(`${error}`);
- return [];
- });
- _folders.set(folderList);
- 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(async (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';
- }
- }
- if (!value) {
- await initChannels();
- await initChatList();
- }
- });
- 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();
- };
- const isWindows = /Windows/i.test(navigator.userAgent);
- </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 hover:bg-gray-50/50 dark:hover:bg-gray-950/50 h-full border-e border-gray-50 dark:border-gray-850 z-10 transition-all"
- id="sidebar"
- >
- <button
- class="flex flex-col flex-1 {isWindows ? 'cursor-pointer' : 'cursor-[e-resize]'}"
- on:click={async () => {
- 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-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition group {isWindows
- ? 'cursor-pointer'
- : 'cursor-[e-resize]'}"
- aria-label={$showSidebar ? $i18n.t('Close Sidebar') : $i18n.t('Open Sidebar')}
- >
- <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-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition group"
- href="/"
- draggable="false"
- on:click={async (e) => {
- e.stopImmediatePropagation();
- e.preventDefault();
- goto('/');
- newChatHandler();
- }}
- aria-label={$i18n.t('New Chat')}
- >
- <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-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition group"
- on:click={(e) => {
- e.stopImmediatePropagation();
- e.preventDefault();
- showSearch.set(true);
- }}
- draggable="false"
- aria-label={$i18n.t('Search')}
- >
- <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-xl 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"
- aria-label={$i18n.t('Notes')}
- >
- <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-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition group"
- href="/workspace"
- on:click={async (e) => {
- e.stopImmediatePropagation();
- e.preventDefault();
- goto('/workspace');
- itemClickHandler();
- }}
- aria-label={$i18n.t('Workspace')}
- 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-xl 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: 250, 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-2 pt-2 pb-1.5 flex justify-between space-x-1 text-gray-600 dark:text-gray-400 sticky top-0 z-10 -mb-3"
- >
- <a
- class="flex items-center rounded-xl size-8.5 h-full justify-center hover:bg-gray-100/50 dark:hover:bg-gray-850/50 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-xl size-8.5 justify-center items-center hover:bg-gray-100/50 dark:hover:bg-gray-850/50 transition {isWindows
- ? 'cursor-pointer'
- : 'cursor-[w-resize]'}"
- on:click={() => {
- showSidebar.set(!$showSidebar);
- }}
- aria-label={$showSidebar ? $i18n.t('Close Sidebar') : $i18n.t('Open Sidebar')}
- >
- <div class=" self-center p-1.5">
- <Sidebar />
- </div>
- </button>
- </Tooltip>
- <div
- class="{scrollTop > 0
- ? 'visible'
- : 'invisible'} sidebar-bg-gradient-to-b bg-linear-to-b from-gray-50 dark:from-gray-950 to-transparent from-50% pointer-events-none absolute inset-0 -z-10 -mb-6"
- ></div>
- </div>
- <div
- class="relative flex flex-col flex-1 overflow-y-auto scrollbar-hidden pt-3 pb-3"
- on:scroll={(e) => {
- if (e.target.scrollTop === 0) {
- scrollTop = 0;
- } else {
- scrollTop = e.target.scrollTop;
- }
- }}
- >
- <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-2xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition outline-none"
- href="/"
- draggable="false"
- on:click={newChatHandler}
- aria-label={$i18n.t('New Chat')}
- >
- <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
- id="sidebar-search-button"
- class="grow flex items-center space-x-3 rounded-2xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition outline-none"
- on:click={() => {
- showSearch.set(true);
- }}
- draggable="false"
- aria-label={$i18n.t('Search')}
- >
- <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
- id="sidebar-notes-button"
- class="grow flex items-center space-x-3 rounded-2xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
- href="/notes"
- on:click={itemClickHandler}
- draggable="false"
- aria-label={$i18n.t('Notes')}
- >
- <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
- id="sidebar-workspace-button"
- class="grow flex items-center space-x-3 rounded-2xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
- href="/workspace"
- on:click={itemClickHandler}
- draggable="false"
- aria-label={$i18n.t('Workspace')}
- >
- <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>
- {#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')}
- chevron={false}
- 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}
- {#if folders}
- <Folder
- className="px-2 mt-0.5"
- name={$i18n.t('Folders')}
- chevron={false}
- onAdd={() => {
- showCreateFolderModal = true;
- }}
- onAddLabel={$i18n.t('New Folder')}
- on:drop={async (e) => {
- const { type, id, item } = e.detail;
- 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();
- }
- }
- }}
- >
- <Folders
- bind:folderRegistry
- {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();
- }}
- />
- </Folder>
- {/if}
- <Folder
- className="px-2 mt-0.5"
- name={$i18n.t('Chats')}
- chevron={false}
- 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;
- }
- );
- folderRegistry[chat.folder_id]?.setFolderItems();
- }
- 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="mb-1">
- <div class="flex flex-col space-y-1 rounded-xl">
- <Folder
- buttonClassName=" text-gray-500"
- 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 text-gray-900 dark:text-gray-200"
- >
- {#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>
- </div>
- {/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=" ">{$i18n.t('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=" ">{$i18n.t('Loading...')}</div>
- </div>
- {/if}
- </div>
- </div>
- </Folder>
- </div>
- <div class="px-1.5 pt-1.5 pb-2 sticky bottom-0 z-10 -mt-3 sidebar">
- <div
- class=" sidebar-bg-gradient-to-t bg-linear-to-t from-gray-50 dark:from-gray-950 to-transparent from-50% pointer-events-none absolute inset-0 -z-10 -mt-6"
- ></div>
- <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-2xl py-2 px-1.5 w-full hover:bg-gray-100/50 dark:hover:bg-gray-900/50 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}
|