SettingsModal.svelte 60 KB

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