SettingsModal.svelte 53 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657
  1. <script lang="ts">
  2. import Modal from '../common/Modal.svelte';
  3. import {
  4. WEB_UI_VERSION,
  5. OLLAMA_API_BASE_URL,
  6. WEBUI_API_BASE_URL,
  7. WEBUI_BASE_URL
  8. } from '$lib/constants';
  9. import toast from 'svelte-french-toast';
  10. import { onMount } from 'svelte';
  11. import { config, info, models, settings, user } from '$lib/stores';
  12. import { splitStream, getGravatarURL } from '$lib/utils';
  13. import Advanced from './Settings/Advanced.svelte';
  14. import { stringify } from 'postcss';
  15. export let show = false;
  16. const saveSettings = async (updated) => {
  17. console.log(updated);
  18. await settings.set({ ...$settings, ...updated });
  19. await models.set(await getModels());
  20. localStorage.setItem('settings', JSON.stringify($settings));
  21. };
  22. let selectedTab = 'general';
  23. // General
  24. let API_BASE_URL = OLLAMA_API_BASE_URL;
  25. let theme = 'dark';
  26. let notificationEnabled = false;
  27. let system = '';
  28. // Advanced
  29. let requestFormat = '';
  30. let options = {
  31. // Advanced
  32. seed: 0,
  33. temperature: '',
  34. repeat_penalty: '',
  35. repeat_last_n: '',
  36. mirostat: '',
  37. mirostat_eta: '',
  38. mirostat_tau: '',
  39. top_k: '',
  40. top_p: '',
  41. stop: '',
  42. tfs_z: '',
  43. num_ctx: ''
  44. };
  45. // Models
  46. let modelTransferring = false;
  47. let modelTag = '';
  48. let digest = '';
  49. let pullProgress = null;
  50. let modelUploadMode = 'file';
  51. let modelInputFile = '';
  52. let modelFileUrl = '';
  53. let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSSISTANT:"`;
  54. let modelFileDigest = '';
  55. let uploadProgress = null;
  56. let deleteModelTag = '';
  57. // Addons
  58. let titleAutoGenerate = true;
  59. let speechAutoSend = false;
  60. let responseAutoCopy = false;
  61. let gravatarEmail = '';
  62. let OPENAI_API_KEY = '';
  63. let OPENAI_API_BASE_URL = '';
  64. // Auth
  65. let authEnabled = false;
  66. let authType = 'Basic';
  67. let authContent = '';
  68. const checkOllamaConnection = async () => {
  69. if (API_BASE_URL === '') {
  70. API_BASE_URL = OLLAMA_API_BASE_URL;
  71. }
  72. const _models = await getModels(API_BASE_URL, 'ollama');
  73. if (_models.length > 0) {
  74. toast.success('Server connection verified');
  75. await models.set(_models);
  76. saveSettings({
  77. API_BASE_URL: API_BASE_URL
  78. });
  79. }
  80. };
  81. const toggleTheme = async () => {
  82. if (theme === 'dark') {
  83. theme = 'light';
  84. } else {
  85. theme = 'dark';
  86. }
  87. localStorage.theme = theme;
  88. document.documentElement.classList.remove(theme === 'dark' ? 'light' : 'dark');
  89. document.documentElement.classList.add(theme);
  90. };
  91. const toggleRequestFormat = async () => {
  92. if (requestFormat === '') {
  93. requestFormat = 'json';
  94. } else {
  95. requestFormat = '';
  96. }
  97. saveSettings({ requestFormat: requestFormat !== '' ? requestFormat : undefined });
  98. };
  99. const toggleSpeechAutoSend = async () => {
  100. speechAutoSend = !speechAutoSend;
  101. saveSettings({ speechAutoSend: speechAutoSend });
  102. };
  103. const toggleTitleAutoGenerate = async () => {
  104. titleAutoGenerate = !titleAutoGenerate;
  105. saveSettings({ titleAutoGenerate: titleAutoGenerate });
  106. };
  107. const toggleNotification = async () => {
  108. const permission = await Notification.requestPermission();
  109. if (permission === 'granted') {
  110. notificationEnabled = !notificationEnabled;
  111. saveSettings({ notificationEnabled: notificationEnabled });
  112. } else {
  113. toast.error(
  114. 'Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.'
  115. );
  116. }
  117. };
  118. const toggleResponseAutoCopy = async () => {
  119. const permission = await navigator.clipboard
  120. .readText()
  121. .then(() => {
  122. return 'granted';
  123. })
  124. .catch(() => {
  125. return '';
  126. });
  127. console.log(permission);
  128. if (permission === 'granted') {
  129. responseAutoCopy = !responseAutoCopy;
  130. saveSettings({ responseAutoCopy: responseAutoCopy });
  131. } else {
  132. toast.error(
  133. 'Clipboard write permission denied. Please check your browser settings to grant the necessary access.'
  134. );
  135. }
  136. };
  137. const toggleAuthHeader = async () => {
  138. authEnabled = !authEnabled;
  139. };
  140. const pullModelHandler = async () => {
  141. modelTransferring = true;
  142. const res = await fetch(`${API_BASE_URL}/pull`, {
  143. method: 'POST',
  144. headers: {
  145. 'Content-Type': 'text/event-stream',
  146. ...($settings.authHeader && { Authorization: $settings.authHeader }),
  147. ...($user && { Authorization: `Bearer ${localStorage.token}` })
  148. },
  149. body: JSON.stringify({
  150. name: modelTag
  151. })
  152. });
  153. const reader = res.body
  154. .pipeThrough(new TextDecoderStream())
  155. .pipeThrough(splitStream('\n'))
  156. .getReader();
  157. while (true) {
  158. const { value, done } = await reader.read();
  159. if (done) break;
  160. try {
  161. let lines = value.split('\n');
  162. for (const line of lines) {
  163. if (line !== '') {
  164. console.log(line);
  165. let data = JSON.parse(line);
  166. console.log(data);
  167. if (data.error) {
  168. throw data.error;
  169. }
  170. if (data.detail) {
  171. throw data.detail;
  172. }
  173. if (data.status) {
  174. if (!data.digest) {
  175. toast.success(data.status);
  176. if (data.status === 'success') {
  177. const notification = new Notification(`Ollama`, {
  178. body: `Model '${modelTag}' has been successfully downloaded.`,
  179. icon: '/favicon.png'
  180. });
  181. }
  182. } else {
  183. digest = data.digest;
  184. if (data.completed) {
  185. pullProgress = Math.round((data.completed / data.total) * 1000) / 10;
  186. } else {
  187. pullProgress = 100;
  188. }
  189. }
  190. }
  191. }
  192. }
  193. } catch (error) {
  194. console.log(error);
  195. toast.error(error);
  196. }
  197. }
  198. modelTag = '';
  199. modelTransferring = false;
  200. models.set(await getModels());
  201. };
  202. const calculateSHA256 = async (file) => {
  203. console.log(file);
  204. // Create a FileReader to read the file asynchronously
  205. const reader = new FileReader();
  206. // Define a promise to handle the file reading
  207. const readFile = new Promise((resolve, reject) => {
  208. reader.onload = () => resolve(reader.result);
  209. reader.onerror = reject;
  210. });
  211. // Read the file as an ArrayBuffer
  212. reader.readAsArrayBuffer(file);
  213. try {
  214. // Wait for the FileReader to finish reading the file
  215. const buffer = await readFile;
  216. // Convert the ArrayBuffer to a Uint8Array
  217. const uint8Array = new Uint8Array(buffer);
  218. // Calculate the SHA-256 hash using Web Crypto API
  219. const hashBuffer = await crypto.subtle.digest('SHA-256', uint8Array);
  220. // Convert the hash to a hexadecimal string
  221. const hashArray = Array.from(new Uint8Array(hashBuffer));
  222. const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, '0')).join('');
  223. return `sha256:${hashHex}`;
  224. } catch (error) {
  225. console.error('Error calculating SHA-256 hash:', error);
  226. throw error;
  227. }
  228. };
  229. const uploadModelHandler = async () => {
  230. modelTransferring = true;
  231. let uploaded = false;
  232. let fileResponse = null;
  233. let name = '';
  234. if (modelUploadMode === 'file') {
  235. const file = modelInputFile[0];
  236. const formData = new FormData();
  237. formData.append('file', file);
  238. fileResponse = await fetch(`${WEBUI_API_BASE_URL}/utils/upload`, {
  239. method: 'POST',
  240. headers: {
  241. ...($settings.authHeader && { Authorization: $settings.authHeader }),
  242. ...($user && { Authorization: `Bearer ${localStorage.token}` })
  243. },
  244. body: formData
  245. }).catch((error) => {
  246. console.log(error);
  247. return null;
  248. });
  249. } else {
  250. fileResponse = await fetch(`${WEBUI_API_BASE_URL}/utils/download?url=${modelFileUrl}`, {
  251. method: 'GET',
  252. headers: {
  253. ...($settings.authHeader && { Authorization: $settings.authHeader }),
  254. ...($user && { Authorization: `Bearer ${localStorage.token}` })
  255. }
  256. }).catch((error) => {
  257. console.log(error);
  258. return null;
  259. });
  260. }
  261. if (fileResponse && fileResponse.ok) {
  262. const reader = fileResponse.body
  263. .pipeThrough(new TextDecoderStream())
  264. .pipeThrough(splitStream('\n'))
  265. .getReader();
  266. while (true) {
  267. const { value, done } = await reader.read();
  268. if (done) break;
  269. try {
  270. let lines = value.split('\n');
  271. for (const line of lines) {
  272. if (line !== '') {
  273. let data = JSON.parse(line.replace(/^data: /, ''));
  274. if (data.progress) {
  275. uploadProgress = data.progress;
  276. }
  277. if (data.error) {
  278. throw data.error;
  279. }
  280. if (data.done) {
  281. modelFileDigest = data.blob;
  282. name = data.name;
  283. uploaded = true;
  284. }
  285. }
  286. }
  287. } catch (error) {
  288. console.log(error);
  289. }
  290. }
  291. }
  292. if (uploaded) {
  293. const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/create`, {
  294. method: 'POST',
  295. headers: {
  296. 'Content-Type': 'text/event-stream',
  297. ...($settings.authHeader && { Authorization: $settings.authHeader }),
  298. ...($user && { Authorization: `Bearer ${localStorage.token}` })
  299. },
  300. body: JSON.stringify({
  301. name: `${name}:latest`,
  302. modelfile: `FROM @${modelFileDigest}\n${modelFileContent}`
  303. })
  304. }).catch((err) => {
  305. console.log(err);
  306. return null;
  307. });
  308. if (res && res.ok) {
  309. const reader = res.body
  310. .pipeThrough(new TextDecoderStream())
  311. .pipeThrough(splitStream('\n'))
  312. .getReader();
  313. while (true) {
  314. const { value, done } = await reader.read();
  315. if (done) break;
  316. try {
  317. let lines = value.split('\n');
  318. for (const line of lines) {
  319. if (line !== '') {
  320. console.log(line);
  321. let data = JSON.parse(line);
  322. console.log(data);
  323. if (data.error) {
  324. throw data.error;
  325. }
  326. if (data.detail) {
  327. throw data.detail;
  328. }
  329. if (data.status) {
  330. if (
  331. !data.digest &&
  332. !data.status.includes('writing') &&
  333. !data.status.includes('sha256')
  334. ) {
  335. toast.success(data.status);
  336. } else {
  337. if (data.digest) {
  338. digest = data.digest;
  339. if (data.completed) {
  340. pullProgress = Math.round((data.completed / data.total) * 1000) / 10;
  341. } else {
  342. pullProgress = 100;
  343. }
  344. }
  345. }
  346. }
  347. }
  348. }
  349. } catch (error) {
  350. console.log(error);
  351. toast.error(error);
  352. }
  353. }
  354. }
  355. }
  356. modelFileUrl = '';
  357. modelInputFile = '';
  358. modelTransferring = false;
  359. models.set(await getModels());
  360. };
  361. const deleteModelHandler = async () => {
  362. const res = await fetch(`${API_BASE_URL}/delete`, {
  363. method: 'DELETE',
  364. headers: {
  365. 'Content-Type': 'text/event-stream',
  366. ...($settings.authHeader && { Authorization: $settings.authHeader }),
  367. ...($user && { Authorization: `Bearer ${localStorage.token}` })
  368. },
  369. body: JSON.stringify({
  370. name: deleteModelTag
  371. })
  372. });
  373. const reader = res.body
  374. .pipeThrough(new TextDecoderStream())
  375. .pipeThrough(splitStream('\n'))
  376. .getReader();
  377. while (true) {
  378. const { value, done } = await reader.read();
  379. if (done) break;
  380. try {
  381. let lines = value.split('\n');
  382. for (const line of lines) {
  383. if (line !== '' && line !== 'null') {
  384. console.log(line);
  385. let data = JSON.parse(line);
  386. console.log(data);
  387. if (data.error) {
  388. throw data.error;
  389. }
  390. if (data.detail) {
  391. throw data.detail;
  392. }
  393. if (data.status) {
  394. }
  395. } else {
  396. toast.success(`Deleted ${deleteModelTag}`);
  397. }
  398. }
  399. } catch (error) {
  400. console.log(error);
  401. toast.error(error);
  402. }
  403. }
  404. deleteModelTag = '';
  405. models.set(await getModels());
  406. };
  407. const getModels = async (url = '', type = 'all') => {
  408. let models = [];
  409. const res = await fetch(`${url ? url : $settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/tags`, {
  410. method: 'GET',
  411. headers: {
  412. Accept: 'application/json',
  413. 'Content-Type': 'application/json',
  414. ...($settings.authHeader && { Authorization: $settings.authHeader }),
  415. ...($user && { Authorization: `Bearer ${localStorage.token}` })
  416. }
  417. })
  418. .then(async (res) => {
  419. if (!res.ok) throw await res.json();
  420. return res.json();
  421. })
  422. .catch((error) => {
  423. console.log(error);
  424. if ('detail' in error) {
  425. toast.error(error.detail);
  426. } else {
  427. toast.error('Server connection failed');
  428. }
  429. return null;
  430. });
  431. console.log(res);
  432. models.push(...(res?.models ?? []));
  433. // If OpenAI API Key exists
  434. if (type === 'all' && $settings.OPENAI_API_KEY) {
  435. const API_BASE_URL = $settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1';
  436. // Validate OPENAI_API_KEY
  437. const openaiModelRes = await fetch(`${API_BASE_URL}/models`, {
  438. method: 'GET',
  439. headers: {
  440. 'Content-Type': 'application/json',
  441. Authorization: `Bearer ${$settings.OPENAI_API_KEY}`
  442. }
  443. })
  444. .then(async (res) => {
  445. if (!res.ok) throw await res.json();
  446. return res.json();
  447. })
  448. .catch((error) => {
  449. console.log(error);
  450. toast.error(`OpenAI: ${error?.error?.message ?? 'Network Problem'}`);
  451. return null;
  452. });
  453. const openAIModels = Array.isArray(openaiModelRes)
  454. ? openaiModelRes
  455. : openaiModelRes?.data ?? null;
  456. models.push(
  457. ...(openAIModels
  458. ? [
  459. { name: 'hr' },
  460. ...openAIModels
  461. .map((model) => ({ name: model.id, external: true }))
  462. .filter((model) =>
  463. API_BASE_URL.includes('openai') ? model.name.includes('gpt') : true
  464. )
  465. ]
  466. : [])
  467. );
  468. }
  469. return models;
  470. };
  471. onMount(() => {
  472. let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
  473. console.log(settings);
  474. theme = localStorage.theme ?? 'dark';
  475. notificationEnabled = settings.notificationEnabled ?? false;
  476. API_BASE_URL = settings.API_BASE_URL ?? OLLAMA_API_BASE_URL;
  477. system = settings.system ?? '';
  478. requestFormat = settings.requestFormat ?? '';
  479. options.seed = settings.seed ?? 0;
  480. options.temperature = settings.temperature ?? '';
  481. options.repeat_penalty = settings.repeat_penalty ?? '';
  482. options.top_k = settings.top_k ?? '';
  483. options.top_p = settings.top_p ?? '';
  484. options.num_ctx = settings.num_ctx ?? '';
  485. options = { ...options, ...settings.options };
  486. titleAutoGenerate = settings.titleAutoGenerate ?? true;
  487. speechAutoSend = settings.speechAutoSend ?? false;
  488. responseAutoCopy = settings.responseAutoCopy ?? false;
  489. gravatarEmail = settings.gravatarEmail ?? '';
  490. OPENAI_API_KEY = settings.OPENAI_API_KEY ?? '';
  491. OPENAI_API_BASE_URL = settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1';
  492. authEnabled = settings.authHeader !== undefined ? true : false;
  493. if (authEnabled) {
  494. authType = settings.authHeader.split(' ')[0];
  495. authContent = settings.authHeader.split(' ')[1];
  496. }
  497. });
  498. </script>
  499. <Modal bind:show>
  500. <div>
  501. <div class=" flex justify-between dark:text-gray-300 px-5 py-4">
  502. <div class=" text-lg font-medium self-center">Settings</div>
  503. <button
  504. class="self-center"
  505. on:click={() => {
  506. show = false;
  507. }}
  508. >
  509. <svg
  510. xmlns="http://www.w3.org/2000/svg"
  511. viewBox="0 0 20 20"
  512. fill="currentColor"
  513. class="w-5 h-5"
  514. >
  515. <path
  516. 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"
  517. />
  518. </svg>
  519. </button>
  520. </div>
  521. <hr class=" dark:border-gray-800" />
  522. <div class="flex flex-col md:flex-row w-full p-4 md:space-x-4">
  523. <div
  524. 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"
  525. >
  526. <button
  527. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  528. 'general'
  529. ? 'bg-gray-200 dark:bg-gray-700'
  530. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  531. on:click={() => {
  532. selectedTab = 'general';
  533. }}
  534. >
  535. <div class=" self-center mr-2">
  536. <svg
  537. xmlns="http://www.w3.org/2000/svg"
  538. viewBox="0 0 20 20"
  539. fill="currentColor"
  540. class="w-4 h-4"
  541. >
  542. <path
  543. fill-rule="evenodd"
  544. 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"
  545. clip-rule="evenodd"
  546. />
  547. </svg>
  548. </div>
  549. <div class=" self-center">General</div>
  550. </button>
  551. <button
  552. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  553. 'advanced'
  554. ? 'bg-gray-200 dark:bg-gray-700'
  555. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  556. on:click={() => {
  557. selectedTab = 'advanced';
  558. }}
  559. >
  560. <div class=" self-center mr-2">
  561. <svg
  562. xmlns="http://www.w3.org/2000/svg"
  563. viewBox="0 0 20 20"
  564. fill="currentColor"
  565. class="w-4 h-4"
  566. >
  567. <path
  568. 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"
  569. />
  570. </svg>
  571. </div>
  572. <div class=" self-center">Advanced</div>
  573. </button>
  574. <button
  575. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  576. 'models'
  577. ? 'bg-gray-200 dark:bg-gray-700'
  578. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  579. on:click={() => {
  580. selectedTab = 'models';
  581. }}
  582. >
  583. <div class=" self-center mr-2">
  584. <svg
  585. xmlns="http://www.w3.org/2000/svg"
  586. viewBox="0 0 20 20"
  587. fill="currentColor"
  588. class="w-4 h-4"
  589. >
  590. <path
  591. fill-rule="evenodd"
  592. 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"
  593. clip-rule="evenodd"
  594. />
  595. </svg>
  596. </div>
  597. <div class=" self-center">Models</div>
  598. </button>
  599. <button
  600. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  601. 'external'
  602. ? 'bg-gray-200 dark:bg-gray-700'
  603. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  604. on:click={() => {
  605. selectedTab = 'external';
  606. }}
  607. >
  608. <div class=" self-center mr-2">
  609. <svg
  610. xmlns="http://www.w3.org/2000/svg"
  611. viewBox="0 0 16 16"
  612. fill="currentColor"
  613. class="w-4 h-4"
  614. >
  615. <path
  616. d="M1 9.5A3.5 3.5 0 0 0 4.5 13H12a3 3 0 0 0 .917-5.857 2.503 2.503 0 0 0-3.198-3.019 3.5 3.5 0 0 0-6.628 2.171A3.5 3.5 0 0 0 1 9.5Z"
  617. />
  618. </svg>
  619. </div>
  620. <div class=" self-center">External</div>
  621. </button>
  622. <button
  623. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  624. 'addons'
  625. ? 'bg-gray-200 dark:bg-gray-700'
  626. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  627. on:click={() => {
  628. selectedTab = 'addons';
  629. }}
  630. >
  631. <div class=" self-center mr-2">
  632. <svg
  633. xmlns="http://www.w3.org/2000/svg"
  634. viewBox="0 0 20 20"
  635. fill="currentColor"
  636. class="w-4 h-4"
  637. >
  638. <path
  639. 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"
  640. />
  641. </svg>
  642. </div>
  643. <div class=" self-center">Add-ons</div>
  644. </button>
  645. {#if !$config || ($config && !$config.auth)}
  646. <button
  647. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  648. 'auth'
  649. ? 'bg-gray-200 dark:bg-gray-700'
  650. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  651. on:click={() => {
  652. selectedTab = 'auth';
  653. }}
  654. >
  655. <div class=" self-center mr-2">
  656. <svg
  657. xmlns="http://www.w3.org/2000/svg"
  658. viewBox="0 0 24 24"
  659. fill="currentColor"
  660. class="w-4 h-4"
  661. >
  662. <path
  663. fill-rule="evenodd"
  664. 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"
  665. clip-rule="evenodd"
  666. />
  667. </svg>
  668. </div>
  669. <div class=" self-center">Authentication</div>
  670. </button>
  671. {/if}
  672. <button
  673. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  674. 'about'
  675. ? 'bg-gray-200 dark:bg-gray-700'
  676. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  677. on:click={() => {
  678. selectedTab = 'about';
  679. }}
  680. >
  681. <div class=" self-center mr-2">
  682. <svg
  683. xmlns="http://www.w3.org/2000/svg"
  684. viewBox="0 0 20 20"
  685. fill="currentColor"
  686. class="w-4 h-4"
  687. >
  688. <path
  689. fill-rule="evenodd"
  690. 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"
  691. clip-rule="evenodd"
  692. />
  693. </svg>
  694. </div>
  695. <div class=" self-center">About</div>
  696. </button>
  697. </div>
  698. <div class="flex-1 md:min-h-[340px]">
  699. {#if selectedTab === 'general'}
  700. <div class="flex flex-col space-y-3">
  701. <div>
  702. <div class=" mb-1 text-sm font-medium">WebUI Settings</div>
  703. <div class=" py-0.5 flex w-full justify-between">
  704. <div class=" self-center text-xs font-medium">Theme</div>
  705. <button
  706. class="p-1 px-3 text-xs flex rounded transition"
  707. on:click={() => {
  708. toggleTheme();
  709. }}
  710. >
  711. {#if theme === 'dark'}
  712. <svg
  713. xmlns="http://www.w3.org/2000/svg"
  714. viewBox="0 0 20 20"
  715. fill="currentColor"
  716. class="w-4 h-4"
  717. >
  718. <path
  719. fill-rule="evenodd"
  720. 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"
  721. clip-rule="evenodd"
  722. />
  723. </svg>
  724. <span class="ml-2 self-center"> Dark </span>
  725. {:else}
  726. <svg
  727. xmlns="http://www.w3.org/2000/svg"
  728. viewBox="0 0 20 20"
  729. fill="currentColor"
  730. class="w-4 h-4 self-center"
  731. >
  732. <path
  733. 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"
  734. />
  735. </svg>
  736. <span class="ml-2 self-center"> Light </span>
  737. {/if}
  738. </button>
  739. </div>
  740. <div>
  741. <div class=" py-0.5 flex w-full justify-between">
  742. <div class=" self-center text-xs font-medium">Notification</div>
  743. <button
  744. class="p-1 px-3 text-xs flex rounded transition"
  745. on:click={() => {
  746. toggleNotification();
  747. }}
  748. type="button"
  749. >
  750. {#if notificationEnabled === true}
  751. <span class="ml-2 self-center">On</span>
  752. {:else}
  753. <span class="ml-2 self-center">Off</span>
  754. {/if}
  755. </button>
  756. </div>
  757. </div>
  758. </div>
  759. <hr class=" dark:border-gray-700" />
  760. <div>
  761. <div class=" mb-2.5 text-sm font-medium">Ollama Server URL</div>
  762. <div class="flex w-full">
  763. <div class="flex-1 mr-2">
  764. <input
  765. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  766. placeholder="Enter URL (e.g. http://localhost:11434/api)"
  767. bind:value={API_BASE_URL}
  768. />
  769. </div>
  770. <button
  771. class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded transition"
  772. on:click={() => {
  773. checkOllamaConnection();
  774. }}
  775. >
  776. <svg
  777. xmlns="http://www.w3.org/2000/svg"
  778. viewBox="0 0 20 20"
  779. fill="currentColor"
  780. class="w-4 h-4"
  781. >
  782. <path
  783. fill-rule="evenodd"
  784. 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"
  785. clip-rule="evenodd"
  786. />
  787. </svg>
  788. </button>
  789. </div>
  790. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  791. Trouble accessing Ollama? <a
  792. class=" text-gray-500 dark:text-gray-300 font-medium"
  793. href="https://github.com/ollama-webui/ollama-webui#troubleshooting"
  794. target="_blank"
  795. >
  796. Click here for help.
  797. </a>
  798. </div>
  799. </div>
  800. <hr class=" dark:border-gray-700" />
  801. <div>
  802. <div class=" mb-2.5 text-sm font-medium">System Prompt</div>
  803. <textarea
  804. bind:value={system}
  805. class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
  806. rows="4"
  807. />
  808. </div>
  809. <div class="flex justify-end pt-3 text-sm font-medium">
  810. <button
  811. class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
  812. on:click={() => {
  813. saveSettings({
  814. API_BASE_URL: API_BASE_URL === '' ? OLLAMA_API_BASE_URL : API_BASE_URL,
  815. system: system !== '' ? system : undefined
  816. });
  817. show = false;
  818. }}
  819. >
  820. Save
  821. </button>
  822. </div>
  823. </div>
  824. {:else if selectedTab === 'advanced'}
  825. <div class="flex flex-col h-full justify-between text-sm">
  826. <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-72">
  827. <div class=" text-sm font-medium">Parameters</div>
  828. <Advanced bind:options />
  829. <hr class=" dark:border-gray-700" />
  830. <div>
  831. <div class=" py-1 flex w-full justify-between">
  832. <div class=" self-center text-sm font-medium">Request Mode</div>
  833. <button
  834. class="p-1 px-3 text-xs flex rounded transition"
  835. on:click={() => {
  836. toggleRequestFormat();
  837. }}
  838. >
  839. {#if requestFormat === ''}
  840. <span class="ml-2 self-center"> Default </span>
  841. {:else if requestFormat === 'json'}
  842. <!-- <svg
  843. xmlns="http://www.w3.org/2000/svg"
  844. viewBox="0 0 20 20"
  845. fill="currentColor"
  846. class="w-4 h-4 self-center"
  847. >
  848. <path
  849. 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"
  850. />
  851. </svg> -->
  852. <span class="ml-2 self-center"> JSON </span>
  853. {/if}
  854. </button>
  855. </div>
  856. </div>
  857. </div>
  858. <div class="flex justify-end pt-3 text-sm font-medium">
  859. <button
  860. class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
  861. on:click={() => {
  862. saveSettings({
  863. options: {
  864. seed: (options.seed !== 0 ? options.seed : undefined) ?? undefined,
  865. stop: options.stop !== '' ? options.stop : undefined,
  866. temperature: options.temperature !== '' ? options.temperature : undefined,
  867. repeat_penalty:
  868. options.repeat_penalty !== '' ? options.repeat_penalty : undefined,
  869. repeat_last_n:
  870. options.repeat_last_n !== '' ? options.repeat_last_n : undefined,
  871. mirostat: options.mirostat !== '' ? options.mirostat : undefined,
  872. mirostat_eta: options.mirostat_eta !== '' ? options.mirostat_eta : undefined,
  873. mirostat_tau: options.mirostat_tau !== '' ? options.mirostat_tau : undefined,
  874. top_k: options.top_k !== '' ? options.top_k : undefined,
  875. top_p: options.top_p !== '' ? options.top_p : undefined,
  876. tfs_z: options.tfs_z !== '' ? options.tfs_z : undefined,
  877. num_ctx: options.num_ctx !== '' ? options.num_ctx : undefined
  878. }
  879. });
  880. show = false;
  881. }}
  882. >
  883. Save
  884. </button>
  885. </div>
  886. </div>
  887. {:else if selectedTab === 'models'}
  888. <div class="flex flex-col h-full justify-between text-sm">
  889. <div class=" space-y-3 pr-1.5 overflow-y-scroll h-80">
  890. <div>
  891. <div class=" mb-2.5 text-sm font-medium">Pull a model from Ollama.ai</div>
  892. <div class="flex w-full">
  893. <div class="flex-1 mr-2">
  894. <input
  895. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  896. placeholder="Enter model tag (e.g. mistral:7b)"
  897. bind:value={modelTag}
  898. />
  899. </div>
  900. <button
  901. class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded transition"
  902. on:click={() => {
  903. pullModelHandler();
  904. }}
  905. disabled={modelTransferring}
  906. >
  907. {#if modelTransferring}
  908. <div class="self-center">
  909. <svg
  910. class=" w-4 h-4"
  911. viewBox="0 0 24 24"
  912. fill="currentColor"
  913. xmlns="http://www.w3.org/2000/svg"
  914. ><style>
  915. .spinner_ajPY {
  916. transform-origin: center;
  917. animation: spinner_AtaB 0.75s infinite linear;
  918. }
  919. @keyframes spinner_AtaB {
  920. 100% {
  921. transform: rotate(360deg);
  922. }
  923. }
  924. </style><path
  925. d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
  926. opacity=".25"
  927. /><path
  928. d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
  929. class="spinner_ajPY"
  930. /></svg
  931. >
  932. </div>
  933. {:else}
  934. <svg
  935. xmlns="http://www.w3.org/2000/svg"
  936. viewBox="0 0 16 16"
  937. fill="currentColor"
  938. class="w-4 h-4"
  939. >
  940. <path
  941. d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
  942. />
  943. <path
  944. d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
  945. />
  946. </svg>
  947. {/if}
  948. </button>
  949. </div>
  950. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  951. To access the available model names for downloading, <a
  952. class=" text-gray-500 dark:text-gray-300 font-medium"
  953. href="https://ollama.ai/library"
  954. target="_blank">click here.</a
  955. >
  956. </div>
  957. {#if pullProgress !== null}
  958. <div class="mt-2">
  959. <div class=" mb-2 text-xs">Pull Progress</div>
  960. <div class="w-full rounded-full dark:bg-gray-800">
  961. <div
  962. class="dark:bg-gray-600 text-xs font-medium text-blue-100 text-center p-0.5 leading-none rounded-full"
  963. style="width: {Math.max(15, pullProgress ?? 0)}%"
  964. >
  965. {pullProgress ?? 0}%
  966. </div>
  967. </div>
  968. <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
  969. {digest}
  970. </div>
  971. </div>
  972. {/if}
  973. </div>
  974. <hr class=" dark:border-gray-700" />
  975. <form
  976. on:submit|preventDefault={() => {
  977. uploadModelHandler();
  978. }}
  979. >
  980. <div class=" mb-2 flex w-full justify-between">
  981. <div class=" text-sm font-medium">Upload a GGUF model</div>
  982. <button
  983. class="p-1 px-3 text-xs flex rounded transition"
  984. on:click={() => {
  985. if (modelUploadMode === 'file') {
  986. modelUploadMode = 'url';
  987. } else {
  988. modelUploadMode = 'file';
  989. }
  990. }}
  991. type="button"
  992. >
  993. {#if modelUploadMode === 'file'}
  994. <span class="ml-2 self-center">File Mode</span>
  995. {:else}
  996. <span class="ml-2 self-center">URL Mode</span>
  997. {/if}
  998. </button>
  999. </div>
  1000. <div class="flex w-full mb-1.5">
  1001. <div class="flex flex-col w-full">
  1002. {#if modelUploadMode === 'file'}
  1003. <div
  1004. class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}"
  1005. >
  1006. <input
  1007. id="model-upload-input"
  1008. type="file"
  1009. bind:files={modelInputFile}
  1010. on:change={() => {
  1011. console.log(modelInputFile);
  1012. }}
  1013. accept=".gguf"
  1014. required
  1015. hidden
  1016. />
  1017. <button
  1018. type="button"
  1019. class="w-full rounded text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-800"
  1020. on:click={() => {
  1021. document.getElementById('model-upload-input').click();
  1022. }}
  1023. >
  1024. {#if modelInputFile && modelInputFile.length > 0}
  1025. {modelInputFile[0].name}
  1026. {:else}
  1027. Click here to select
  1028. {/if}
  1029. </button>
  1030. </div>
  1031. {:else}
  1032. <div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
  1033. <input
  1034. class="w-full rounded text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-800 outline-none {modelFileUrl !==
  1035. ''
  1036. ? 'mr-2'
  1037. : ''}"
  1038. type="url"
  1039. required
  1040. bind:value={modelFileUrl}
  1041. placeholder="Type HuggingFace Resolve (Download) URL"
  1042. />
  1043. </div>
  1044. {/if}
  1045. </div>
  1046. {#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
  1047. <button
  1048. class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded transition"
  1049. type="submit"
  1050. disabled={modelTransferring}
  1051. >
  1052. {#if modelTransferring}
  1053. <div class="self-center">
  1054. <svg
  1055. class=" w-4 h-4"
  1056. viewBox="0 0 24 24"
  1057. fill="currentColor"
  1058. xmlns="http://www.w3.org/2000/svg"
  1059. ><style>
  1060. .spinner_ajPY {
  1061. transform-origin: center;
  1062. animation: spinner_AtaB 0.75s infinite linear;
  1063. }
  1064. @keyframes spinner_AtaB {
  1065. 100% {
  1066. transform: rotate(360deg);
  1067. }
  1068. }
  1069. </style><path
  1070. d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
  1071. opacity=".25"
  1072. /><path
  1073. d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
  1074. class="spinner_ajPY"
  1075. /></svg
  1076. >
  1077. </div>
  1078. {:else}
  1079. <svg
  1080. xmlns="http://www.w3.org/2000/svg"
  1081. viewBox="0 0 16 16"
  1082. fill="currentColor"
  1083. class="w-4 h-4"
  1084. >
  1085. <path
  1086. d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
  1087. />
  1088. <path
  1089. d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
  1090. />
  1091. </svg>
  1092. {/if}
  1093. </button>
  1094. {/if}
  1095. </div>
  1096. {#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
  1097. <div>
  1098. <div>
  1099. <div class=" my-2.5 text-sm font-medium">Modelfile Content</div>
  1100. <textarea
  1101. bind:value={modelFileContent}
  1102. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
  1103. rows="8"
  1104. />
  1105. </div>
  1106. </div>
  1107. {/if}
  1108. <div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
  1109. To access the GGUF models available for downloading, <a
  1110. class=" text-gray-500 dark:text-gray-300 font-medium"
  1111. href="https://huggingface.co/models?search=gguf"
  1112. target="_blank">click here.</a
  1113. >
  1114. </div>
  1115. {#if uploadProgress !== null}
  1116. <div class="mt-2">
  1117. <div class=" mb-2 text-xs">Upload Progress</div>
  1118. <div class="w-full rounded-full dark:bg-gray-800">
  1119. <div
  1120. class="dark:bg-gray-600 text-xs font-medium text-blue-100 text-center p-0.5 leading-none rounded-full"
  1121. style="width: {Math.max(15, uploadProgress ?? 0)}%"
  1122. >
  1123. {uploadProgress ?? 0}%
  1124. </div>
  1125. </div>
  1126. <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
  1127. {modelFileDigest}
  1128. </div>
  1129. </div>
  1130. {/if}
  1131. </form>
  1132. <hr class=" dark:border-gray-700" />
  1133. <div>
  1134. <div class=" mb-2.5 text-sm font-medium">Delete a model</div>
  1135. <div class="flex w-full">
  1136. <div class="flex-1 mr-2">
  1137. <select
  1138. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  1139. bind:value={deleteModelTag}
  1140. placeholder="Select a model"
  1141. >
  1142. {#if !deleteModelTag}
  1143. <option value="" disabled selected>Select a model</option>
  1144. {/if}
  1145. {#each $models.filter((m) => m.size != null) as model}
  1146. <option value={model.name} class="bg-gray-100 dark:bg-gray-700"
  1147. >{model.name +
  1148. ' (' +
  1149. (model.size / 1024 ** 3).toFixed(1) +
  1150. ' GB)'}</option
  1151. >
  1152. {/each}
  1153. </select>
  1154. </div>
  1155. <button
  1156. class="px-3 bg-red-700 hover:bg-red-800 text-gray-100 rounded transition"
  1157. on:click={() => {
  1158. deleteModelHandler();
  1159. }}
  1160. >
  1161. <svg
  1162. xmlns="http://www.w3.org/2000/svg"
  1163. viewBox="0 0 16 16"
  1164. fill="currentColor"
  1165. class="w-4 h-4"
  1166. >
  1167. <path
  1168. fill-rule="evenodd"
  1169. d="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z"
  1170. clip-rule="evenodd"
  1171. />
  1172. </svg>
  1173. </button>
  1174. </div>
  1175. </div>
  1176. </div>
  1177. </div>
  1178. {:else if selectedTab === 'external'}
  1179. <form
  1180. class="flex flex-col h-full justify-between space-y-3 text-sm"
  1181. on:submit|preventDefault={() => {
  1182. saveSettings({
  1183. OPENAI_API_KEY: OPENAI_API_KEY !== '' ? OPENAI_API_KEY : undefined,
  1184. OPENAI_API_BASE_URL: OPENAI_API_BASE_URL !== '' ? OPENAI_API_BASE_URL : undefined
  1185. });
  1186. show = false;
  1187. }}
  1188. >
  1189. <div class=" space-y-3">
  1190. <div>
  1191. <div class=" mb-2.5 text-sm font-medium">OpenAI API Key</div>
  1192. <div class="flex w-full">
  1193. <div class="flex-1">
  1194. <input
  1195. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  1196. placeholder="Enter OpenAI API Key"
  1197. bind:value={OPENAI_API_KEY}
  1198. autocomplete="off"
  1199. />
  1200. </div>
  1201. </div>
  1202. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  1203. Adds optional support for online models.
  1204. </div>
  1205. </div>
  1206. <hr class=" dark:border-gray-700" />
  1207. <div>
  1208. <div class=" mb-2.5 text-sm font-medium">OpenAI API Base URL</div>
  1209. <div class="flex w-full">
  1210. <div class="flex-1">
  1211. <input
  1212. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  1213. placeholder="Enter OpenAI API Key"
  1214. bind:value={OPENAI_API_BASE_URL}
  1215. autocomplete="off"
  1216. />
  1217. </div>
  1218. </div>
  1219. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  1220. WebUI will make requests to <span class=" text-gray-200"
  1221. >'{OPENAI_API_BASE_URL}/chat'</span
  1222. >
  1223. </div>
  1224. </div>
  1225. </div>
  1226. <div class="flex justify-end pt-3 text-sm font-medium">
  1227. <button
  1228. class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
  1229. type="submit"
  1230. >
  1231. Save
  1232. </button>
  1233. </div>
  1234. </form>
  1235. {:else if selectedTab === 'addons'}
  1236. <form
  1237. class="flex flex-col h-full justify-between space-y-3 text-sm"
  1238. on:submit|preventDefault={() => {
  1239. saveSettings({
  1240. gravatarEmail: gravatarEmail !== '' ? gravatarEmail : undefined,
  1241. gravatarUrl: gravatarEmail !== '' ? getGravatarURL(gravatarEmail) : undefined
  1242. });
  1243. show = false;
  1244. }}
  1245. >
  1246. <div class=" space-y-3">
  1247. <div>
  1248. <div class=" mb-1 text-sm font-medium">WebUI Add-ons</div>
  1249. <div>
  1250. <div class=" py-0.5 flex w-full justify-between">
  1251. <div class=" self-center text-xs font-medium">Title Auto Generation</div>
  1252. <button
  1253. class="p-1 px-3 text-xs flex rounded transition"
  1254. on:click={() => {
  1255. toggleTitleAutoGenerate();
  1256. }}
  1257. type="button"
  1258. >
  1259. {#if titleAutoGenerate === true}
  1260. <span class="ml-2 self-center">On</span>
  1261. {:else}
  1262. <span class="ml-2 self-center">Off</span>
  1263. {/if}
  1264. </button>
  1265. </div>
  1266. </div>
  1267. <div>
  1268. <div class=" py-0.5 flex w-full justify-between">
  1269. <div class=" self-center text-xs font-medium">Voice Input Auto-Send</div>
  1270. <button
  1271. class="p-1 px-3 text-xs flex rounded transition"
  1272. on:click={() => {
  1273. toggleSpeechAutoSend();
  1274. }}
  1275. type="button"
  1276. >
  1277. {#if speechAutoSend === true}
  1278. <span class="ml-2 self-center">On</span>
  1279. {:else}
  1280. <span class="ml-2 self-center">Off</span>
  1281. {/if}
  1282. </button>
  1283. </div>
  1284. </div>
  1285. <div>
  1286. <div class=" py-0.5 flex w-full justify-between">
  1287. <div class=" self-center text-xs font-medium">
  1288. Response AutoCopy to Clipboard
  1289. </div>
  1290. <button
  1291. class="p-1 px-3 text-xs flex rounded transition"
  1292. on:click={() => {
  1293. toggleResponseAutoCopy();
  1294. }}
  1295. type="button"
  1296. >
  1297. {#if responseAutoCopy === true}
  1298. <span class="ml-2 self-center">On</span>
  1299. {:else}
  1300. <span class="ml-2 self-center">Off</span>
  1301. {/if}
  1302. </button>
  1303. </div>
  1304. </div>
  1305. </div>
  1306. <hr class=" dark:border-gray-700" />
  1307. <div>
  1308. <div class=" mb-2.5 text-sm font-medium">
  1309. Gravatar Email <span class=" text-gray-400 text-sm">(optional)</span>
  1310. </div>
  1311. <div class="flex w-full">
  1312. <div class="flex-1">
  1313. <input
  1314. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  1315. placeholder="Enter Your Email"
  1316. bind:value={gravatarEmail}
  1317. autocomplete="off"
  1318. type="email"
  1319. />
  1320. </div>
  1321. </div>
  1322. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  1323. Changes user profile image to match your <a
  1324. class=" text-gray-500 dark:text-gray-300 font-medium"
  1325. href="https://gravatar.com/"
  1326. target="_blank">Gravatar.</a
  1327. >
  1328. </div>
  1329. </div>
  1330. </div>
  1331. <div class="flex justify-end pt-3 text-sm font-medium">
  1332. <button
  1333. class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
  1334. type="submit"
  1335. >
  1336. Save
  1337. </button>
  1338. </div>
  1339. </form>
  1340. {:else if selectedTab === 'auth'}
  1341. <form
  1342. class="flex flex-col h-full justify-between space-y-3 text-sm"
  1343. on:submit|preventDefault={() => {
  1344. console.log('auth save');
  1345. saveSettings({
  1346. authHeader: authEnabled ? `${authType} ${authContent}` : undefined
  1347. });
  1348. show = false;
  1349. }}
  1350. >
  1351. <div class=" space-y-3">
  1352. <div>
  1353. <div class=" py-1 flex w-full justify-between">
  1354. <div class=" self-center text-sm font-medium">Authorization Header</div>
  1355. <button
  1356. class="p-1 px-3 text-xs flex rounded transition"
  1357. type="button"
  1358. on:click={() => {
  1359. toggleAuthHeader();
  1360. }}
  1361. >
  1362. {#if authEnabled === true}
  1363. <svg
  1364. xmlns="http://www.w3.org/2000/svg"
  1365. viewBox="0 0 24 24"
  1366. fill="currentColor"
  1367. class="w-4 h-4"
  1368. >
  1369. <path
  1370. fill-rule="evenodd"
  1371. 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"
  1372. clip-rule="evenodd"
  1373. />
  1374. </svg>
  1375. <span class="ml-2 self-center"> On </span>
  1376. {:else}
  1377. <svg
  1378. xmlns="http://www.w3.org/2000/svg"
  1379. viewBox="0 0 24 24"
  1380. fill="currentColor"
  1381. class="w-4 h-4"
  1382. >
  1383. <path
  1384. 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"
  1385. />
  1386. </svg>
  1387. <span class="ml-2 self-center">Off</span>
  1388. {/if}
  1389. </button>
  1390. </div>
  1391. </div>
  1392. {#if authEnabled}
  1393. <hr class=" dark:border-gray-700" />
  1394. <div class="mt-2">
  1395. <div class=" py-1 flex w-full space-x-2">
  1396. <button
  1397. class=" py-1 font-semibold flex rounded transition"
  1398. on:click={() => {
  1399. authType = authType === 'Basic' ? 'Bearer' : 'Basic';
  1400. }}
  1401. type="button"
  1402. >
  1403. {#if authType === 'Basic'}
  1404. <span class="self-center mr-2">Basic</span>
  1405. {:else if authType === 'Bearer'}
  1406. <span class="self-center mr-2">Bearer</span>
  1407. {/if}
  1408. </button>
  1409. <div class="flex-1">
  1410. <input
  1411. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  1412. placeholder="Enter Authorization Header Content"
  1413. bind:value={authContent}
  1414. />
  1415. </div>
  1416. </div>
  1417. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  1418. Toggle between <span class=" text-gray-500 dark:text-gray-300 font-medium"
  1419. >'Basic'</span
  1420. >
  1421. and <span class=" text-gray-500 dark:text-gray-300 font-medium">'Bearer'</span> by
  1422. clicking on the label next to the input.
  1423. </div>
  1424. </div>
  1425. <hr class=" dark:border-gray-700" />
  1426. <div>
  1427. <div class=" mb-2.5 text-sm font-medium">Preview Authorization Header</div>
  1428. <textarea
  1429. value={JSON.stringify({
  1430. Authorization: `${authType} ${authContent}`
  1431. })}
  1432. class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
  1433. rows="2"
  1434. disabled
  1435. />
  1436. </div>
  1437. {/if}
  1438. </div>
  1439. <div class="flex justify-end pt-3 text-sm font-medium">
  1440. <button
  1441. class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
  1442. type="submit"
  1443. >
  1444. Save
  1445. </button>
  1446. </div>
  1447. </form>
  1448. {:else if selectedTab === 'about'}
  1449. <div class="flex flex-col h-full justify-between space-y-3 text-sm mb-6">
  1450. <div class=" space-y-3">
  1451. <div>
  1452. <div class=" mb-2.5 text-sm font-medium">Ollama Web UI Version</div>
  1453. <div class="flex w-full">
  1454. <div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
  1455. {$config && $config.version ? $config.version : WEB_UI_VERSION}
  1456. </div>
  1457. </div>
  1458. </div>
  1459. <hr class=" dark:border-gray-700" />
  1460. <div>
  1461. <div class=" mb-2.5 text-sm font-medium">Ollama Version</div>
  1462. <div class="flex w-full">
  1463. <div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
  1464. {$info?.ollama?.version ?? 'N/A'}
  1465. </div>
  1466. </div>
  1467. </div>
  1468. <hr class=" dark:border-gray-700" />
  1469. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  1470. Created by <a
  1471. class=" text-gray-500 dark:text-gray-300 font-medium"
  1472. href="https://github.com/tjbck"
  1473. target="_blank">Timothy J. Baek</a
  1474. >
  1475. </div>
  1476. <div>
  1477. <a href="https://github.com/ollama-webui/ollama-webui">
  1478. <img
  1479. alt="Github Repo"
  1480. src="https://img.shields.io/github/stars/ollama-webui/ollama-webui?style=social&label=Star us on Github"
  1481. />
  1482. </a>
  1483. </div>
  1484. </div>
  1485. </div>
  1486. {/if}
  1487. </div>
  1488. </div>
  1489. </div>
  1490. </Modal>
  1491. <style>
  1492. input::-webkit-outer-spin-button,
  1493. input::-webkit-inner-spin-button {
  1494. /* display: none; <- Crashes Chrome on hover */
  1495. -webkit-appearance: none;
  1496. margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
  1497. }
  1498. .tabs::-webkit-scrollbar {
  1499. display: none; /* for Chrome, Safari and Opera */
  1500. }
  1501. .tabs {
  1502. -ms-overflow-style: none; /* IE and Edge */
  1503. scrollbar-width: none; /* Firefox */
  1504. }
  1505. input[type='number'] {
  1506. -moz-appearance: textfield; /* Firefox */
  1507. }
  1508. </style>