SettingsModal.svelte 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182
  1. <script lang="ts">
  2. import Modal from '../common/Modal.svelte';
  3. import { WEB_UI_VERSION, OLLAMA_API_BASE_URL } from '$lib/constants';
  4. import toast from 'svelte-french-toast';
  5. import { onMount } from 'svelte';
  6. import { config, info, models, settings, user } from '$lib/stores';
  7. import { splitStream, getGravatarURL } from '$lib/utils';
  8. import Advanced from './Settings/Advanced.svelte';
  9. export let show = false;
  10. const saveSettings = async (updated) => {
  11. console.log(updated);
  12. await settings.set({ ...$settings, ...updated });
  13. await models.set(await getModels());
  14. localStorage.setItem('settings', JSON.stringify($settings));
  15. };
  16. let selectedTab = 'general';
  17. // General
  18. let API_BASE_URL = OLLAMA_API_BASE_URL;
  19. let theme = 'dark';
  20. let notificationEnabled = false;
  21. let system = '';
  22. // Advanced
  23. let requestFormat = '';
  24. let options = {
  25. // Advanced
  26. seed: 0,
  27. temperature: '',
  28. repeat_penalty: '',
  29. repeat_last_n: '',
  30. mirostat: '',
  31. mirostat_eta: '',
  32. mirostat_tau: '',
  33. top_k: '',
  34. top_p: '',
  35. stop: '',
  36. tfs_z: '',
  37. num_ctx: ''
  38. };
  39. // Models
  40. let modelTag = '';
  41. let deleteModelTag = '';
  42. let digest = '';
  43. let pullProgress = null;
  44. // Addons
  45. let titleAutoGenerate = true;
  46. let speechAutoSend = false;
  47. let responseAutoCopy = false;
  48. let gravatarEmail = '';
  49. let OPENAI_API_KEY = '';
  50. // Auth
  51. let authEnabled = false;
  52. let authType = 'Basic';
  53. let authContent = '';
  54. const checkOllamaConnection = async () => {
  55. if (API_BASE_URL === '') {
  56. API_BASE_URL = OLLAMA_API_BASE_URL;
  57. }
  58. const _models = await getModels(API_BASE_URL, 'ollama');
  59. if (_models.length > 0) {
  60. toast.success('Server connection verified');
  61. await models.set(_models);
  62. saveSettings({
  63. API_BASE_URL: API_BASE_URL
  64. });
  65. }
  66. };
  67. const toggleTheme = async () => {
  68. if (theme === 'dark') {
  69. theme = 'light';
  70. } else {
  71. theme = 'dark';
  72. }
  73. localStorage.theme = theme;
  74. document.documentElement.classList.remove(theme === 'dark' ? 'light' : 'dark');
  75. document.documentElement.classList.add(theme);
  76. };
  77. const toggleRequestFormat = async () => {
  78. if (requestFormat === '') {
  79. requestFormat = 'json';
  80. } else {
  81. requestFormat = '';
  82. }
  83. saveSettings({ requestFormat: requestFormat !== '' ? requestFormat : undefined });
  84. };
  85. const toggleSpeechAutoSend = async () => {
  86. speechAutoSend = !speechAutoSend;
  87. saveSettings({ speechAutoSend: speechAutoSend });
  88. };
  89. const toggleTitleAutoGenerate = async () => {
  90. titleAutoGenerate = !titleAutoGenerate;
  91. saveSettings({ titleAutoGenerate: titleAutoGenerate });
  92. };
  93. const toggleNotification = async () => {
  94. const permission = await Notification.requestPermission();
  95. if (permission === 'granted') {
  96. notificationEnabled = !notificationEnabled;
  97. saveSettings({ notificationEnabled: notificationEnabled });
  98. } else {
  99. toast.error(
  100. 'Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.'
  101. );
  102. }
  103. };
  104. const toggleResponseAutoCopy = async () => {
  105. const permission = await navigator.clipboard
  106. .readText()
  107. .then(() => {
  108. return 'granted';
  109. })
  110. .catch(() => {
  111. return '';
  112. });
  113. console.log(permission);
  114. if (permission === 'granted') {
  115. responseAutoCopy = !responseAutoCopy;
  116. saveSettings({ responseAutoCopy: responseAutoCopy });
  117. } else {
  118. toast.error(
  119. 'Clipboard write permission denied. Please check your browser settings to grant the necessary access.'
  120. );
  121. }
  122. };
  123. const toggleAuthHeader = async () => {
  124. authEnabled = !authEnabled;
  125. };
  126. const pullModelHandler = async () => {
  127. const res = await fetch(`${API_BASE_URL}/pull`, {
  128. method: 'POST',
  129. headers: {
  130. 'Content-Type': 'text/event-stream',
  131. ...($settings.authHeader && { Authorization: $settings.authHeader }),
  132. ...($user && { Authorization: `Bearer ${localStorage.token}` })
  133. },
  134. body: JSON.stringify({
  135. name: modelTag
  136. })
  137. });
  138. const reader = res.body
  139. .pipeThrough(new TextDecoderStream())
  140. .pipeThrough(splitStream('\n'))
  141. .getReader();
  142. while (true) {
  143. const { value, done } = await reader.read();
  144. if (done) break;
  145. try {
  146. let lines = value.split('\n');
  147. for (const line of lines) {
  148. if (line !== '') {
  149. console.log(line);
  150. let data = JSON.parse(line);
  151. console.log(data);
  152. if (data.error) {
  153. throw data.error;
  154. }
  155. if (data.detail) {
  156. throw data.detail;
  157. }
  158. if (data.status) {
  159. if (!data.digest) {
  160. toast.success(data.status);
  161. if (data.status === 'success') {
  162. const notification = new Notification(`Ollama`, {
  163. body: `Model '${modelTag}' has been successfully downloaded.`,
  164. icon: '/favicon.png'
  165. });
  166. }
  167. } else {
  168. digest = data.digest;
  169. if (data.completed) {
  170. pullProgress = Math.round((data.completed / data.total) * 1000) / 10;
  171. } else {
  172. pullProgress = 100;
  173. }
  174. }
  175. }
  176. }
  177. }
  178. } catch (error) {
  179. console.log(error);
  180. toast.error(error);
  181. }
  182. }
  183. modelTag = '';
  184. models.set(await getModels());
  185. };
  186. const deleteModelHandler = async () => {
  187. const res = await fetch(`${API_BASE_URL}/delete`, {
  188. method: 'DELETE',
  189. headers: {
  190. 'Content-Type': 'text/event-stream',
  191. ...($settings.authHeader && { Authorization: $settings.authHeader }),
  192. ...($user && { Authorization: `Bearer ${localStorage.token}` })
  193. },
  194. body: JSON.stringify({
  195. name: deleteModelTag
  196. })
  197. });
  198. const reader = res.body
  199. .pipeThrough(new TextDecoderStream())
  200. .pipeThrough(splitStream('\n'))
  201. .getReader();
  202. while (true) {
  203. const { value, done } = await reader.read();
  204. if (done) break;
  205. try {
  206. let lines = value.split('\n');
  207. for (const line of lines) {
  208. if (line !== '' && line !== 'null') {
  209. console.log(line);
  210. let data = JSON.parse(line);
  211. console.log(data);
  212. if (data.error) {
  213. throw data.error;
  214. }
  215. if (data.detail) {
  216. throw data.detail;
  217. }
  218. if (data.status) {
  219. }
  220. } else {
  221. toast.success(`Deleted ${deleteModelTag}`);
  222. }
  223. }
  224. } catch (error) {
  225. console.log(error);
  226. toast.error(error);
  227. }
  228. }
  229. deleteModelTag = '';
  230. models.set(await getModels());
  231. };
  232. const getModels = async (url = '', type = 'all') => {
  233. let models = [];
  234. const res = await fetch(`${url ? url : $settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/tags`, {
  235. method: 'GET',
  236. headers: {
  237. Accept: 'application/json',
  238. 'Content-Type': 'application/json',
  239. ...($settings.authHeader && { Authorization: $settings.authHeader }),
  240. ...($user && { Authorization: `Bearer ${localStorage.token}` })
  241. }
  242. })
  243. .then(async (res) => {
  244. if (!res.ok) throw await res.json();
  245. return res.json();
  246. })
  247. .catch((error) => {
  248. console.log(error);
  249. if ('detail' in error) {
  250. toast.error(error.detail);
  251. } else {
  252. toast.error('Server connection failed');
  253. }
  254. return null;
  255. });
  256. console.log(res);
  257. models.push(...(res?.models ?? []));
  258. // If OpenAI API Key exists
  259. if (type === 'all' && $settings.OPENAI_API_KEY) {
  260. // Validate OPENAI_API_KEY
  261. const openaiModelRes = await fetch(`https://api.openai.com/v1/models`, {
  262. method: 'GET',
  263. headers: {
  264. 'Content-Type': 'application/json',
  265. Authorization: `Bearer ${$settings.OPENAI_API_KEY}`
  266. }
  267. })
  268. .then(async (res) => {
  269. if (!res.ok) throw await res.json();
  270. return res.json();
  271. })
  272. .catch((error) => {
  273. console.log(error);
  274. toast.error(`OpenAI: ${error?.error?.message ?? 'Network Problem'}`);
  275. return null;
  276. });
  277. const openAIModels = openaiModelRes?.data ?? null;
  278. models.push(
  279. ...(openAIModels
  280. ? [
  281. { name: 'hr' },
  282. ...openAIModels
  283. .map((model) => ({ name: model.id, label: 'OpenAI' }))
  284. .filter((model) => model.name.includes('gpt'))
  285. ]
  286. : [])
  287. );
  288. }
  289. return models;
  290. };
  291. onMount(() => {
  292. let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
  293. console.log(settings);
  294. theme = localStorage.theme ?? 'dark';
  295. notificationEnabled = settings.notificationEnabled ?? false;
  296. API_BASE_URL = settings.API_BASE_URL ?? OLLAMA_API_BASE_URL;
  297. system = settings.system ?? '';
  298. requestFormat = settings.requestFormat ?? '';
  299. options.seed = settings.seed ?? 0;
  300. options.temperature = settings.temperature ?? '';
  301. options.repeat_penalty = settings.repeat_penalty ?? '';
  302. options.top_k = settings.top_k ?? '';
  303. options.top_p = settings.top_p ?? '';
  304. options.num_ctx = settings.num_ctx ?? '';
  305. options = { ...options, ...settings.options };
  306. titleAutoGenerate = settings.titleAutoGenerate ?? true;
  307. speechAutoSend = settings.speechAutoSend ?? false;
  308. responseAutoCopy = settings.responseAutoCopy ?? false;
  309. gravatarEmail = settings.gravatarEmail ?? '';
  310. OPENAI_API_KEY = settings.OPENAI_API_KEY ?? '';
  311. authEnabled = settings.authHeader !== undefined ? true : false;
  312. if (authEnabled) {
  313. authType = settings.authHeader.split(' ')[0];
  314. authContent = settings.authHeader.split(' ')[1];
  315. }
  316. });
  317. </script>
  318. <Modal bind:show>
  319. <div>
  320. <div class=" flex justify-between dark:text-gray-300 px-5 py-4">
  321. <div class=" text-lg font-medium self-center">Settings</div>
  322. <button
  323. class="self-center"
  324. on:click={() => {
  325. show = false;
  326. }}
  327. >
  328. <svg
  329. xmlns="http://www.w3.org/2000/svg"
  330. viewBox="0 0 20 20"
  331. fill="currentColor"
  332. class="w-5 h-5"
  333. >
  334. <path
  335. d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
  336. />
  337. </svg>
  338. </button>
  339. </div>
  340. <hr class=" dark:border-gray-800" />
  341. <div class="flex flex-col md:flex-row w-full p-4 md:space-x-4">
  342. <div
  343. class="tabs flex flex-row overflow-x-auto space-x-1 md:space-x-0 md:space-y-1 md:flex-col flex-1 md:flex-none md:w-40 dark:text-gray-200 text-xs text-left mb-3 md:mb-0"
  344. >
  345. <button
  346. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  347. 'general'
  348. ? 'bg-gray-200 dark:bg-gray-700'
  349. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  350. on:click={() => {
  351. selectedTab = 'general';
  352. }}
  353. >
  354. <div class=" self-center mr-2">
  355. <svg
  356. xmlns="http://www.w3.org/2000/svg"
  357. viewBox="0 0 20 20"
  358. fill="currentColor"
  359. class="w-4 h-4"
  360. >
  361. <path
  362. fill-rule="evenodd"
  363. d="M8.34 1.804A1 1 0 019.32 1h1.36a1 1 0 01.98.804l.295 1.473c.497.144.971.342 1.416.587l1.25-.834a1 1 0 011.262.125l.962.962a1 1 0 01.125 1.262l-.834 1.25c.245.445.443.919.587 1.416l1.473.294a1 1 0 01.804.98v1.361a1 1 0 01-.804.98l-1.473.295a6.95 6.95 0 01-.587 1.416l.834 1.25a1 1 0 01-.125 1.262l-.962.962a1 1 0 01-1.262.125l-1.25-.834a6.953 6.953 0 01-1.416.587l-.294 1.473a1 1 0 01-.98.804H9.32a1 1 0 01-.98-.804l-.295-1.473a6.957 6.957 0 01-1.416-.587l-1.25.834a1 1 0 01-1.262-.125l-.962-.962a1 1 0 01-.125-1.262l.834-1.25a6.957 6.957 0 01-.587-1.416l-1.473-.294A1 1 0 011 10.68V9.32a1 1 0 01.804-.98l1.473-.295c.144-.497.342-.971.587-1.416l-.834-1.25a1 1 0 01.125-1.262l.962-.962A1 1 0 015.38 3.03l1.25.834a6.957 6.957 0 011.416-.587l.294-1.473zM13 10a3 3 0 11-6 0 3 3 0 016 0z"
  364. clip-rule="evenodd"
  365. />
  366. </svg>
  367. </div>
  368. <div class=" self-center">General</div>
  369. </button>
  370. <button
  371. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  372. 'advanced'
  373. ? 'bg-gray-200 dark:bg-gray-700'
  374. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  375. on:click={() => {
  376. selectedTab = 'advanced';
  377. }}
  378. >
  379. <div class=" self-center mr-2">
  380. <svg
  381. xmlns="http://www.w3.org/2000/svg"
  382. viewBox="0 0 20 20"
  383. fill="currentColor"
  384. class="w-4 h-4"
  385. >
  386. <path
  387. d="M17 2.75a.75.75 0 00-1.5 0v5.5a.75.75 0 001.5 0v-5.5zM17 15.75a.75.75 0 00-1.5 0v1.5a.75.75 0 001.5 0v-1.5zM3.75 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5a.75.75 0 01.75-.75zM4.5 2.75a.75.75 0 00-1.5 0v5.5a.75.75 0 001.5 0v-5.5zM10 11a.75.75 0 01.75.75v5.5a.75.75 0 01-1.5 0v-5.5A.75.75 0 0110 11zM10.75 2.75a.75.75 0 00-1.5 0v1.5a.75.75 0 001.5 0v-1.5zM10 6a2 2 0 100 4 2 2 0 000-4zM3.75 10a2 2 0 100 4 2 2 0 000-4zM16.25 10a2 2 0 100 4 2 2 0 000-4z"
  388. />
  389. </svg>
  390. </div>
  391. <div class=" self-center">Advanced</div>
  392. </button>
  393. <button
  394. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  395. 'models'
  396. ? 'bg-gray-200 dark:bg-gray-700'
  397. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  398. on:click={() => {
  399. selectedTab = 'models';
  400. }}
  401. >
  402. <div class=" self-center mr-2">
  403. <svg
  404. xmlns="http://www.w3.org/2000/svg"
  405. viewBox="0 0 20 20"
  406. fill="currentColor"
  407. class="w-4 h-4"
  408. >
  409. <path
  410. fill-rule="evenodd"
  411. d="M10 1c3.866 0 7 1.79 7 4s-3.134 4-7 4-7-1.79-7-4 3.134-4 7-4zm5.694 8.13c.464-.264.91-.583 1.306-.952V10c0 2.21-3.134 4-7 4s-7-1.79-7-4V8.178c.396.37.842.688 1.306.953C5.838 10.006 7.854 10.5 10 10.5s4.162-.494 5.694-1.37zM3 13.179V15c0 2.21 3.134 4 7 4s7-1.79 7-4v-1.822c-.396.37-.842.688-1.306.953-1.532.875-3.548 1.369-5.694 1.369s-4.162-.494-5.694-1.37A7.009 7.009 0 013 13.179z"
  412. clip-rule="evenodd"
  413. />
  414. </svg>
  415. </div>
  416. <div class=" self-center">Models</div>
  417. </button>
  418. <button
  419. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  420. 'addons'
  421. ? 'bg-gray-200 dark:bg-gray-700'
  422. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  423. on:click={() => {
  424. selectedTab = 'addons';
  425. }}
  426. >
  427. <div class=" self-center mr-2">
  428. <svg
  429. xmlns="http://www.w3.org/2000/svg"
  430. viewBox="0 0 20 20"
  431. fill="currentColor"
  432. class="w-4 h-4"
  433. >
  434. <path
  435. d="M12 4.467c0-.405.262-.75.559-1.027.276-.257.441-.584.441-.94 0-.828-.895-1.5-2-1.5s-2 .672-2 1.5c0 .362.171.694.456.953.29.265.544.6.544.994a.968.968 0 01-1.024.974 39.655 39.655 0 01-3.014-.306.75.75 0 00-.847.847c.14.993.242 1.999.306 3.014A.968.968 0 014.447 10c-.393 0-.729-.253-.994-.544C3.194 9.17 2.862 9 2.5 9 1.672 9 1 9.895 1 11s.672 2 1.5 2c.356 0 .683-.165.94-.441.276-.297.622-.559 1.027-.559a.997.997 0 011.004 1.03 39.747 39.747 0 01-.319 3.734.75.75 0 00.64.842c1.05.146 2.111.252 3.184.318A.97.97 0 0010 16.948c0-.394-.254-.73-.545-.995C9.171 15.693 9 15.362 9 15c0-.828.895-1.5 2-1.5s2 .672 2 1.5c0 .356-.165.683-.441.94-.297.276-.559.622-.559 1.027a.998.998 0 001.03 1.005c1.337-.05 2.659-.162 3.961-.337a.75.75 0 00.644-.644c.175-1.302.288-2.624.337-3.961A.998.998 0 0016.967 12c-.405 0-.75.262-1.027.559-.257.276-.584.441-.94.441-.828 0-1.5-.895-1.5-2s.672-2 1.5-2c.362 0 .694.17.953.455.265.291.601.545.995.545a.97.97 0 00.976-1.024 41.159 41.159 0 00-.318-3.184.75.75 0 00-.842-.64c-1.228.164-2.473.271-3.734.319A.997.997 0 0112 4.467z"
  436. />
  437. </svg>
  438. </div>
  439. <div class=" self-center">Add-ons</div>
  440. </button>
  441. {#if !$config || ($config && !$config.auth)}
  442. <button
  443. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  444. 'auth'
  445. ? 'bg-gray-200 dark:bg-gray-700'
  446. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  447. on:click={() => {
  448. selectedTab = 'auth';
  449. }}
  450. >
  451. <div class=" self-center mr-2">
  452. <svg
  453. xmlns="http://www.w3.org/2000/svg"
  454. viewBox="0 0 24 24"
  455. fill="currentColor"
  456. class="w-4 h-4"
  457. >
  458. <path
  459. fill-rule="evenodd"
  460. d="M12.516 2.17a.75.75 0 00-1.032 0 11.209 11.209 0 01-7.877 3.08.75.75 0 00-.722.515A12.74 12.74 0 002.25 9.75c0 5.942 4.064 10.933 9.563 12.348a.749.749 0 00.374 0c5.499-1.415 9.563-6.406 9.563-12.348 0-1.39-.223-2.73-.635-3.985a.75.75 0 00-.722-.516l-.143.001c-2.996 0-5.717-1.17-7.734-3.08zm3.094 8.016a.75.75 0 10-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 00-1.06 1.06l2.25 2.25a.75.75 0 001.14-.094l3.75-5.25z"
  461. clip-rule="evenodd"
  462. />
  463. </svg>
  464. </div>
  465. <div class=" self-center">Authentication</div>
  466. </button>
  467. {/if}
  468. <button
  469. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  470. 'about'
  471. ? 'bg-gray-200 dark:bg-gray-700'
  472. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  473. on:click={() => {
  474. selectedTab = 'about';
  475. }}
  476. >
  477. <div class=" self-center mr-2">
  478. <svg
  479. xmlns="http://www.w3.org/2000/svg"
  480. viewBox="0 0 20 20"
  481. fill="currentColor"
  482. class="w-4 h-4"
  483. >
  484. <path
  485. fill-rule="evenodd"
  486. d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z"
  487. clip-rule="evenodd"
  488. />
  489. </svg>
  490. </div>
  491. <div class=" self-center">About</div>
  492. </button>
  493. </div>
  494. <div class="flex-1 md:min-h-[340px]">
  495. {#if selectedTab === 'general'}
  496. <div class="flex flex-col space-y-3">
  497. <div>
  498. <div class=" mb-1 text-sm font-medium">WebUI Settings</div>
  499. <div class=" py-0.5 flex w-full justify-between">
  500. <div class=" self-center text-xs font-medium">Theme</div>
  501. <button
  502. class="p-1 px-3 text-xs flex rounded transition"
  503. on:click={() => {
  504. toggleTheme();
  505. }}
  506. >
  507. {#if theme === 'dark'}
  508. <svg
  509. xmlns="http://www.w3.org/2000/svg"
  510. viewBox="0 0 20 20"
  511. fill="currentColor"
  512. class="w-4 h-4"
  513. >
  514. <path
  515. fill-rule="evenodd"
  516. d="M7.455 2.004a.75.75 0 01.26.77 7 7 0 009.958 7.967.75.75 0 011.067.853A8.5 8.5 0 116.647 1.921a.75.75 0 01.808.083z"
  517. clip-rule="evenodd"
  518. />
  519. </svg>
  520. <span class="ml-2 self-center"> Dark </span>
  521. {:else}
  522. <svg
  523. xmlns="http://www.w3.org/2000/svg"
  524. viewBox="0 0 20 20"
  525. fill="currentColor"
  526. class="w-4 h-4 self-center"
  527. >
  528. <path
  529. d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
  530. />
  531. </svg>
  532. <span class="ml-2 self-center"> Light </span>
  533. {/if}
  534. </button>
  535. </div>
  536. <div>
  537. <div class=" py-0.5 flex w-full justify-between">
  538. <div class=" self-center text-xs font-medium">Notification</div>
  539. <button
  540. class="p-1 px-3 text-xs flex rounded transition"
  541. on:click={() => {
  542. toggleNotification();
  543. }}
  544. type="button"
  545. >
  546. {#if notificationEnabled === true}
  547. <span class="ml-2 self-center">On</span>
  548. {:else}
  549. <span class="ml-2 self-center">Off</span>
  550. {/if}
  551. </button>
  552. </div>
  553. </div>
  554. </div>
  555. <hr class=" dark:border-gray-700" />
  556. <div>
  557. <div class=" mb-2.5 text-sm font-medium">Ollama Server URL</div>
  558. <div class="flex w-full">
  559. <div class="flex-1 mr-2">
  560. <input
  561. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  562. placeholder="Enter URL (e.g. http://localhost:11434/api)"
  563. bind:value={API_BASE_URL}
  564. />
  565. </div>
  566. <button
  567. class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded transition"
  568. on:click={() => {
  569. checkOllamaConnection();
  570. }}
  571. >
  572. <svg
  573. xmlns="http://www.w3.org/2000/svg"
  574. viewBox="0 0 20 20"
  575. fill="currentColor"
  576. class="w-4 h-4"
  577. >
  578. <path
  579. fill-rule="evenodd"
  580. d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
  581. clip-rule="evenodd"
  582. />
  583. </svg>
  584. </button>
  585. </div>
  586. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  587. Trouble accessing Ollama? <a
  588. class=" text-gray-500 dark:text-gray-300 font-medium"
  589. href="https://github.com/ollama-webui/ollama-webui#troubleshooting"
  590. target="_blank"
  591. >
  592. Click here for help.
  593. </a>
  594. </div>
  595. </div>
  596. <hr class=" dark:border-gray-700" />
  597. <div>
  598. <div class=" mb-2.5 text-sm font-medium">System Prompt</div>
  599. <textarea
  600. bind:value={system}
  601. class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
  602. rows="4"
  603. />
  604. </div>
  605. <div class="flex justify-end pt-3 text-sm font-medium">
  606. <button
  607. class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
  608. on:click={() => {
  609. saveSettings({
  610. API_BASE_URL: API_BASE_URL === '' ? OLLAMA_API_BASE_URL : API_BASE_URL,
  611. system: system !== '' ? system : undefined
  612. });
  613. show = false;
  614. }}
  615. >
  616. Save
  617. </button>
  618. </div>
  619. </div>
  620. {:else if selectedTab === 'advanced'}
  621. <div class="flex flex-col h-full justify-between text-sm">
  622. <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-72">
  623. <div class=" text-sm font-medium">Parameters</div>
  624. <Advanced bind:options />
  625. <hr class=" dark:border-gray-700" />
  626. <div>
  627. <div class=" py-1 flex w-full justify-between">
  628. <div class=" self-center text-sm font-medium">Request Mode</div>
  629. <button
  630. class="p-1 px-3 text-xs flex rounded transition"
  631. on:click={() => {
  632. toggleRequestFormat();
  633. }}
  634. >
  635. {#if requestFormat === ''}
  636. <span class="ml-2 self-center"> Default </span>
  637. {:else if requestFormat === 'json'}
  638. <!-- <svg
  639. xmlns="http://www.w3.org/2000/svg"
  640. viewBox="0 0 20 20"
  641. fill="currentColor"
  642. class="w-4 h-4 self-center"
  643. >
  644. <path
  645. d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
  646. />
  647. </svg> -->
  648. <span class="ml-2 self-center"> JSON </span>
  649. {/if}
  650. </button>
  651. </div>
  652. </div>
  653. </div>
  654. <div class="flex justify-end pt-3 text-sm font-medium">
  655. <button
  656. class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
  657. on:click={() => {
  658. saveSettings({
  659. options: {
  660. seed: (options.seed !== 0 ? options.seed : undefined) ?? undefined,
  661. stop: options.stop !== '' ? options.stop : undefined,
  662. temperature: options.temperature !== '' ? options.temperature : undefined,
  663. repeat_penalty:
  664. options.repeat_penalty !== '' ? options.repeat_penalty : undefined,
  665. repeat_last_n:
  666. options.repeat_last_n !== '' ? options.repeat_last_n : undefined,
  667. mirostat: options.mirostat !== '' ? options.mirostat : undefined,
  668. mirostat_eta: options.mirostat_eta !== '' ? options.mirostat_eta : undefined,
  669. mirostat_tau: options.mirostat_tau !== '' ? options.mirostat_tau : undefined,
  670. top_k: options.top_k !== '' ? options.top_k : undefined,
  671. top_p: options.top_p !== '' ? options.top_p : undefined,
  672. tfs_z: options.tfs_z !== '' ? options.tfs_z : undefined,
  673. num_ctx: options.num_ctx !== '' ? options.num_ctx : undefined
  674. }
  675. });
  676. show = false;
  677. }}
  678. >
  679. Save
  680. </button>
  681. </div>
  682. </div>
  683. {:else if selectedTab === 'models'}
  684. <div class="flex flex-col space-y-3 text-sm mb-10">
  685. <div>
  686. <div class=" mb-2.5 text-sm font-medium">Pull a model</div>
  687. <div class="flex w-full">
  688. <div class="flex-1 mr-2">
  689. <input
  690. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  691. placeholder="Enter model tag (e.g. mistral:7b)"
  692. bind:value={modelTag}
  693. />
  694. </div>
  695. <button
  696. class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 rounded transition"
  697. on:click={() => {
  698. pullModelHandler();
  699. }}
  700. >
  701. <svg
  702. xmlns="http://www.w3.org/2000/svg"
  703. viewBox="0 0 20 20"
  704. fill="currentColor"
  705. class="w-4 h-4"
  706. >
  707. <path
  708. d="M10.75 2.75a.75.75 0 00-1.5 0v8.614L6.295 8.235a.75.75 0 10-1.09 1.03l4.25 4.5a.75.75 0 001.09 0l4.25-4.5a.75.75 0 00-1.09-1.03l-2.955 3.129V2.75z"
  709. />
  710. <path
  711. d="M3.5 12.75a.75.75 0 00-1.5 0v2.5A2.75 2.75 0 004.75 18h10.5A2.75 2.75 0 0018 15.25v-2.5a.75.75 0 00-1.5 0v2.5c0 .69-.56 1.25-1.25 1.25H4.75c-.69 0-1.25-.56-1.25-1.25v-2.5z"
  712. />
  713. </svg>
  714. </button>
  715. </div>
  716. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  717. To access the available model names for downloading, <a
  718. class=" text-gray-500 dark:text-gray-300 font-medium"
  719. href="https://ollama.ai/library"
  720. target="_blank">click here.</a
  721. >
  722. </div>
  723. {#if pullProgress !== null}
  724. <div class="mt-2">
  725. <div class=" mb-2 text-xs">Pull Progress</div>
  726. <div class="w-full rounded-full dark:bg-gray-800">
  727. <div
  728. class="dark:bg-gray-600 text-xs font-medium text-blue-100 text-center p-0.5 leading-none rounded-full"
  729. style="width: {Math.max(15, pullProgress ?? 0)}%"
  730. >
  731. {pullProgress ?? 0}%
  732. </div>
  733. </div>
  734. <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
  735. {digest}
  736. </div>
  737. </div>
  738. {/if}
  739. </div>
  740. <hr class=" dark:border-gray-700" />
  741. <div>
  742. <div class=" mb-2.5 text-sm font-medium">Delete a model</div>
  743. <div class="flex w-full">
  744. <div class="flex-1 mr-2">
  745. <select
  746. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  747. bind:value={deleteModelTag}
  748. placeholder="Select a model"
  749. >
  750. {#if !deleteModelTag}
  751. <option value="" disabled selected>Select a model</option>
  752. {/if}
  753. {#each $models.filter((m) => m.size != null) as model}
  754. <option value={model.name} class="bg-gray-100 dark:bg-gray-700"
  755. >{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option
  756. >
  757. {/each}
  758. </select>
  759. </div>
  760. <button
  761. class="px-3 bg-red-700 hover:bg-red-800 text-gray-100 rounded transition"
  762. on:click={() => {
  763. deleteModelHandler();
  764. }}
  765. >
  766. <svg
  767. xmlns="http://www.w3.org/2000/svg"
  768. viewBox="0 0 20 20"
  769. fill="currentColor"
  770. class="w-4 h-4"
  771. >
  772. <path
  773. fill-rule="evenodd"
  774. d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 002.742-2.53l.841-10.52.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34.06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z"
  775. clip-rule="evenodd"
  776. />
  777. </svg>
  778. </button>
  779. </div>
  780. </div>
  781. </div>
  782. {:else if selectedTab === 'addons'}
  783. <form
  784. class="flex flex-col h-full justify-between space-y-3 text-sm"
  785. on:submit|preventDefault={() => {
  786. saveSettings({
  787. gravatarEmail: gravatarEmail !== '' ? gravatarEmail : undefined,
  788. gravatarUrl: gravatarEmail !== '' ? getGravatarURL(gravatarEmail) : undefined,
  789. OPENAI_API_KEY: OPENAI_API_KEY !== '' ? OPENAI_API_KEY : undefined
  790. });
  791. show = false;
  792. }}
  793. >
  794. <div class=" space-y-3">
  795. <div>
  796. <div class=" mb-1 text-sm font-medium">WebUI Add-ons</div>
  797. <div>
  798. <div class=" py-0.5 flex w-full justify-between">
  799. <div class=" self-center text-xs font-medium">Title Auto Generation</div>
  800. <button
  801. class="p-1 px-3 text-xs flex rounded transition"
  802. on:click={() => {
  803. toggleTitleAutoGenerate();
  804. }}
  805. type="button"
  806. >
  807. {#if titleAutoGenerate === true}
  808. <span class="ml-2 self-center">On</span>
  809. {:else}
  810. <span class="ml-2 self-center">Off</span>
  811. {/if}
  812. </button>
  813. </div>
  814. </div>
  815. <div>
  816. <div class=" py-0.5 flex w-full justify-between">
  817. <div class=" self-center text-xs font-medium">Voice Input Auto-Send</div>
  818. <button
  819. class="p-1 px-3 text-xs flex rounded transition"
  820. on:click={() => {
  821. toggleSpeechAutoSend();
  822. }}
  823. type="button"
  824. >
  825. {#if speechAutoSend === true}
  826. <span class="ml-2 self-center">On</span>
  827. {:else}
  828. <span class="ml-2 self-center">Off</span>
  829. {/if}
  830. </button>
  831. </div>
  832. </div>
  833. <div>
  834. <div class=" py-0.5 flex w-full justify-between">
  835. <div class=" self-center text-xs font-medium">
  836. Response AutoCopy to Clipboard
  837. </div>
  838. <button
  839. class="p-1 px-3 text-xs flex rounded transition"
  840. on:click={() => {
  841. toggleResponseAutoCopy();
  842. }}
  843. type="button"
  844. >
  845. {#if responseAutoCopy === true}
  846. <span class="ml-2 self-center">On</span>
  847. {:else}
  848. <span class="ml-2 self-center">Off</span>
  849. {/if}
  850. </button>
  851. </div>
  852. </div>
  853. </div>
  854. <hr class=" dark:border-gray-700" />
  855. <div>
  856. <div class=" mb-2.5 text-sm font-medium">
  857. Gravatar Email <span class=" text-gray-400 text-sm">(optional)</span>
  858. </div>
  859. <div class="flex w-full">
  860. <div class="flex-1">
  861. <input
  862. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  863. placeholder="Enter Your Email"
  864. bind:value={gravatarEmail}
  865. autocomplete="off"
  866. type="email"
  867. />
  868. </div>
  869. </div>
  870. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  871. Changes user profile image to match your <a
  872. class=" text-gray-500 dark:text-gray-300 font-medium"
  873. href="https://gravatar.com/"
  874. target="_blank">Gravatar.</a
  875. >
  876. </div>
  877. </div>
  878. <hr class=" dark:border-gray-700" />
  879. <div>
  880. <div class=" mb-2.5 text-sm font-medium">
  881. OpenAI API Key <span class=" text-gray-400 text-sm">(optional)</span>
  882. </div>
  883. <div class="flex w-full">
  884. <div class="flex-1">
  885. <input
  886. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  887. placeholder="Enter OpenAI API Key"
  888. bind:value={OPENAI_API_KEY}
  889. autocomplete="off"
  890. />
  891. </div>
  892. </div>
  893. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  894. Adds optional support for 'gpt-*' models available.
  895. </div>
  896. </div>
  897. </div>
  898. <div class="flex justify-end pt-3 text-sm font-medium">
  899. <button
  900. class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
  901. type="submit"
  902. >
  903. Save
  904. </button>
  905. </div>
  906. </form>
  907. {:else if selectedTab === 'auth'}
  908. <form
  909. class="flex flex-col h-full justify-between space-y-3 text-sm"
  910. on:submit|preventDefault={() => {
  911. console.log('auth save');
  912. saveSettings({
  913. authHeader: authEnabled ? `${authType} ${authContent}` : undefined
  914. });
  915. show = false;
  916. }}
  917. >
  918. <div class=" space-y-3">
  919. <div>
  920. <div class=" py-1 flex w-full justify-between">
  921. <div class=" self-center text-sm font-medium">Authorization Header</div>
  922. <button
  923. class="p-1 px-3 text-xs flex rounded transition"
  924. type="button"
  925. on:click={() => {
  926. toggleAuthHeader();
  927. }}
  928. >
  929. {#if authEnabled === true}
  930. <svg
  931. xmlns="http://www.w3.org/2000/svg"
  932. viewBox="0 0 24 24"
  933. fill="currentColor"
  934. class="w-4 h-4"
  935. >
  936. <path
  937. fill-rule="evenodd"
  938. d="M12 1.5a5.25 5.25 0 00-5.25 5.25v3a3 3 0 00-3 3v6.75a3 3 0 003 3h10.5a3 3 0 003-3v-6.75a3 3 0 00-3-3v-3c0-2.9-2.35-5.25-5.25-5.25zm3.75 8.25v-3a3.75 3.75 0 10-7.5 0v3h7.5z"
  939. clip-rule="evenodd"
  940. />
  941. </svg>
  942. <span class="ml-2 self-center"> On </span>
  943. {:else}
  944. <svg
  945. xmlns="http://www.w3.org/2000/svg"
  946. viewBox="0 0 24 24"
  947. fill="currentColor"
  948. class="w-4 h-4"
  949. >
  950. <path
  951. d="M18 1.5c2.9 0 5.25 2.35 5.25 5.25v3.75a.75.75 0 01-1.5 0V6.75a3.75 3.75 0 10-7.5 0v3a3 3 0 013 3v6.75a3 3 0 01-3 3H3.75a3 3 0 01-3-3v-6.75a3 3 0 013-3h9v-3c0-2.9 2.35-5.25 5.25-5.25z"
  952. />
  953. </svg>
  954. <span class="ml-2 self-center">Off</span>
  955. {/if}
  956. </button>
  957. </div>
  958. </div>
  959. {#if authEnabled}
  960. <hr class=" dark:border-gray-700" />
  961. <div class="mt-2">
  962. <div class=" py-1 flex w-full space-x-2">
  963. <button
  964. class=" py-1 font-semibold flex rounded transition"
  965. on:click={() => {
  966. authType = authType === 'Basic' ? 'Bearer' : 'Basic';
  967. }}
  968. type="button"
  969. >
  970. {#if authType === 'Basic'}
  971. <span class="self-center mr-2">Basic</span>
  972. {:else if authType === 'Bearer'}
  973. <span class="self-center mr-2">Bearer</span>
  974. {/if}
  975. </button>
  976. <div class="flex-1">
  977. <input
  978. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  979. placeholder="Enter Authorization Header Content"
  980. bind:value={authContent}
  981. />
  982. </div>
  983. </div>
  984. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  985. Toggle between <span class=" text-gray-500 dark:text-gray-300 font-medium"
  986. >'Basic'</span
  987. >
  988. and <span class=" text-gray-500 dark:text-gray-300 font-medium">'Bearer'</span> by
  989. clicking on the label next to the input.
  990. </div>
  991. </div>
  992. <hr class=" dark:border-gray-700" />
  993. <div>
  994. <div class=" mb-2.5 text-sm font-medium">Preview Authorization Header</div>
  995. <textarea
  996. value={JSON.stringify({
  997. Authorization: `${authType} ${authContent}`
  998. })}
  999. class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
  1000. rows="2"
  1001. disabled
  1002. />
  1003. </div>
  1004. {/if}
  1005. </div>
  1006. <div class="flex justify-end pt-3 text-sm font-medium">
  1007. <button
  1008. class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
  1009. type="submit"
  1010. >
  1011. Save
  1012. </button>
  1013. </div>
  1014. </form>
  1015. {:else if selectedTab === 'about'}
  1016. <div class="flex flex-col h-full justify-between space-y-3 text-sm mb-6">
  1017. <div class=" space-y-3">
  1018. <div>
  1019. <div class=" mb-2.5 text-sm font-medium">Ollama Web UI Version</div>
  1020. <div class="flex w-full">
  1021. <div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
  1022. {$config && $config.version ? $config.version : WEB_UI_VERSION}
  1023. </div>
  1024. </div>
  1025. </div>
  1026. <hr class=" dark:border-gray-700" />
  1027. <div>
  1028. <div class=" mb-2.5 text-sm font-medium">Ollama Version</div>
  1029. <div class="flex w-full">
  1030. <div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
  1031. {$info?.ollama?.version ?? 'N/A'}
  1032. </div>
  1033. </div>
  1034. </div>
  1035. <hr class=" dark:border-gray-700" />
  1036. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  1037. Created by <a
  1038. class=" text-gray-500 dark:text-gray-300 font-medium"
  1039. href="https://github.com/tjbck"
  1040. target="_blank">Timothy J. Baek</a
  1041. >
  1042. </div>
  1043. <div>
  1044. <a href="https://github.com/ollama-webui/ollama-webui">
  1045. <img
  1046. alt="Github Repo"
  1047. src="https://img.shields.io/github/stars/ollama-webui/ollama-webui?style=social&label=Star us on Github"
  1048. />
  1049. </a>
  1050. </div>
  1051. </div>
  1052. </div>
  1053. {/if}
  1054. </div>
  1055. </div>
  1056. </div>
  1057. </Modal>
  1058. <style>
  1059. input::-webkit-outer-spin-button,
  1060. input::-webkit-inner-spin-button {
  1061. /* display: none; <- Crashes Chrome on hover */
  1062. -webkit-appearance: none;
  1063. margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
  1064. }
  1065. .tabs::-webkit-scrollbar {
  1066. display: none; /* for Chrome, Safari and Opera */
  1067. }
  1068. .tabs {
  1069. -ms-overflow-style: none; /* IE and Edge */
  1070. scrollbar-width: none; /* Firefox */
  1071. }
  1072. input[type='number'] {
  1073. -moz-appearance: textfield; /* Firefox */
  1074. }
  1075. </style>