SettingsModal.svelte 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076
  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, 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 system = '';
  21. // Advanced
  22. let requestFormat = '';
  23. let options = {
  24. // Advanced
  25. seed: 0,
  26. temperature: '',
  27. repeat_penalty: '',
  28. repeat_last_n: '',
  29. mirostat: '',
  30. mirostat_eta: '',
  31. mirostat_tau: '',
  32. top_k: '',
  33. top_p: '',
  34. stop: '',
  35. tfs_z: '',
  36. num_ctx: ''
  37. };
  38. // Models
  39. let modelTag = '';
  40. let deleteModelTag = '';
  41. let digest = '';
  42. let pullProgress = null;
  43. // Addons
  44. let titleAutoGenerate = true;
  45. let speechAutoSend = false;
  46. let gravatarEmail = '';
  47. let OPENAI_API_KEY = '';
  48. // Auth
  49. let authEnabled = false;
  50. let authType = 'Basic';
  51. let authContent = '';
  52. const checkOllamaConnection = async () => {
  53. if (API_BASE_URL === '') {
  54. API_BASE_URL = OLLAMA_API_BASE_URL;
  55. }
  56. const _models = await getModels(API_BASE_URL, 'ollama');
  57. if (_models.length > 0) {
  58. toast.success('Server connection verified');
  59. await models.set(_models);
  60. saveSettings({
  61. API_BASE_URL: API_BASE_URL
  62. });
  63. }
  64. };
  65. const toggleTheme = async () => {
  66. if (theme === 'dark') {
  67. theme = 'light';
  68. } else {
  69. theme = 'dark';
  70. }
  71. localStorage.theme = theme;
  72. document.documentElement.classList.remove(theme === 'dark' ? 'light' : 'dark');
  73. document.documentElement.classList.add(theme);
  74. };
  75. const toggleRequestFormat = async () => {
  76. if (requestFormat === '') {
  77. requestFormat = 'json';
  78. } else {
  79. requestFormat = '';
  80. }
  81. saveSettings({ requestFormat: requestFormat !== '' ? requestFormat : undefined });
  82. };
  83. const toggleSpeechAutoSend = async () => {
  84. speechAutoSend = !speechAutoSend;
  85. saveSettings({ speechAutoSend: speechAutoSend });
  86. };
  87. const toggleTitleAutoGenerate = async () => {
  88. titleAutoGenerate = !titleAutoGenerate;
  89. saveSettings({ titleAutoGenerate: titleAutoGenerate });
  90. };
  91. const toggleAuthHeader = async () => {
  92. authEnabled = !authEnabled;
  93. };
  94. const pullModelHandler = async () => {
  95. const res = await fetch(`${API_BASE_URL}/pull`, {
  96. method: 'POST',
  97. headers: {
  98. 'Content-Type': 'text/event-stream',
  99. ...($settings.authHeader && { Authorization: $settings.authHeader }),
  100. ...($user && { Authorization: `Bearer ${localStorage.token}` })
  101. },
  102. body: JSON.stringify({
  103. name: modelTag
  104. })
  105. });
  106. const reader = res.body
  107. .pipeThrough(new TextDecoderStream())
  108. .pipeThrough(splitStream('\n'))
  109. .getReader();
  110. while (true) {
  111. const { value, done } = await reader.read();
  112. if (done) break;
  113. try {
  114. let lines = value.split('\n');
  115. for (const line of lines) {
  116. if (line !== '') {
  117. console.log(line);
  118. let data = JSON.parse(line);
  119. console.log(data);
  120. if (data.error) {
  121. throw data.error;
  122. }
  123. if (data.detail) {
  124. throw data.detail;
  125. }
  126. if (data.status) {
  127. if (!data.digest) {
  128. toast.success(data.status);
  129. } else {
  130. digest = data.digest;
  131. if (data.completed) {
  132. pullProgress = Math.round((data.completed / data.total) * 1000) / 10;
  133. } else {
  134. pullProgress = 100;
  135. }
  136. }
  137. }
  138. }
  139. }
  140. } catch (error) {
  141. console.log(error);
  142. toast.error(error);
  143. }
  144. }
  145. modelTag = '';
  146. models.set(await getModels());
  147. };
  148. const deleteModelHandler = async () => {
  149. const res = await fetch(`${API_BASE_URL}/delete`, {
  150. method: 'DELETE',
  151. headers: {
  152. 'Content-Type': 'text/event-stream',
  153. ...($settings.authHeader && { Authorization: $settings.authHeader }),
  154. ...($user && { Authorization: `Bearer ${localStorage.token}` })
  155. },
  156. body: JSON.stringify({
  157. name: deleteModelTag
  158. })
  159. });
  160. const reader = res.body
  161. .pipeThrough(new TextDecoderStream())
  162. .pipeThrough(splitStream('\n'))
  163. .getReader();
  164. while (true) {
  165. const { value, done } = await reader.read();
  166. if (done) break;
  167. try {
  168. let lines = value.split('\n');
  169. for (const line of lines) {
  170. if (line !== '' && line !== 'null') {
  171. console.log(line);
  172. let data = JSON.parse(line);
  173. console.log(data);
  174. if (data.error) {
  175. throw data.error;
  176. }
  177. if (data.detail) {
  178. throw data.detail;
  179. }
  180. if (data.status) {
  181. }
  182. } else {
  183. toast.success(`Deleted ${deleteModelTag}`);
  184. }
  185. }
  186. } catch (error) {
  187. console.log(error);
  188. toast.error(error);
  189. }
  190. }
  191. deleteModelTag = '';
  192. models.set(await getModels());
  193. };
  194. const getModels = async (url = '', type = 'all') => {
  195. let models = [];
  196. const res = await fetch(`${url ? url : $settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/tags`, {
  197. method: 'GET',
  198. headers: {
  199. Accept: 'application/json',
  200. 'Content-Type': 'application/json',
  201. ...($settings.authHeader && { Authorization: $settings.authHeader }),
  202. ...($user && { Authorization: `Bearer ${localStorage.token}` })
  203. }
  204. })
  205. .then(async (res) => {
  206. if (!res.ok) throw await res.json();
  207. return res.json();
  208. })
  209. .catch((error) => {
  210. console.log(error);
  211. if ('detail' in error) {
  212. toast.error(error.detail);
  213. } else {
  214. toast.error('Server connection failed');
  215. }
  216. return null;
  217. });
  218. console.log(res);
  219. models.push(...(res?.models ?? []));
  220. // If OpenAI API Key exists
  221. if (type === 'all' && $settings.OPENAI_API_KEY) {
  222. // Validate OPENAI_API_KEY
  223. const openaiModelRes = await fetch(`https://api.openai.com/v1/models`, {
  224. method: 'GET',
  225. headers: {
  226. 'Content-Type': 'application/json',
  227. Authorization: `Bearer ${$settings.OPENAI_API_KEY}`
  228. }
  229. })
  230. .then(async (res) => {
  231. if (!res.ok) throw await res.json();
  232. return res.json();
  233. })
  234. .catch((error) => {
  235. console.log(error);
  236. toast.error(`OpenAI: ${error?.error?.message ?? 'Network Problem'}`);
  237. return null;
  238. });
  239. const openAIModels = openaiModelRes?.data ?? null;
  240. models.push(
  241. ...(openAIModels
  242. ? [
  243. { name: 'hr' },
  244. ...openAIModels
  245. .map((model) => ({ name: model.id, label: 'OpenAI' }))
  246. .filter((model) => model.name.includes('gpt'))
  247. ]
  248. : [])
  249. );
  250. }
  251. return models;
  252. };
  253. onMount(() => {
  254. let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
  255. console.log(settings);
  256. theme = localStorage.theme ?? 'dark';
  257. API_BASE_URL = settings.API_BASE_URL ?? OLLAMA_API_BASE_URL;
  258. system = settings.system ?? '';
  259. requestFormat = settings.requestFormat ?? '';
  260. options.seed = settings.seed ?? 0;
  261. options.temperature = settings.temperature ?? '';
  262. options.repeat_penalty = settings.repeat_penalty ?? '';
  263. options.top_k = settings.top_k ?? '';
  264. options.top_p = settings.top_p ?? '';
  265. options.num_ctx = settings.num_ctx ?? '';
  266. options = { ...options, ...settings.options };
  267. titleAutoGenerate = settings.titleAutoGenerate ?? true;
  268. speechAutoSend = settings.speechAutoSend ?? false;
  269. gravatarEmail = settings.gravatarEmail ?? '';
  270. OPENAI_API_KEY = settings.OPENAI_API_KEY ?? '';
  271. authEnabled = settings.authHeader !== undefined ? true : false;
  272. if (authEnabled) {
  273. authType = settings.authHeader.split(' ')[0];
  274. authContent = settings.authHeader.split(' ')[1];
  275. }
  276. });
  277. </script>
  278. <Modal bind:show>
  279. <div>
  280. <div class=" flex justify-between dark:text-gray-300 px-5 py-4">
  281. <div class=" text-lg font-medium self-center">Settings</div>
  282. <button
  283. class="self-center"
  284. on:click={() => {
  285. show = false;
  286. }}
  287. >
  288. <svg
  289. xmlns="http://www.w3.org/2000/svg"
  290. viewBox="0 0 20 20"
  291. fill="currentColor"
  292. class="w-5 h-5"
  293. >
  294. <path
  295. 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"
  296. />
  297. </svg>
  298. </button>
  299. </div>
  300. <hr class=" dark:border-gray-800" />
  301. <div class="flex flex-col md:flex-row w-full p-4 md:space-x-4">
  302. <div
  303. 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"
  304. >
  305. <button
  306. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  307. 'general'
  308. ? 'bg-gray-200 dark:bg-gray-700'
  309. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  310. on:click={() => {
  311. selectedTab = 'general';
  312. }}
  313. >
  314. <div class=" self-center mr-2">
  315. <svg
  316. xmlns="http://www.w3.org/2000/svg"
  317. viewBox="0 0 20 20"
  318. fill="currentColor"
  319. class="w-4 h-4"
  320. >
  321. <path
  322. fill-rule="evenodd"
  323. 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"
  324. clip-rule="evenodd"
  325. />
  326. </svg>
  327. </div>
  328. <div class=" self-center">General</div>
  329. </button>
  330. <button
  331. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  332. 'advanced'
  333. ? 'bg-gray-200 dark:bg-gray-700'
  334. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  335. on:click={() => {
  336. selectedTab = 'advanced';
  337. }}
  338. >
  339. <div class=" self-center mr-2">
  340. <svg
  341. xmlns="http://www.w3.org/2000/svg"
  342. viewBox="0 0 20 20"
  343. fill="currentColor"
  344. class="w-4 h-4"
  345. >
  346. <path
  347. 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"
  348. />
  349. </svg>
  350. </div>
  351. <div class=" self-center">Advanced</div>
  352. </button>
  353. <button
  354. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  355. 'models'
  356. ? 'bg-gray-200 dark:bg-gray-700'
  357. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  358. on:click={() => {
  359. selectedTab = 'models';
  360. }}
  361. >
  362. <div class=" self-center mr-2">
  363. <svg
  364. xmlns="http://www.w3.org/2000/svg"
  365. viewBox="0 0 20 20"
  366. fill="currentColor"
  367. class="w-4 h-4"
  368. >
  369. <path
  370. fill-rule="evenodd"
  371. 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"
  372. clip-rule="evenodd"
  373. />
  374. </svg>
  375. </div>
  376. <div class=" self-center">Models</div>
  377. </button>
  378. <button
  379. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  380. 'addons'
  381. ? 'bg-gray-200 dark:bg-gray-700'
  382. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  383. on:click={() => {
  384. selectedTab = 'addons';
  385. }}
  386. >
  387. <div class=" self-center mr-2">
  388. <svg
  389. xmlns="http://www.w3.org/2000/svg"
  390. viewBox="0 0 20 20"
  391. fill="currentColor"
  392. class="w-4 h-4"
  393. >
  394. <path
  395. 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"
  396. />
  397. </svg>
  398. </div>
  399. <div class=" self-center">Add-ons</div>
  400. </button>
  401. {#if !$config || ($config && !$config.auth)}
  402. <button
  403. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  404. 'auth'
  405. ? 'bg-gray-200 dark:bg-gray-700'
  406. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  407. on:click={() => {
  408. selectedTab = 'auth';
  409. }}
  410. >
  411. <div class=" self-center mr-2">
  412. <svg
  413. xmlns="http://www.w3.org/2000/svg"
  414. viewBox="0 0 24 24"
  415. fill="currentColor"
  416. class="w-4 h-4"
  417. >
  418. <path
  419. fill-rule="evenodd"
  420. 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"
  421. clip-rule="evenodd"
  422. />
  423. </svg>
  424. </div>
  425. <div class=" self-center">Authentication</div>
  426. </button>
  427. {/if}
  428. <button
  429. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  430. 'about'
  431. ? 'bg-gray-200 dark:bg-gray-700'
  432. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  433. on:click={() => {
  434. selectedTab = 'about';
  435. }}
  436. >
  437. <div class=" self-center mr-2">
  438. <svg
  439. xmlns="http://www.w3.org/2000/svg"
  440. viewBox="0 0 20 20"
  441. fill="currentColor"
  442. class="w-4 h-4"
  443. >
  444. <path
  445. fill-rule="evenodd"
  446. 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"
  447. clip-rule="evenodd"
  448. />
  449. </svg>
  450. </div>
  451. <div class=" self-center">About</div>
  452. </button>
  453. </div>
  454. <div class="flex-1 md:min-h-[340px]">
  455. {#if selectedTab === 'general'}
  456. <div class="flex flex-col space-y-3">
  457. <div>
  458. <div class=" py-1 flex w-full justify-between">
  459. <div class=" self-center text-sm font-medium">Theme</div>
  460. <button
  461. class="p-1 px-3 text-xs flex rounded transition"
  462. on:click={() => {
  463. toggleTheme();
  464. }}
  465. >
  466. {#if theme === 'dark'}
  467. <svg
  468. xmlns="http://www.w3.org/2000/svg"
  469. viewBox="0 0 20 20"
  470. fill="currentColor"
  471. class="w-4 h-4"
  472. >
  473. <path
  474. fill-rule="evenodd"
  475. 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"
  476. clip-rule="evenodd"
  477. />
  478. </svg>
  479. <span class="ml-2 self-center"> Dark </span>
  480. {:else}
  481. <svg
  482. xmlns="http://www.w3.org/2000/svg"
  483. viewBox="0 0 20 20"
  484. fill="currentColor"
  485. class="w-4 h-4 self-center"
  486. >
  487. <path
  488. 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"
  489. />
  490. </svg>
  491. <span class="ml-2 self-center"> Light </span>
  492. {/if}
  493. </button>
  494. </div>
  495. </div>
  496. <hr class=" dark:border-gray-700" />
  497. <div>
  498. <div class=" mb-2.5 text-sm font-medium">Ollama Server URL</div>
  499. <div class="flex w-full">
  500. <div class="flex-1 mr-2">
  501. <input
  502. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  503. placeholder="Enter URL (e.g. http://localhost:11434/api)"
  504. bind:value={API_BASE_URL}
  505. />
  506. </div>
  507. <button
  508. class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded transition"
  509. on:click={() => {
  510. checkOllamaConnection();
  511. }}
  512. >
  513. <svg
  514. xmlns="http://www.w3.org/2000/svg"
  515. viewBox="0 0 20 20"
  516. fill="currentColor"
  517. class="w-4 h-4"
  518. >
  519. <path
  520. fill-rule="evenodd"
  521. 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"
  522. clip-rule="evenodd"
  523. />
  524. </svg>
  525. </button>
  526. </div>
  527. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  528. Trouble accessing Ollama? <a
  529. class=" text-gray-500 dark:text-gray-300 font-medium"
  530. href="https://github.com/ollama-webui/ollama-webui#troubleshooting"
  531. target="_blank"
  532. >
  533. Click here for help.
  534. </a>
  535. </div>
  536. </div>
  537. <hr class=" dark:border-gray-700" />
  538. <div>
  539. <div class=" mb-2.5 text-sm font-medium">System Prompt</div>
  540. <textarea
  541. bind:value={system}
  542. class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
  543. rows="4"
  544. />
  545. </div>
  546. <div class="flex justify-end pt-3 text-sm font-medium">
  547. <button
  548. class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
  549. on:click={() => {
  550. saveSettings({
  551. API_BASE_URL: API_BASE_URL === '' ? OLLAMA_API_BASE_URL : API_BASE_URL,
  552. system: system !== '' ? system : undefined
  553. });
  554. show = false;
  555. }}
  556. >
  557. Save
  558. </button>
  559. </div>
  560. </div>
  561. {:else if selectedTab === 'advanced'}
  562. <div class="flex flex-col h-full justify-between text-sm">
  563. <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-72">
  564. <div class=" text-sm font-medium">Parameters</div>
  565. <Advanced bind:options />
  566. <hr class=" dark:border-gray-700" />
  567. <div>
  568. <div class=" py-1 flex w-full justify-between">
  569. <div class=" self-center text-sm font-medium">Request Mode</div>
  570. <button
  571. class="p-1 px-3 text-xs flex rounded transition"
  572. on:click={() => {
  573. toggleRequestFormat();
  574. }}
  575. >
  576. {#if requestFormat === ''}
  577. <span class="ml-2 self-center"> Default </span>
  578. {:else if requestFormat === 'json'}
  579. <!-- <svg
  580. xmlns="http://www.w3.org/2000/svg"
  581. viewBox="0 0 20 20"
  582. fill="currentColor"
  583. class="w-4 h-4 self-center"
  584. >
  585. <path
  586. 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"
  587. />
  588. </svg> -->
  589. <span class="ml-2 self-center"> JSON </span>
  590. {/if}
  591. </button>
  592. </div>
  593. </div>
  594. </div>
  595. <div class="flex justify-end pt-3 text-sm font-medium">
  596. <button
  597. class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
  598. on:click={() => {
  599. saveSettings({
  600. options: {
  601. seed: (options.seed !== 0 ? options.seed : undefined) ?? undefined,
  602. stop: options.stop !== '' ? options.stop : undefined,
  603. temperature: options.temperature !== '' ? options.temperature : undefined,
  604. repeat_penalty:
  605. options.repeat_penalty !== '' ? options.repeat_penalty : undefined,
  606. repeat_last_n:
  607. options.repeat_last_n !== '' ? options.repeat_last_n : undefined,
  608. mirostat: options.mirostat !== '' ? options.mirostat : undefined,
  609. mirostat_eta: options.mirostat_eta !== '' ? options.mirostat_eta : undefined,
  610. mirostat_tau: options.mirostat_tau !== '' ? options.mirostat_tau : undefined,
  611. top_k: options.top_k !== '' ? options.top_k : undefined,
  612. top_p: options.top_p !== '' ? options.top_p : undefined,
  613. tfs_z: options.tfs_z !== '' ? options.tfs_z : undefined,
  614. num_ctx: options.num_ctx !== '' ? options.num_ctx : undefined
  615. }
  616. });
  617. show = false;
  618. }}
  619. >
  620. Save
  621. </button>
  622. </div>
  623. </div>
  624. {:else if selectedTab === 'models'}
  625. <div class="flex flex-col space-y-3 text-sm mb-10">
  626. <div>
  627. <div class=" mb-2.5 text-sm font-medium">Pull a model</div>
  628. <div class="flex w-full">
  629. <div class="flex-1 mr-2">
  630. <input
  631. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  632. placeholder="Enter model tag (e.g. mistral:7b)"
  633. bind:value={modelTag}
  634. />
  635. </div>
  636. <button
  637. class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 rounded transition"
  638. on:click={() => {
  639. pullModelHandler();
  640. }}
  641. >
  642. <svg
  643. xmlns="http://www.w3.org/2000/svg"
  644. viewBox="0 0 20 20"
  645. fill="currentColor"
  646. class="w-4 h-4"
  647. >
  648. <path
  649. 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"
  650. />
  651. <path
  652. 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"
  653. />
  654. </svg>
  655. </button>
  656. </div>
  657. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  658. To access the available model names for downloading, <a
  659. class=" text-gray-500 dark:text-gray-300 font-medium"
  660. href="https://ollama.ai/library"
  661. target="_blank">click here.</a
  662. >
  663. </div>
  664. {#if pullProgress !== null}
  665. <div class="mt-2">
  666. <div class=" mb-2 text-xs">Pull Progress</div>
  667. <div class="w-full rounded-full dark:bg-gray-800">
  668. <div
  669. class="dark:bg-gray-600 text-xs font-medium text-blue-100 text-center p-0.5 leading-none rounded-full"
  670. style="width: {Math.max(15, pullProgress ?? 0)}%"
  671. >
  672. {pullProgress ?? 0}%
  673. </div>
  674. </div>
  675. <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
  676. {digest}
  677. </div>
  678. </div>
  679. {/if}
  680. </div>
  681. <hr class=" dark:border-gray-700" />
  682. <div>
  683. <div class=" mb-2.5 text-sm font-medium">Delete a model</div>
  684. <div class="flex w-full">
  685. <div class="flex-1 mr-2">
  686. <select
  687. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  688. bind:value={deleteModelTag}
  689. placeholder="Select a model"
  690. >
  691. {#if !deleteModelTag}
  692. <option value="" disabled selected>Select a model</option>
  693. {/if}
  694. {#each $models.filter((m) => m.size != null) as model}
  695. <option value={model.name} class="bg-gray-100 dark:bg-gray-700"
  696. >{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option
  697. >
  698. {/each}
  699. </select>
  700. </div>
  701. <button
  702. class="px-3 bg-red-700 hover:bg-red-800 text-gray-100 rounded transition"
  703. on:click={() => {
  704. deleteModelHandler();
  705. }}
  706. >
  707. <svg
  708. xmlns="http://www.w3.org/2000/svg"
  709. viewBox="0 0 20 20"
  710. fill="currentColor"
  711. class="w-4 h-4"
  712. >
  713. <path
  714. fill-rule="evenodd"
  715. 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"
  716. clip-rule="evenodd"
  717. />
  718. </svg>
  719. </button>
  720. </div>
  721. </div>
  722. </div>
  723. {:else if selectedTab === 'addons'}
  724. <form
  725. class="flex flex-col h-full justify-between space-y-3 text-sm"
  726. on:submit|preventDefault={() => {
  727. saveSettings({
  728. gravatarEmail: gravatarEmail !== '' ? gravatarEmail : undefined,
  729. gravatarUrl: gravatarEmail !== '' ? getGravatarURL(gravatarEmail) : undefined,
  730. OPENAI_API_KEY: OPENAI_API_KEY !== '' ? OPENAI_API_KEY : undefined
  731. });
  732. show = false;
  733. }}
  734. >
  735. <div class=" space-y-3">
  736. <div>
  737. <div class=" py-1 flex w-full justify-between">
  738. <div class=" self-center text-sm font-medium">Title Auto Generation</div>
  739. <button
  740. class="p-1 px-3 text-xs flex rounded transition"
  741. on:click={() => {
  742. toggleTitleAutoGenerate();
  743. }}
  744. type="button"
  745. >
  746. {#if titleAutoGenerate === true}
  747. <span class="ml-2 self-center">On</span>
  748. {:else}
  749. <span class="ml-2 self-center">Off</span>
  750. {/if}
  751. </button>
  752. </div>
  753. </div>
  754. <hr class=" dark:border-gray-700" />
  755. <div>
  756. <div class=" py-1 flex w-full justify-between">
  757. <div class=" self-center text-sm font-medium">Voice Input Auto-Send</div>
  758. <button
  759. class="p-1 px-3 text-xs flex rounded transition"
  760. on:click={() => {
  761. toggleSpeechAutoSend();
  762. }}
  763. type="button"
  764. >
  765. {#if speechAutoSend === true}
  766. <span class="ml-2 self-center">On</span>
  767. {:else}
  768. <span class="ml-2 self-center">Off</span>
  769. {/if}
  770. </button>
  771. </div>
  772. </div>
  773. <hr class=" dark:border-gray-700" />
  774. <div>
  775. <div class=" mb-2.5 text-sm font-medium">
  776. Gravatar Email <span class=" text-gray-400 text-sm">(optional)</span>
  777. </div>
  778. <div class="flex w-full">
  779. <div class="flex-1">
  780. <input
  781. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  782. placeholder="Enter Your Email"
  783. bind:value={gravatarEmail}
  784. autocomplete="off"
  785. type="email"
  786. />
  787. </div>
  788. </div>
  789. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  790. Changes user profile image to match your <a
  791. class=" text-gray-500 dark:text-gray-300 font-medium"
  792. href="https://gravatar.com/"
  793. target="_blank">Gravatar.</a
  794. >
  795. </div>
  796. </div>
  797. <hr class=" dark:border-gray-700" />
  798. <div>
  799. <div class=" mb-2.5 text-sm font-medium">
  800. OpenAI API Key <span class=" text-gray-400 text-sm">(optional)</span>
  801. </div>
  802. <div class="flex w-full">
  803. <div class="flex-1">
  804. <input
  805. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  806. placeholder="Enter OpenAI API Key"
  807. bind:value={OPENAI_API_KEY}
  808. autocomplete="off"
  809. />
  810. </div>
  811. </div>
  812. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  813. Adds optional support for 'gpt-*' models available.
  814. </div>
  815. </div>
  816. </div>
  817. <div class="flex justify-end pt-3 text-sm font-medium">
  818. <button
  819. class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
  820. type="submit"
  821. >
  822. Save
  823. </button>
  824. </div>
  825. </form>
  826. {:else if selectedTab === 'auth'}
  827. <form
  828. class="flex flex-col h-full justify-between space-y-3 text-sm"
  829. on:submit|preventDefault={() => {
  830. console.log('auth save');
  831. saveSettings({
  832. authHeader: authEnabled ? `${authType} ${authContent}` : undefined
  833. });
  834. show = false;
  835. }}
  836. >
  837. <div class=" space-y-3">
  838. <div>
  839. <div class=" py-1 flex w-full justify-between">
  840. <div class=" self-center text-sm font-medium">Authorization Header</div>
  841. <button
  842. class="p-1 px-3 text-xs flex rounded transition"
  843. type="button"
  844. on:click={() => {
  845. toggleAuthHeader();
  846. }}
  847. >
  848. {#if authEnabled === true}
  849. <svg
  850. xmlns="http://www.w3.org/2000/svg"
  851. viewBox="0 0 24 24"
  852. fill="currentColor"
  853. class="w-4 h-4"
  854. >
  855. <path
  856. fill-rule="evenodd"
  857. 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"
  858. clip-rule="evenodd"
  859. />
  860. </svg>
  861. <span class="ml-2 self-center"> On </span>
  862. {:else}
  863. <svg
  864. xmlns="http://www.w3.org/2000/svg"
  865. viewBox="0 0 24 24"
  866. fill="currentColor"
  867. class="w-4 h-4"
  868. >
  869. <path
  870. 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"
  871. />
  872. </svg>
  873. <span class="ml-2 self-center">Off</span>
  874. {/if}
  875. </button>
  876. </div>
  877. </div>
  878. {#if authEnabled}
  879. <hr class=" dark:border-gray-700" />
  880. <div class="mt-2">
  881. <div class=" py-1 flex w-full space-x-2">
  882. <button
  883. class=" py-1 font-semibold flex rounded transition"
  884. on:click={() => {
  885. authType = authType === 'Basic' ? 'Bearer' : 'Basic';
  886. }}
  887. type="button"
  888. >
  889. {#if authType === 'Basic'}
  890. <span class="self-center mr-2">Basic</span>
  891. {:else if authType === 'Bearer'}
  892. <span class="self-center mr-2">Bearer</span>
  893. {/if}
  894. </button>
  895. <div class="flex-1">
  896. <input
  897. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  898. placeholder="Enter Authorization Header Content"
  899. bind:value={authContent}
  900. />
  901. </div>
  902. </div>
  903. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  904. Toggle between <span class=" text-gray-500 dark:text-gray-300 font-medium"
  905. >'Basic'</span
  906. >
  907. and <span class=" text-gray-500 dark:text-gray-300 font-medium">'Bearer'</span> by
  908. clicking on the label next to the input.
  909. </div>
  910. </div>
  911. <hr class=" dark:border-gray-700" />
  912. <div>
  913. <div class=" mb-2.5 text-sm font-medium">Preview Authorization Header</div>
  914. <textarea
  915. value={JSON.stringify({
  916. Authorization: `${authType} ${authContent}`
  917. })}
  918. class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
  919. rows="2"
  920. disabled
  921. />
  922. </div>
  923. {/if}
  924. </div>
  925. <div class="flex justify-end pt-3 text-sm font-medium">
  926. <button
  927. class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
  928. type="submit"
  929. >
  930. Save
  931. </button>
  932. </div>
  933. </form>
  934. {:else if selectedTab === 'about'}
  935. <div class="flex flex-col h-full justify-between space-y-3 text-sm mb-6">
  936. <div class=" space-y-3">
  937. <div>
  938. <div class=" mb-2.5 text-sm font-medium">Ollama Web UI Version</div>
  939. <div class="flex w-full">
  940. <div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
  941. {$config && $config.version ? $config.version : WEB_UI_VERSION}
  942. </div>
  943. </div>
  944. </div>
  945. <hr class=" dark:border-gray-700" />
  946. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  947. Created by <a
  948. class=" text-gray-500 dark:text-gray-300 font-medium"
  949. href="https://github.com/tjbck"
  950. target="_blank">Timothy J. Baek</a
  951. >
  952. </div>
  953. <div>
  954. <a href="https://github.com/ollama-webui/ollama-webui">
  955. <img
  956. alt="Github Repo"
  957. src="https://img.shields.io/github/stars/ollama-webui/ollama-webui?style=social&label=Star us on Github"
  958. />
  959. </a>
  960. </div>
  961. </div>
  962. </div>
  963. {/if}
  964. </div>
  965. </div>
  966. </div>
  967. </Modal>
  968. <style>
  969. input::-webkit-outer-spin-button,
  970. input::-webkit-inner-spin-button {
  971. /* display: none; <- Crashes Chrome on hover */
  972. -webkit-appearance: none;
  973. margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
  974. }
  975. .tabs::-webkit-scrollbar {
  976. display: none; /* for Chrome, Safari and Opera */
  977. }
  978. .tabs {
  979. -ms-overflow-style: none; /* IE and Edge */
  980. scrollbar-width: none; /* Firefox */
  981. }
  982. input[type='number'] {
  983. -moz-appearance: textfield; /* Firefox */
  984. }
  985. </style>