Navbar.svelte 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. <script lang="ts">
  2. import { getContext } from 'svelte';
  3. import { toast } from 'svelte-sonner';
  4. import {
  5. WEBUI_NAME,
  6. banners,
  7. chatId,
  8. config,
  9. mobile,
  10. settings,
  11. showArchivedChats,
  12. showControls,
  13. showSidebar,
  14. temporaryChatEnabled,
  15. user
  16. } from '$lib/stores';
  17. import { slide } from 'svelte/transition';
  18. import { page } from '$app/stores';
  19. import { goto } from '$app/navigation';
  20. import ShareChatModal from '../chat/ShareChatModal.svelte';
  21. import ModelSelector from '../chat/ModelSelector.svelte';
  22. import Tooltip from '../common/Tooltip.svelte';
  23. import Menu from '$lib/components/layout/Navbar/Menu.svelte';
  24. import UserMenu from '$lib/components/layout/Sidebar/UserMenu.svelte';
  25. import AdjustmentsHorizontal from '../icons/AdjustmentsHorizontal.svelte';
  26. import PencilSquare from '../icons/PencilSquare.svelte';
  27. import Banner from '../common/Banner.svelte';
  28. import Sidebar from '../icons/Sidebar.svelte';
  29. import ChatBubbleDotted from '../icons/ChatBubbleDotted.svelte';
  30. import ChatBubbleDottedChecked from '../icons/ChatBubbleDottedChecked.svelte';
  31. import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
  32. import ChatPlus from '../icons/ChatPlus.svelte';
  33. const i18n = getContext('i18n');
  34. export let initNewChat: Function;
  35. export let title: string = $WEBUI_NAME;
  36. export let shareEnabled: boolean = false;
  37. export let chat;
  38. export let history;
  39. export let selectedModels;
  40. export let showModelSelector = true;
  41. export let showBanners = true;
  42. export let onSaveTempChat: () => {};
  43. export let archiveChatHandler: (id: string) => void;
  44. export let moveChatHandler: (id: string, folderId: string) => void;
  45. let closedBannerIds = [];
  46. let showShareChatModal = false;
  47. let showDownloadChatModal = false;
  48. </script>
  49. <ShareChatModal bind:show={showShareChatModal} chatId={$chatId} />
  50. <button
  51. id="new-chat-button"
  52. class="hidden"
  53. on:click={() => {
  54. initNewChat();
  55. }}
  56. aria-label="New Chat"
  57. />
  58. <nav class="sticky top-0 z-30 w-full py-1 -mb-8 flex flex-col items-center drag-region">
  59. <div class="flex items-center w-full pl-1.5 pr-1">
  60. <div
  61. class=" bg-linear-to-b via-50% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1]"
  62. ></div>
  63. <div class=" flex max-w-full w-full mx-auto px-1.5 md:px-2 pt-0.5 bg-transparent">
  64. <div class="flex items-center w-full max-w-full">
  65. {#if $mobile && !$showSidebar}
  66. <div
  67. class="-translate-x-0.5 mr-1 mt-1 self-start flex flex-none items-center text-gray-600 dark:text-gray-400"
  68. >
  69. <Tooltip content={$showSidebar ? $i18n.t('Close Sidebar') : $i18n.t('Open Sidebar')}>
  70. <button
  71. class=" cursor-pointer flex rounded-lg hover:bg-gray-100 dark:hover:bg-gray-850 transition"
  72. on:click={() => {
  73. showSidebar.set(!$showSidebar);
  74. }}
  75. >
  76. <div class=" self-center p-1.5">
  77. <Sidebar />
  78. </div>
  79. </button>
  80. </Tooltip>
  81. </div>
  82. {/if}
  83. <div
  84. class="flex-1 overflow-hidden max-w-full py-0.5
  85. {$showSidebar ? 'ml-1' : ''}
  86. "
  87. >
  88. {#if showModelSelector}
  89. <ModelSelector bind:selectedModels showSetDefault={!shareEnabled} />
  90. {/if}
  91. </div>
  92. <div class="self-start flex flex-none items-center text-gray-600 dark:text-gray-400">
  93. <!-- <div class="md:hidden flex self-center w-[1px] h-5 mx-2 bg-gray-300 dark:bg-stone-700" /> -->
  94. {#if $user?.role === 'user' ? ($user?.permissions?.chat?.temporary ?? true) && !($user?.permissions?.chat?.temporary_enforced ?? false) : true}
  95. {#if !chat?.id}
  96. <Tooltip content={$i18n.t(`Temporary Chat`)}>
  97. <button
  98. class="flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
  99. id="temporary-chat-button"
  100. on:click={async () => {
  101. if (($settings?.temporaryChatByDefault ?? false) && $temporaryChatEnabled) {
  102. // for proper initNewChat handling
  103. await temporaryChatEnabled.set(null);
  104. } else {
  105. await temporaryChatEnabled.set(!$temporaryChatEnabled);
  106. }
  107. await goto('/');
  108. // add 'temporary-chat=true' to the URL
  109. if ($temporaryChatEnabled) {
  110. window.history.replaceState(null, '', '?temporary-chat=true');
  111. } else {
  112. window.history.replaceState(null, '', location.pathname);
  113. }
  114. }}
  115. >
  116. <div class=" m-auto self-center">
  117. {#if $temporaryChatEnabled}
  118. <ChatBubbleDottedChecked className=" size-4.5" strokeWidth="1.5" />
  119. {:else}
  120. <ChatBubbleDotted className=" size-4.5" strokeWidth="1.5" />
  121. {/if}
  122. </div>
  123. </button>
  124. </Tooltip>
  125. {:else if $temporaryChatEnabled}
  126. <Tooltip content={$i18n.t(`Save Chat`)}>
  127. <button
  128. class="flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
  129. id="save-temporary-chat-button"
  130. on:click={async () => {
  131. onSaveTempChat();
  132. }}
  133. >
  134. <div class=" m-auto self-center">
  135. <ChatPlus className=" size-4.5" strokeWidth="1.5" />
  136. </div>
  137. </button>
  138. </Tooltip>
  139. {/if}
  140. {/if}
  141. {#if shareEnabled && chat && (chat.id || $temporaryChatEnabled)}
  142. <Menu
  143. {chat}
  144. {shareEnabled}
  145. shareHandler={() => {
  146. showShareChatModal = !showShareChatModal;
  147. }}
  148. archiveChatHandler={() => {
  149. archiveChatHandler(chat.id);
  150. }}
  151. {moveChatHandler}
  152. >
  153. <button
  154. class="flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
  155. id="chat-context-menu-button"
  156. >
  157. <div class=" m-auto self-center">
  158. <EllipsisHorizontal className=" size-5" strokeWidth="1.5" />
  159. </div>
  160. </button>
  161. </Menu>
  162. {/if}
  163. {#if $user?.role === 'admin' || ($user?.permissions.chat?.controls ?? true)}
  164. <Tooltip content={$i18n.t('Controls')}>
  165. <button
  166. class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
  167. on:click={async () => {
  168. await showControls.set(!$showControls);
  169. }}
  170. aria-label="Controls"
  171. >
  172. <div class=" m-auto self-center">
  173. <AdjustmentsHorizontal className=" size-5" strokeWidth="1" />
  174. </div>
  175. </button>
  176. </Tooltip>
  177. {/if}
  178. {#if $mobile}
  179. <Tooltip content={$i18n.t('New Chat')}>
  180. <button
  181. class=" flex {$showSidebar
  182. ? 'md:hidden'
  183. : ''} cursor-pointer px-2 py-2 rounded-xl text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-850 transition"
  184. on:click={() => {
  185. initNewChat();
  186. }}
  187. aria-label="New Chat"
  188. >
  189. <div class=" m-auto self-center">
  190. <PencilSquare className=" size-5" strokeWidth="2" />
  191. </div>
  192. </button>
  193. </Tooltip>
  194. {/if}
  195. {#if $user !== undefined && $user !== null}
  196. <UserMenu
  197. className="max-w-[240px]"
  198. role={$user?.role}
  199. help={true}
  200. on:show={(e) => {
  201. if (e.detail === 'archived-chat') {
  202. showArchivedChats.set(true);
  203. }
  204. }}
  205. >
  206. <div
  207. class="select-none flex rounded-xl p-1.5 w-full hover:bg-gray-50 dark:hover:bg-gray-850 transition"
  208. >
  209. <div class=" self-center">
  210. <span class="sr-only">{$i18n.t('User menu')}</span>
  211. <img
  212. src={$user?.profile_image_url}
  213. class="size-6 object-cover rounded-full"
  214. alt=""
  215. draggable="false"
  216. />
  217. </div>
  218. </div>
  219. </UserMenu>
  220. {/if}
  221. </div>
  222. </div>
  223. </div>
  224. </div>
  225. {#if $temporaryChatEnabled && $chatId === 'local'}
  226. <div class=" w-full z-30 text-center">
  227. <div class="text-xs text-gray-500">{$i18n.t('Temporary Chat')}</div>
  228. </div>
  229. {/if}
  230. <div class="absolute top-[100%] left-0 right-0 h-fit">
  231. {#if !history.currentId && !$chatId && ($banners.length > 0 || ($config?.license_metadata?.type ?? null) === 'trial' || (($config?.license_metadata?.seats ?? null) !== null && $config?.user_count > $config?.license_metadata?.seats))}
  232. <div class=" w-full z-30 mt-5">
  233. <div class=" flex flex-col gap-1 w-full">
  234. {#if ($config?.license_metadata?.type ?? null) === 'trial'}
  235. <Banner
  236. banner={{
  237. type: 'info',
  238. title: 'Trial License',
  239. content: $i18n.t(
  240. 'You are currently using a trial license. Please contact support to upgrade your license.'
  241. )
  242. }}
  243. />
  244. {/if}
  245. {#if ($config?.license_metadata?.seats ?? null) !== null && $config?.user_count > $config?.license_metadata?.seats}
  246. <Banner
  247. banner={{
  248. type: 'error',
  249. title: 'License Error',
  250. content: $i18n.t(
  251. 'Exceeded the number of seats in your license. Please contact support to increase the number of seats.'
  252. )
  253. }}
  254. />
  255. {/if}
  256. {#if showBanners}
  257. {#each $banners.filter((b) => ![...JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]'), ...closedBannerIds].includes(b.id)) as banner (banner.id)}
  258. <Banner
  259. {banner}
  260. on:dismiss={(e) => {
  261. const bannerId = e.detail;
  262. if (banner.dismissible) {
  263. localStorage.setItem(
  264. 'dismissedBannerIds',
  265. JSON.stringify(
  266. [
  267. bannerId,
  268. ...JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]')
  269. ].filter((id) => $banners.find((b) => b.id === id))
  270. )
  271. );
  272. } else {
  273. closedBannerIds = [...closedBannerIds, bannerId];
  274. }
  275. }}
  276. />
  277. {/each}
  278. {/if}
  279. </div>
  280. </div>
  281. {/if}
  282. </div>
  283. </nav>