+layout.svelte 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. <script lang="ts">
  2. import { v4 as uuidv4 } from 'uuid';
  3. import { openDB, deleteDB } from 'idb';
  4. import { onMount, tick } from 'svelte';
  5. import { goto } from '$app/navigation';
  6. import {
  7. config,
  8. info,
  9. user,
  10. showSettings,
  11. settings,
  12. models,
  13. db,
  14. chats,
  15. chatId,
  16. modelfiles
  17. } from '$lib/stores';
  18. import SettingsModal from '$lib/components/chat/SettingsModal.svelte';
  19. import Sidebar from '$lib/components/layout/Sidebar.svelte';
  20. import toast from 'svelte-french-toast';
  21. import { OLLAMA_API_BASE_URL, WEBUI_API_BASE_URL } from '$lib/constants';
  22. let requiredOllamaVersion = '0.1.16';
  23. let loaded = false;
  24. const getModels = async () => {
  25. let models = [];
  26. const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/tags`, {
  27. method: 'GET',
  28. headers: {
  29. Accept: 'application/json',
  30. 'Content-Type': 'application/json',
  31. ...($settings.authHeader && { Authorization: $settings.authHeader }),
  32. ...($user && { Authorization: `Bearer ${localStorage.token}` })
  33. }
  34. })
  35. .then(async (res) => {
  36. if (!res.ok) throw await res.json();
  37. return res.json();
  38. })
  39. .catch((error) => {
  40. console.log(error);
  41. if ('detail' in error) {
  42. toast.error(error.detail);
  43. } else {
  44. toast.error('Server connection failed');
  45. }
  46. return null;
  47. });
  48. console.log(res);
  49. models.push(...(res?.models ?? []));
  50. // If OpenAI API Key exists
  51. if ($settings.OPENAI_API_KEY) {
  52. // Validate OPENAI_API_KEY
  53. const openaiModelRes = await fetch(`https://api.openai.com/v1/models`, {
  54. method: 'GET',
  55. headers: {
  56. 'Content-Type': 'application/json',
  57. Authorization: `Bearer ${$settings.OPENAI_API_KEY}`
  58. }
  59. })
  60. .then(async (res) => {
  61. if (!res.ok) throw await res.json();
  62. return res.json();
  63. })
  64. .catch((error) => {
  65. console.log(error);
  66. toast.error(`OpenAI: ${error?.error?.message ?? 'Network Problem'}`);
  67. return null;
  68. });
  69. const openAIModels = openaiModelRes?.data ?? null;
  70. models.push(
  71. ...(openAIModels
  72. ? [
  73. { name: 'hr' },
  74. ...openAIModels
  75. .map((model) => ({ name: model.id, label: 'OpenAI' }))
  76. .filter((model) => model.name.includes('gpt'))
  77. ]
  78. : [])
  79. );
  80. }
  81. return models;
  82. };
  83. const getDB = async () => {
  84. const DB = await openDB('Chats', 1, {
  85. upgrade(db) {
  86. const store = db.createObjectStore('chats', {
  87. keyPath: 'id',
  88. autoIncrement: true
  89. });
  90. store.createIndex('timestamp', 'timestamp');
  91. }
  92. });
  93. return {
  94. db: DB,
  95. getChatById: async function (id) {
  96. return await this.db.get('chats', id);
  97. },
  98. getChats: async function () {
  99. let chats = await this.db.getAllFromIndex('chats', 'timestamp');
  100. chats = chats.map((item, idx) => ({
  101. title: chats[chats.length - 1 - idx].title,
  102. id: chats[chats.length - 1 - idx].id
  103. }));
  104. return chats;
  105. },
  106. exportChats: async function () {
  107. let chats = await this.db.getAllFromIndex('chats', 'timestamp');
  108. chats = chats.map((item, idx) => chats[chats.length - 1 - idx]);
  109. return chats;
  110. },
  111. addChats: async function (_chats) {
  112. for (const chat of _chats) {
  113. console.log(chat);
  114. await this.addChat(chat);
  115. }
  116. await chats.set(await this.getChats());
  117. },
  118. addChat: async function (chat) {
  119. await this.db.put('chats', {
  120. ...chat
  121. });
  122. },
  123. createNewChat: async function (chat) {
  124. await this.addChat({ ...chat, timestamp: Date.now() });
  125. await chats.set(await this.getChats());
  126. },
  127. updateChatById: async function (id, updated) {
  128. const chat = await this.getChatById(id);
  129. await this.db.put('chats', {
  130. ...chat,
  131. ...updated,
  132. timestamp: Date.now()
  133. });
  134. await chats.set(await this.getChats());
  135. },
  136. deleteChatById: async function (id) {
  137. if ($chatId === id) {
  138. goto('/');
  139. await chatId.set(uuidv4());
  140. }
  141. await this.db.delete('chats', id);
  142. await chats.set(await this.getChats());
  143. },
  144. deleteAllChat: async function () {
  145. const tx = this.db.transaction('chats', 'readwrite');
  146. await Promise.all([tx.store.clear(), tx.done]);
  147. await chats.set(await this.getChats());
  148. }
  149. };
  150. };
  151. const getOllamaVersion = async () => {
  152. const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/version`, {
  153. method: 'GET',
  154. headers: {
  155. Accept: 'application/json',
  156. 'Content-Type': 'application/json',
  157. ...($settings.authHeader && { Authorization: $settings.authHeader }),
  158. ...($user && { Authorization: `Bearer ${localStorage.token}` })
  159. }
  160. })
  161. .then(async (res) => {
  162. if (!res.ok) throw await res.json();
  163. return res.json();
  164. })
  165. .catch((error) => {
  166. console.log(error);
  167. if ('detail' in error) {
  168. toast.error(error.detail);
  169. } else {
  170. toast.error('Server connection failed');
  171. }
  172. return null;
  173. });
  174. console.log(res);
  175. return res?.version ?? '0';
  176. };
  177. const setOllamaVersion = async (ollamaVersion) => {
  178. await info.set({ ...$info, ollama: { version: ollamaVersion } });
  179. if (
  180. ollamaVersion.localeCompare(requiredOllamaVersion, undefined, {
  181. numeric: true,
  182. sensitivity: 'case',
  183. caseFirst: 'upper'
  184. }) < 0
  185. ) {
  186. toast.error(`Ollama Version: ${ollamaVersion}`);
  187. }
  188. };
  189. onMount(async () => {
  190. if ($config && $config.auth && $user === undefined) {
  191. await goto('/auth');
  192. }
  193. await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
  194. await models.set(await getModels());
  195. await modelfiles.set(JSON.parse(localStorage.getItem('modelfiles') ?? '[]'));
  196. modelfiles.subscribe(async () => {
  197. await models.set(await getModels());
  198. });
  199. let _db = await getDB();
  200. await db.set(_db);
  201. await setOllamaVersion(await getOllamaVersion());
  202. await tick();
  203. loaded = true;
  204. });
  205. </script>
  206. {#if loaded}
  207. <div class="app relative">
  208. {#if ($info?.ollama?.version ?? '0').localeCompare( requiredOllamaVersion, undefined, { numeric: true, sensitivity: 'case', caseFirst: 'upper' } ) < 0}
  209. <div class="absolute w-full h-full flex z-50">
  210. <div
  211. class="absolute rounded-xl w-full h-full backdrop-blur bg-gray-900/60 flex justify-center"
  212. >
  213. <div class="m-auto pb-44 flex flex-col justify-center">
  214. <div class="max-w-md">
  215. <div class="text-center dark:text-white text-2xl font-medium z-50">
  216. Connection Issue or Update Needed
  217. </div>
  218. <div class=" mt-4 text-center text-sm dark:text-gray-200 w-full">
  219. Oops! It seems like your Ollama needs a little attention. <br
  220. class=" hidden sm:flex"
  221. />We've detected either a connection hiccup or observed that you're using an older
  222. version. Ensure you're on the latest Ollama version
  223. <br class=" hidden sm:flex" />(version
  224. <span class=" dark:text-white font-medium">{requiredOllamaVersion} or higher</span>)
  225. or check your connection.
  226. </div>
  227. <div class=" mt-6 mx-auto relative group w-fit">
  228. <button
  229. class="relative z-20 flex px-5 py-2 rounded-full bg-gray-100 hover:bg-gray-200 transition font-medium text-sm"
  230. on:click={async () => {
  231. await setOllamaVersion(await getOllamaVersion());
  232. }}
  233. >
  234. Check Again
  235. </button>
  236. <button
  237. class="text-xs text-center w-full mt-2 text-gray-400 underline"
  238. on:click={async () => {
  239. await setOllamaVersion(requiredOllamaVersion);
  240. }}>Close</button
  241. >
  242. </div>
  243. </div>
  244. </div>
  245. </div>
  246. </div>
  247. {/if}
  248. <div
  249. class=" text-gray-700 dark:text-gray-100 bg-white dark:bg-gray-800 min-h-screen overflow-auto flex flex-row"
  250. >
  251. <Sidebar />
  252. <SettingsModal bind:show={$showSettings} />
  253. <slot />
  254. </div>
  255. </div>
  256. {/if}
  257. <style>
  258. .loading {
  259. display: inline-block;
  260. clip-path: inset(0 1ch 0 0);
  261. animation: l 1s steps(3) infinite;
  262. letter-spacing: -0.5px;
  263. }
  264. @keyframes l {
  265. to {
  266. clip-path: inset(0 -1ch 0 0);
  267. }
  268. }
  269. pre[class*='language-'] {
  270. position: relative;
  271. overflow: auto;
  272. /* make space */
  273. margin: 5px 0;
  274. padding: 1.75rem 0 1.75rem 1rem;
  275. border-radius: 10px;
  276. }
  277. pre[class*='language-'] button {
  278. position: absolute;
  279. top: 5px;
  280. right: 5px;
  281. font-size: 0.9rem;
  282. padding: 0.15rem;
  283. background-color: #828282;
  284. border: ridge 1px #7b7b7c;
  285. border-radius: 5px;
  286. text-shadow: #c4c4c4 0 0 2px;
  287. }
  288. pre[class*='language-'] button:hover {
  289. cursor: pointer;
  290. background-color: #bcbabb;
  291. }
  292. </style>