+layout.svelte 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. <script lang="ts">
  2. import toast from 'svelte-french-toast';
  3. import { openDB, deleteDB } from 'idb';
  4. import fileSaver from 'file-saver';
  5. const { saveAs } = fileSaver;
  6. import { onMount, tick } from 'svelte';
  7. import { goto } from '$app/navigation';
  8. import { getOllamaModels, getOllamaVersion } from '$lib/apis/ollama';
  9. import { getModelfiles } from '$lib/apis/modelfiles';
  10. import { getPrompts } from '$lib/apis/prompts';
  11. import { getOpenAIModels } from '$lib/apis/openai';
  12. import { getLiteLLMModels } from '$lib/apis/litellm';
  13. import { getDocs } from '$lib/apis/documents';
  14. import { getAllChatTags } from '$lib/apis/chats';
  15. import {
  16. user,
  17. showSettings,
  18. settings,
  19. models,
  20. modelfiles,
  21. prompts,
  22. documents,
  23. tags,
  24. showChangelog,
  25. config
  26. } from '$lib/stores';
  27. import { REQUIRED_OLLAMA_VERSION, WEBUI_API_BASE_URL } from '$lib/constants';
  28. import { compareVersion } from '$lib/utils';
  29. import SettingsModal from '$lib/components/chat/SettingsModal.svelte';
  30. import Sidebar from '$lib/components/layout/Sidebar.svelte';
  31. import ShortcutsModal from '$lib/components/chat/ShortcutsModal.svelte';
  32. import ChangelogModal from '$lib/components/ChangelogModal.svelte';
  33. let ollamaVersion = '';
  34. let loaded = false;
  35. let DB = null;
  36. let localDBChats = [];
  37. let showShortcuts = false;
  38. const getModels = async () => {
  39. let models = await Promise.all([
  40. await getOllamaModels(localStorage.token).catch((error) => {
  41. console.log(error);
  42. return null;
  43. }),
  44. await getOpenAIModels(localStorage.token).catch((error) => {
  45. console.log(error);
  46. return null;
  47. }),
  48. await getLiteLLMModels(localStorage.token).catch((error) => {
  49. console.log(error);
  50. return null;
  51. })
  52. ]);
  53. models = models
  54. .filter((models) => models)
  55. .reduce((a, e, i, arr) => a.concat(e, ...(i < arr.length - 1 ? [{ name: 'hr' }] : [])), []);
  56. return models;
  57. };
  58. const setOllamaVersion = async (version: string = '') => {
  59. if (version === '') {
  60. version = await getOllamaVersion(localStorage.token).catch((error) => {
  61. return '';
  62. });
  63. }
  64. ollamaVersion = version;
  65. console.log(ollamaVersion);
  66. if (compareVersion(REQUIRED_OLLAMA_VERSION, ollamaVersion)) {
  67. toast.error(`Ollama Version: ${ollamaVersion !== '' ? ollamaVersion : 'Not Detected'}`);
  68. }
  69. };
  70. onMount(async () => {
  71. if ($user === undefined) {
  72. await goto('/auth');
  73. } else if (['user', 'admin'].includes($user.role)) {
  74. try {
  75. // Check if IndexedDB exists
  76. DB = await openDB('Chats', 1);
  77. if (DB) {
  78. const chats = await DB.getAllFromIndex('chats', 'timestamp');
  79. localDBChats = chats.map((item, idx) => chats[chats.length - 1 - idx]);
  80. if (localDBChats.length === 0) {
  81. await deleteDB('Chats');
  82. }
  83. console.log('localdb', localDBChats);
  84. }
  85. console.log(DB);
  86. } catch (error) {
  87. // IndexedDB Not Found
  88. console.log('IDB Not Found');
  89. }
  90. console.log();
  91. await models.set(await getModels());
  92. await tick();
  93. await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
  94. await modelfiles.set(await getModelfiles(localStorage.token));
  95. await prompts.set(await getPrompts(localStorage.token));
  96. await documents.set(await getDocs(localStorage.token));
  97. await tags.set(await getAllChatTags(localStorage.token));
  98. modelfiles.subscribe(async () => {
  99. // should fetch models
  100. await models.set(await getModels());
  101. });
  102. document.addEventListener('keydown', function (event) {
  103. const isCtrlPressed = event.ctrlKey || event.metaKey; // metaKey is for Cmd key on Mac
  104. // Check if the Shift key is pressed
  105. const isShiftPressed = event.shiftKey;
  106. // Check if Ctrl + Shift + O is pressed
  107. if (isCtrlPressed && isShiftPressed && event.key.toLowerCase() === 'o') {
  108. event.preventDefault();
  109. console.log('newChat');
  110. document.getElementById('sidebar-new-chat-button')?.click();
  111. }
  112. // Check if Shift + Esc is pressed
  113. if (isShiftPressed && event.key === 'Escape') {
  114. event.preventDefault();
  115. console.log('focusInput');
  116. document.getElementById('chat-textarea')?.focus();
  117. }
  118. // Check if Ctrl + Shift + ; is pressed
  119. if (isCtrlPressed && isShiftPressed && event.key === ';') {
  120. event.preventDefault();
  121. console.log('copyLastCodeBlock');
  122. const button = [...document.getElementsByClassName('copy-code-button')]?.at(-1);
  123. button?.click();
  124. }
  125. // Check if Ctrl + Shift + C is pressed
  126. if (isCtrlPressed && isShiftPressed && event.key.toLowerCase() === 'c') {
  127. event.preventDefault();
  128. console.log('copyLastResponse');
  129. const button = [...document.getElementsByClassName('copy-response-button')]?.at(-1);
  130. console.log(button);
  131. button?.click();
  132. }
  133. // Check if Ctrl + Shift + S is pressed
  134. if (isCtrlPressed && isShiftPressed && event.key.toLowerCase() === 's') {
  135. event.preventDefault();
  136. console.log('toggleSidebar');
  137. document.getElementById('sidebar-toggle-button')?.click();
  138. }
  139. // Check if Ctrl + Shift + Backspace is pressed
  140. if (isCtrlPressed && isShiftPressed && event.key === 'Backspace') {
  141. event.preventDefault();
  142. console.log('deleteChat');
  143. document.getElementById('delete-chat-button')?.click();
  144. }
  145. // Check if Ctrl + . is pressed
  146. if (isCtrlPressed && event.key === '.') {
  147. event.preventDefault();
  148. console.log('openSettings');
  149. document.getElementById('open-settings-button')?.click();
  150. }
  151. // Check if Ctrl + / is pressed
  152. if (isCtrlPressed && event.key === '/') {
  153. event.preventDefault();
  154. console.log('showShortcuts');
  155. document.getElementById('show-shortcuts-button')?.click();
  156. }
  157. });
  158. if ($user.role === 'admin') {
  159. showChangelog.set(localStorage.version !== $config.version);
  160. }
  161. await tick();
  162. }
  163. loaded = true;
  164. });
  165. </script>
  166. {#if loaded}
  167. <div class=" hidden lg:flex fixed bottom-0 right-0 px-3 py-3 z-10">
  168. <button
  169. id="show-shortcuts-button"
  170. class="text-gray-600 dark:text-gray-300 bg-gray-300/20 w-6 h-6 flex items-center justify-center text-xs rounded-full"
  171. on:click={() => {
  172. showShortcuts = !showShortcuts;
  173. }}
  174. >
  175. ?
  176. </button>
  177. </div>
  178. <ShortcutsModal bind:show={showShortcuts} />
  179. <div class="app relative">
  180. {#if !['user', 'admin'].includes($user.role)}
  181. <div class="fixed w-full h-full flex z-50">
  182. <div
  183. class="absolute w-full h-full backdrop-blur-md bg-white/20 dark:bg-gray-900/50 flex justify-center"
  184. >
  185. <div class="m-auto pb-44 flex flex-col justify-center">
  186. <div class="max-w-md">
  187. <div class="text-center dark:text-white text-2xl font-medium z-50">
  188. Account Activation Pending<br /> Contact Admin for WebUI Access
  189. </div>
  190. <div class=" mt-4 text-center text-sm dark:text-gray-200 w-full">
  191. Your account status is currently pending activation. To access the WebUI, please
  192. reach out to the administrator. Admins can manage user statuses from the Admin
  193. Panel.
  194. </div>
  195. <div class=" mt-6 mx-auto relative group w-fit">
  196. <button
  197. class="relative z-20 flex px-5 py-2 rounded-full bg-white border border-gray-100 dark:border-none hover:bg-gray-100 transition font-medium text-sm"
  198. on:click={async () => {
  199. location.href = '/';
  200. }}
  201. >
  202. Check Again
  203. </button>
  204. <button
  205. class="text-xs text-center w-full mt-2 text-gray-400 underline"
  206. on:click={async () => {
  207. localStorage.removeItem('token');
  208. location.href = '/auth';
  209. }}>Sign Out</button
  210. >
  211. </div>
  212. </div>
  213. </div>
  214. </div>
  215. </div>
  216. {:else if localDBChats.length > 0}
  217. <div class="fixed w-full h-full flex z-50">
  218. <div
  219. class="absolute w-full h-full backdrop-blur-md bg-white/20 dark:bg-gray-900/50 flex justify-center"
  220. >
  221. <div class="m-auto pb-44 flex flex-col justify-center">
  222. <div class="max-w-md">
  223. <div class="text-center dark:text-white text-2xl font-medium z-50">
  224. Important Update<br /> Action Required for Chat Log Storage
  225. </div>
  226. <div class=" mt-4 text-center text-sm dark:text-gray-200 w-full">
  227. Saving chat logs directly to your browser's storage is no longer supported. Please
  228. take a moment to download and delete your chat logs by clicking the button below.
  229. Don't worry, you can easily re-import your chat logs to the backend through <span
  230. class="font-semibold dark:text-white">Settings > Chats > Import Chats</span
  231. >. This ensures that your valuable conversations are securely saved to your backend
  232. database. Thank you!
  233. </div>
  234. <div class=" mt-6 mx-auto relative group w-fit">
  235. <button
  236. class="relative z-20 flex px-5 py-2 rounded-full bg-white border border-gray-100 dark:border-none hover:bg-gray-100 transition font-medium text-sm"
  237. on:click={async () => {
  238. let blob = new Blob([JSON.stringify(localDBChats)], {
  239. type: 'application/json'
  240. });
  241. saveAs(blob, `chat-export-${Date.now()}.json`);
  242. const tx = DB.transaction('chats', 'readwrite');
  243. await Promise.all([tx.store.clear(), tx.done]);
  244. await deleteDB('Chats');
  245. localDBChats = [];
  246. }}
  247. >
  248. Download & Delete
  249. </button>
  250. <button
  251. class="text-xs text-center w-full mt-2 text-gray-400 underline"
  252. on:click={async () => {
  253. localDBChats = [];
  254. }}>Close</button
  255. >
  256. </div>
  257. </div>
  258. </div>
  259. </div>
  260. </div>
  261. {/if}
  262. <div
  263. class=" text-gray-700 dark:text-gray-100 bg-white dark:bg-gray-900 min-h-screen overflow-auto flex flex-row"
  264. >
  265. <Sidebar />
  266. <SettingsModal bind:show={$showSettings} />
  267. <ChangelogModal bind:show={$showChangelog} />
  268. <slot />
  269. </div>
  270. </div>
  271. {/if}
  272. <style>
  273. .loading {
  274. display: inline-block;
  275. clip-path: inset(0 1ch 0 0);
  276. animation: l 1s steps(3) infinite;
  277. letter-spacing: -0.5px;
  278. }
  279. @keyframes l {
  280. to {
  281. clip-path: inset(0 -1ch 0 0);
  282. }
  283. }
  284. pre[class*='language-'] {
  285. position: relative;
  286. overflow: auto;
  287. /* make space */
  288. margin: 5px 0;
  289. padding: 1.75rem 0 1.75rem 1rem;
  290. border-radius: 10px;
  291. }
  292. pre[class*='language-'] button {
  293. position: absolute;
  294. top: 5px;
  295. right: 5px;
  296. font-size: 0.9rem;
  297. padding: 0.15rem;
  298. background-color: #828282;
  299. border: ridge 1px #7b7b7c;
  300. border-radius: 5px;
  301. text-shadow: #c4c4c4 0 0 2px;
  302. }
  303. pre[class*='language-'] button:hover {
  304. cursor: pointer;
  305. background-color: #bcbabb;
  306. }
  307. </style>