+layout.svelte 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. <script lang="ts">
  2. import { toast } from 'svelte-sonner';
  3. import { onMount, tick, getContext } from 'svelte';
  4. import { openDB, deleteDB } from 'idb';
  5. import fileSaver from 'file-saver';
  6. const { saveAs } = fileSaver;
  7. import { goto } from '$app/navigation';
  8. import { getModels as _getModels } from '$lib/apis';
  9. import { getAllChatTags } from '$lib/apis/chats';
  10. import { getPrompts } from '$lib/apis/prompts';
  11. import { getDocs } from '$lib/apis/documents';
  12. import { getTools } from '$lib/apis/tools';
  13. import { getBanners } from '$lib/apis/configs';
  14. import { getUserSettings } from '$lib/apis/users';
  15. import {
  16. user,
  17. showSettings,
  18. settings,
  19. models,
  20. prompts,
  21. documents,
  22. tags,
  23. banners,
  24. showChangelog,
  25. config,
  26. showCallOverlay,
  27. tools
  28. } from '$lib/stores';
  29. import SettingsModal from '$lib/components/chat/SettingsModal.svelte';
  30. import Sidebar from '$lib/components/layout/Sidebar.svelte';
  31. import ChangelogModal from '$lib/components/ChangelogModal.svelte';
  32. import AccountPending from '$lib/components/layout/Overlay/AccountPending.svelte';
  33. const i18n = getContext('i18n');
  34. let loaded = false;
  35. let DB = null;
  36. let localDBChats = [];
  37. const getModels = async () => {
  38. return _getModels(localStorage.token);
  39. };
  40. onMount(async () => {
  41. if ($user === undefined) {
  42. await goto('/auth');
  43. } else if (['user', 'admin'].includes($user.role)) {
  44. try {
  45. // Check if IndexedDB exists
  46. DB = await openDB('Chats', 1);
  47. if (DB) {
  48. const chats = await DB.getAllFromIndex('chats', 'timestamp');
  49. localDBChats = chats.map((item, idx) => chats[chats.length - 1 - idx]);
  50. if (localDBChats.length === 0) {
  51. await deleteDB('Chats');
  52. }
  53. }
  54. console.log(DB);
  55. } catch (error) {
  56. // IndexedDB Not Found
  57. }
  58. const userSettings = await getUserSettings(localStorage.token).catch((error) => {
  59. console.error(error);
  60. return null;
  61. });
  62. if (userSettings) {
  63. await settings.set(userSettings.ui);
  64. } else {
  65. await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
  66. }
  67. await Promise.all([
  68. (async () => {
  69. models.set(await getModels());
  70. })(),
  71. (async () => {
  72. prompts.set(await getPrompts(localStorage.token));
  73. })(),
  74. (async () => {
  75. documents.set(await getDocs(localStorage.token));
  76. })(),
  77. (async () => {
  78. tools.set(await getTools(localStorage.token));
  79. })(),
  80. (async () => {
  81. banners.set(await getBanners(localStorage.token));
  82. })(),
  83. (async () => {
  84. tags.set(await getAllChatTags(localStorage.token));
  85. })()
  86. ]);
  87. document.addEventListener('keydown', function (event) {
  88. const isCtrlPressed = event.ctrlKey || event.metaKey; // metaKey is for Cmd key on Mac
  89. // Check if the Shift key is pressed
  90. const isShiftPressed = event.shiftKey;
  91. // Check if Ctrl + Shift + O is pressed
  92. if (isCtrlPressed && isShiftPressed && event.key.toLowerCase() === 'o') {
  93. event.preventDefault();
  94. console.log('newChat');
  95. document.getElementById('sidebar-new-chat-button')?.click();
  96. }
  97. // Check if Shift + Esc is pressed
  98. if (isShiftPressed && event.key === 'Escape') {
  99. event.preventDefault();
  100. console.log('focusInput');
  101. document.getElementById('chat-textarea')?.focus();
  102. }
  103. // Check if Ctrl + Shift + ; is pressed
  104. if (isCtrlPressed && isShiftPressed && event.key === ';') {
  105. event.preventDefault();
  106. console.log('copyLastCodeBlock');
  107. const button = [...document.getElementsByClassName('copy-code-button')]?.at(-1);
  108. button?.click();
  109. }
  110. // Check if Ctrl + Shift + C is pressed
  111. if (isCtrlPressed && isShiftPressed && event.key.toLowerCase() === 'c') {
  112. event.preventDefault();
  113. console.log('copyLastResponse');
  114. const button = [...document.getElementsByClassName('copy-response-button')]?.at(-1);
  115. console.log(button);
  116. button?.click();
  117. }
  118. // Check if Ctrl + Shift + S is pressed
  119. if (isCtrlPressed && isShiftPressed && event.key.toLowerCase() === 's') {
  120. event.preventDefault();
  121. console.log('toggleSidebar');
  122. document.getElementById('sidebar-toggle-button')?.click();
  123. }
  124. // Check if Ctrl + Shift + Backspace is pressed
  125. if (isCtrlPressed && isShiftPressed && event.key === 'Backspace') {
  126. event.preventDefault();
  127. console.log('deleteChat');
  128. document.getElementById('delete-chat-button')?.click();
  129. }
  130. // Check if Ctrl + . is pressed
  131. if (isCtrlPressed && event.key === '.') {
  132. event.preventDefault();
  133. console.log('openSettings');
  134. showSettings.set(!$showSettings);
  135. }
  136. // Check if Ctrl + / is pressed
  137. if (isCtrlPressed && event.key === '/') {
  138. event.preventDefault();
  139. console.log('showShortcuts');
  140. document.getElementById('show-shortcuts-button')?.click();
  141. }
  142. });
  143. if ($user.role === 'admin') {
  144. showChangelog.set(localStorage.version !== $config.version);
  145. }
  146. await tick();
  147. }
  148. loaded = true;
  149. });
  150. </script>
  151. <SettingsModal bind:show={$showSettings} />
  152. <ChangelogModal bind:show={$showChangelog} />
  153. <div class="app relative">
  154. <div
  155. class=" text-gray-700 dark:text-gray-100 bg-white dark:bg-gray-900 h-screen max-h-[100dvh] overflow-auto flex flex-row"
  156. >
  157. {#if loaded}
  158. {#if !['user', 'admin'].includes($user.role)}
  159. <AccountPending />
  160. {:else if localDBChats.length > 0}
  161. <div class="fixed w-full h-full flex z-50">
  162. <div
  163. class="absolute w-full h-full backdrop-blur-md bg-white/20 dark:bg-gray-900/50 flex justify-center"
  164. >
  165. <div class="m-auto pb-44 flex flex-col justify-center">
  166. <div class="max-w-md">
  167. <div class="text-center dark:text-white text-2xl font-medium z-50">
  168. Important Update<br /> Action Required for Chat Log Storage
  169. </div>
  170. <div class=" mt-4 text-center text-sm dark:text-gray-200 w-full">
  171. {$i18n.t(
  172. "Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through"
  173. )}
  174. <span class="font-semibold dark:text-white"
  175. >{$i18n.t('Settings')} > {$i18n.t('Chats')} > {$i18n.t('Import Chats')}</span
  176. >. {$i18n.t(
  177. 'This ensures that your valuable conversations are securely saved to your backend database. Thank you!'
  178. )}
  179. </div>
  180. <div class=" mt-6 mx-auto relative group w-fit">
  181. <button
  182. 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"
  183. on:click={async () => {
  184. let blob = new Blob([JSON.stringify(localDBChats)], {
  185. type: 'application/json'
  186. });
  187. saveAs(blob, `chat-export-${Date.now()}.json`);
  188. const tx = DB.transaction('chats', 'readwrite');
  189. await Promise.all([tx.store.clear(), tx.done]);
  190. await deleteDB('Chats');
  191. localDBChats = [];
  192. }}
  193. >
  194. Download & Delete
  195. </button>
  196. <button
  197. class="text-xs text-center w-full mt-2 text-gray-400 underline"
  198. on:click={async () => {
  199. localDBChats = [];
  200. }}>{$i18n.t('Close')}</button
  201. >
  202. </div>
  203. </div>
  204. </div>
  205. </div>
  206. </div>
  207. {/if}
  208. <Sidebar />
  209. <slot />
  210. {/if}
  211. </div>
  212. </div>
  213. <style>
  214. .loading {
  215. display: inline-block;
  216. clip-path: inset(0 1ch 0 0);
  217. animation: l 1s steps(3) infinite;
  218. letter-spacing: -0.5px;
  219. }
  220. @keyframes l {
  221. to {
  222. clip-path: inset(0 -1ch 0 0);
  223. }
  224. }
  225. pre[class*='language-'] {
  226. position: relative;
  227. overflow: auto;
  228. /* make space */
  229. margin: 5px 0;
  230. padding: 1.75rem 0 1.75rem 1rem;
  231. border-radius: 10px;
  232. }
  233. pre[class*='language-'] button {
  234. position: absolute;
  235. top: 5px;
  236. right: 5px;
  237. font-size: 0.9rem;
  238. padding: 0.15rem;
  239. background-color: #828282;
  240. border: ridge 1px #7b7b7c;
  241. border-radius: 5px;
  242. text-shadow: #c4c4c4 0 0 2px;
  243. }
  244. pre[class*='language-'] button:hover {
  245. cursor: pointer;
  246. background-color: #bcbabb;
  247. }
  248. </style>