SettingsModal.svelte 74 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288
  1. <script lang="ts">
  2. import toast from 'svelte-french-toast';
  3. import queue from 'async/queue';
  4. import fileSaver from 'file-saver';
  5. const { saveAs } = fileSaver;
  6. import { goto } from '$app/navigation';
  7. import { onMount } from 'svelte';
  8. import {
  9. getOllamaVersion,
  10. getOllamaModels,
  11. getOllamaAPIUrl,
  12. updateOllamaAPIUrl,
  13. pullModel,
  14. createModel,
  15. deleteModel
  16. } from '$lib/apis/ollama';
  17. import { updateUserPassword } from '$lib/apis/auths';
  18. import { createNewChat, deleteAllChats, getAllChats, getChatList } from '$lib/apis/chats';
  19. import { WEB_UI_VERSION, WEBUI_API_BASE_URL } from '$lib/constants';
  20. import { config, models, voices, settings, user, chats } from '$lib/stores';
  21. import { splitStream, getGravatarURL, getImportOrigin, convertOpenAIChats } from '$lib/utils';
  22. import Advanced from './Settings/Advanced.svelte';
  23. import Modal from '../common/Modal.svelte';
  24. import {
  25. getOpenAIKey,
  26. getOpenAIModels,
  27. getOpenAIUrl,
  28. updateOpenAIKey,
  29. updateOpenAIUrl
  30. } from '$lib/apis/openai';
  31. import { resetVectorDB } from '$lib/apis/rag';
  32. import { setDefaultPromptSuggestions } from '$lib/apis/configs';
  33. import { getBackendConfig } from '$lib/apis';
  34. export let show = false;
  35. const saveSettings = async (updated) => {
  36. console.log(updated);
  37. await settings.set({ ...$settings, ...updated });
  38. await models.set(await getModels());
  39. localStorage.setItem('settings', JSON.stringify($settings));
  40. };
  41. let selectedTab = 'general';
  42. // General
  43. let API_BASE_URL = '';
  44. let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light'];
  45. let theme = 'dark';
  46. let notificationEnabled = false;
  47. let system = '';
  48. // Advanced
  49. let requestFormat = '';
  50. let options = {
  51. // Advanced
  52. seed: 0,
  53. temperature: '',
  54. repeat_penalty: '',
  55. repeat_last_n: '',
  56. mirostat: '',
  57. mirostat_eta: '',
  58. mirostat_tau: '',
  59. top_k: '',
  60. top_p: '',
  61. stop: '',
  62. tfs_z: '',
  63. num_ctx: '',
  64. num_predict: ''
  65. };
  66. // Models
  67. const MAX_PARALLEL_DOWNLOADS = 3;
  68. const modelDownloadQueue = queue(
  69. (task: { modelName: string }, cb) =>
  70. pullModelHandlerProcessor({ modelName: task.modelName, callback: cb }),
  71. MAX_PARALLEL_DOWNLOADS
  72. );
  73. let modelDownloadStatus: Record<string, any> = {};
  74. let modelTransferring = false;
  75. let modelTag = '';
  76. let digest = '';
  77. let pullProgress = null;
  78. let modelUploadMode = 'file';
  79. let modelInputFile = '';
  80. let modelFileUrl = '';
  81. let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSSISTANT:"`;
  82. let modelFileDigest = '';
  83. let uploadProgress = null;
  84. let deleteModelTag = '';
  85. // External
  86. let OPENAI_API_KEY = '';
  87. let OPENAI_API_BASE_URL = '';
  88. // Interface
  89. let promptSuggestions = [];
  90. // Addons
  91. let titleAutoGenerate = true;
  92. let speechAutoSend = false;
  93. let responseAutoCopy = false;
  94. let gravatarEmail = '';
  95. let titleAutoGenerateModel = '';
  96. // Voice
  97. let speakVoice = '';
  98. // Chats
  99. let saveChatHistory = true;
  100. let importFiles;
  101. let showDeleteConfirm = false;
  102. // Auth
  103. let authEnabled = false;
  104. let authType = 'Basic';
  105. let authContent = '';
  106. // Account
  107. let currentPassword = '';
  108. let newPassword = '';
  109. let newPasswordConfirm = '';
  110. // About
  111. let ollamaVersion = '';
  112. $: if (importFiles) {
  113. console.log(importFiles);
  114. let reader = new FileReader();
  115. reader.onload = (event) => {
  116. let chats = JSON.parse(event.target.result);
  117. console.log(chats);
  118. if (getImportOrigin(chats) == 'openai') {
  119. try {
  120. chats = convertOpenAIChats(chats);
  121. } catch (error) {
  122. console.log('Unable to import chats:', error);
  123. }
  124. }
  125. importChats(chats);
  126. };
  127. if (importFiles.length > 0) {
  128. reader.readAsText(importFiles[0]);
  129. }
  130. }
  131. const importChats = async (_chats) => {
  132. for (const chat of _chats) {
  133. console.log(chat);
  134. if (chat.chat) {
  135. await createNewChat(localStorage.token, chat.chat);
  136. } else {
  137. await createNewChat(localStorage.token, chat);
  138. }
  139. }
  140. await chats.set(await getChatList(localStorage.token));
  141. };
  142. const exportChats = async () => {
  143. let blob = new Blob([JSON.stringify(await getAllChats(localStorage.token))], {
  144. type: 'application/json'
  145. });
  146. saveAs(blob, `chat-export-${Date.now()}.json`);
  147. };
  148. const deleteChats = async () => {
  149. await goto('/');
  150. await deleteAllChats(localStorage.token);
  151. await chats.set(await getChatList(localStorage.token));
  152. };
  153. const updateOllamaAPIUrlHandler = async () => {
  154. API_BASE_URL = await updateOllamaAPIUrl(localStorage.token, API_BASE_URL);
  155. const _models = await getModels('ollama');
  156. if (_models.length > 0) {
  157. toast.success('Server connection verified');
  158. await models.set(_models);
  159. }
  160. };
  161. const updateOpenAIHandler = async () => {
  162. OPENAI_API_BASE_URL = await updateOpenAIUrl(localStorage.token, OPENAI_API_BASE_URL);
  163. OPENAI_API_KEY = await updateOpenAIKey(localStorage.token, OPENAI_API_KEY);
  164. await models.set(await getModels());
  165. };
  166. const updateInterfaceHandler = async () => {
  167. promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions);
  168. await config.set(await getBackendConfig());
  169. };
  170. const toggleTheme = async () => {
  171. if (theme === 'dark') {
  172. theme = 'light';
  173. } else {
  174. theme = 'dark';
  175. }
  176. localStorage.theme = theme;
  177. document.documentElement.classList.remove(theme === 'dark' ? 'light' : 'dark');
  178. document.documentElement.classList.add(theme);
  179. };
  180. const toggleRequestFormat = async () => {
  181. if (requestFormat === '') {
  182. requestFormat = 'json';
  183. } else {
  184. requestFormat = '';
  185. }
  186. saveSettings({ requestFormat: requestFormat !== '' ? requestFormat : undefined });
  187. };
  188. const toggleSpeechAutoSend = async () => {
  189. speechAutoSend = !speechAutoSend;
  190. saveSettings({ speechAutoSend: speechAutoSend });
  191. };
  192. const toggleTitleAutoGenerate = async () => {
  193. titleAutoGenerate = !titleAutoGenerate;
  194. saveSettings({ titleAutoGenerate: titleAutoGenerate });
  195. };
  196. const toggleNotification = async () => {
  197. const permission = await Notification.requestPermission();
  198. if (permission === 'granted') {
  199. notificationEnabled = !notificationEnabled;
  200. saveSettings({ notificationEnabled: notificationEnabled });
  201. } else {
  202. toast.error(
  203. 'Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.'
  204. );
  205. }
  206. };
  207. const toggleResponseAutoCopy = async () => {
  208. const permission = await navigator.clipboard
  209. .readText()
  210. .then(() => {
  211. return 'granted';
  212. })
  213. .catch(() => {
  214. return '';
  215. });
  216. console.log(permission);
  217. if (permission === 'granted') {
  218. responseAutoCopy = !responseAutoCopy;
  219. saveSettings({ responseAutoCopy: responseAutoCopy });
  220. } else {
  221. toast.error(
  222. 'Clipboard write permission denied. Please check your browser settings to grant the necessary access.'
  223. );
  224. }
  225. };
  226. const toggleSaveChatHistory = async () => {
  227. saveChatHistory = !saveChatHistory;
  228. console.log(saveChatHistory);
  229. if (saveChatHistory === false) {
  230. await goto('/');
  231. }
  232. saveSettings({ saveChatHistory: saveChatHistory });
  233. };
  234. const pullModelHandlerProcessor = async (opts: { modelName: string; callback: Function }) => {
  235. const res = await pullModel(localStorage.token, opts.modelName).catch((error) => {
  236. opts.callback({ success: false, error, modelName: opts.modelName });
  237. return null;
  238. });
  239. if (res) {
  240. const reader = res.body
  241. .pipeThrough(new TextDecoderStream())
  242. .pipeThrough(splitStream('\n'))
  243. .getReader();
  244. while (true) {
  245. try {
  246. const { value, done } = await reader.read();
  247. if (done) break;
  248. let lines = value.split('\n');
  249. for (const line of lines) {
  250. if (line !== '') {
  251. let data = JSON.parse(line);
  252. if (data.error) {
  253. throw data.error;
  254. }
  255. if (data.detail) {
  256. throw data.detail;
  257. }
  258. if (data.status) {
  259. if (data.digest) {
  260. let downloadProgress = 0;
  261. if (data.completed) {
  262. downloadProgress = Math.round((data.completed / data.total) * 1000) / 10;
  263. } else {
  264. downloadProgress = 100;
  265. }
  266. modelDownloadStatus[opts.modelName] = {
  267. pullProgress: downloadProgress,
  268. digest: data.digest
  269. };
  270. } else {
  271. toast.success(data.status);
  272. }
  273. }
  274. }
  275. }
  276. } catch (error) {
  277. console.log(error);
  278. if (typeof error !== 'string') {
  279. error = error.message;
  280. }
  281. opts.callback({ success: false, error, modelName: opts.modelName });
  282. }
  283. }
  284. opts.callback({ success: true, modelName: opts.modelName });
  285. }
  286. };
  287. const pullModelHandler = async () => {
  288. const sanitizedModelTag = modelTag.trim();
  289. if (modelDownloadStatus[sanitizedModelTag]) {
  290. toast.error(`Model '${sanitizedModelTag}' is already in queue for downloading.`);
  291. return;
  292. }
  293. if (Object.keys(modelDownloadStatus).length === 3) {
  294. toast.error('Maximum of 3 models can be downloaded simultaneously. Please try again later.');
  295. return;
  296. }
  297. modelTransferring = true;
  298. modelDownloadQueue.push(
  299. { modelName: sanitizedModelTag },
  300. async (data: { modelName: string; success: boolean; error?: Error }) => {
  301. const { modelName } = data;
  302. // Remove the downloaded model
  303. delete modelDownloadStatus[modelName];
  304. console.log(data);
  305. if (!data.success) {
  306. toast.error(data.error);
  307. } else {
  308. toast.success(`Model '${modelName}' has been successfully downloaded.`);
  309. const notification = new Notification(`Ollama`, {
  310. body: `Model '${modelName}' has been successfully downloaded.`,
  311. icon: '/favicon.png'
  312. });
  313. models.set(await getModels());
  314. }
  315. }
  316. );
  317. modelTag = '';
  318. modelTransferring = false;
  319. };
  320. const uploadModelHandler = async () => {
  321. modelTransferring = true;
  322. uploadProgress = 0;
  323. let uploaded = false;
  324. let fileResponse = null;
  325. let name = '';
  326. if (modelUploadMode === 'file') {
  327. const file = modelInputFile[0];
  328. const formData = new FormData();
  329. formData.append('file', file);
  330. fileResponse = await fetch(`${WEBUI_API_BASE_URL}/utils/upload`, {
  331. method: 'POST',
  332. headers: {
  333. ...($user && { Authorization: `Bearer ${localStorage.token}` })
  334. },
  335. body: formData
  336. }).catch((error) => {
  337. console.log(error);
  338. return null;
  339. });
  340. } else {
  341. fileResponse = await fetch(`${WEBUI_API_BASE_URL}/utils/download?url=${modelFileUrl}`, {
  342. method: 'GET',
  343. headers: {
  344. ...($user && { Authorization: `Bearer ${localStorage.token}` })
  345. }
  346. }).catch((error) => {
  347. console.log(error);
  348. return null;
  349. });
  350. }
  351. if (fileResponse && fileResponse.ok) {
  352. const reader = fileResponse.body
  353. .pipeThrough(new TextDecoderStream())
  354. .pipeThrough(splitStream('\n'))
  355. .getReader();
  356. while (true) {
  357. const { value, done } = await reader.read();
  358. if (done) break;
  359. try {
  360. let lines = value.split('\n');
  361. for (const line of lines) {
  362. if (line !== '') {
  363. let data = JSON.parse(line.replace(/^data: /, ''));
  364. if (data.progress) {
  365. uploadProgress = data.progress;
  366. }
  367. if (data.error) {
  368. throw data.error;
  369. }
  370. if (data.done) {
  371. modelFileDigest = data.blob;
  372. name = data.name;
  373. uploaded = true;
  374. }
  375. }
  376. }
  377. } catch (error) {
  378. console.log(error);
  379. }
  380. }
  381. }
  382. if (uploaded) {
  383. const res = await createModel(
  384. localStorage.token,
  385. `${name}:latest`,
  386. `FROM @${modelFileDigest}\n${modelFileContent}`
  387. );
  388. if (res && res.ok) {
  389. const reader = res.body
  390. .pipeThrough(new TextDecoderStream())
  391. .pipeThrough(splitStream('\n'))
  392. .getReader();
  393. while (true) {
  394. const { value, done } = await reader.read();
  395. if (done) break;
  396. try {
  397. let lines = value.split('\n');
  398. for (const line of lines) {
  399. if (line !== '') {
  400. console.log(line);
  401. let data = JSON.parse(line);
  402. console.log(data);
  403. if (data.error) {
  404. throw data.error;
  405. }
  406. if (data.detail) {
  407. throw data.detail;
  408. }
  409. if (data.status) {
  410. if (
  411. !data.digest &&
  412. !data.status.includes('writing') &&
  413. !data.status.includes('sha256')
  414. ) {
  415. toast.success(data.status);
  416. } else {
  417. if (data.digest) {
  418. digest = data.digest;
  419. if (data.completed) {
  420. pullProgress = Math.round((data.completed / data.total) * 1000) / 10;
  421. } else {
  422. pullProgress = 100;
  423. }
  424. }
  425. }
  426. }
  427. }
  428. }
  429. } catch (error) {
  430. console.log(error);
  431. toast.error(error);
  432. }
  433. }
  434. }
  435. }
  436. modelFileUrl = '';
  437. modelInputFile = '';
  438. modelTransferring = false;
  439. uploadProgress = null;
  440. models.set(await getModels());
  441. };
  442. const deleteModelHandler = async () => {
  443. const res = await deleteModel(localStorage.token, deleteModelTag).catch((error) => {
  444. toast.error(error);
  445. });
  446. if (res) {
  447. toast.success(`Deleted ${deleteModelTag}`);
  448. }
  449. deleteModelTag = '';
  450. models.set(await getModels());
  451. };
  452. const getModels = async (type = 'all') => {
  453. const models = [];
  454. models.push(
  455. ...(await getOllamaModels(localStorage.token).catch((error) => {
  456. toast.error(error);
  457. return [];
  458. }))
  459. );
  460. // If OpenAI API Key exists
  461. if (type === 'all' && OPENAI_API_KEY) {
  462. const openAIModels = await getOpenAIModels(localStorage.token).catch((error) => {
  463. console.log(error);
  464. return null;
  465. });
  466. models.push(...(openAIModels ? [{ name: 'hr' }, ...openAIModels] : []));
  467. }
  468. return models;
  469. };
  470. const updatePasswordHandler = async () => {
  471. if (newPassword === newPasswordConfirm) {
  472. const res = await updateUserPassword(localStorage.token, currentPassword, newPassword).catch(
  473. (error) => {
  474. toast.error(error);
  475. return null;
  476. }
  477. );
  478. if (res) {
  479. toast.success('Successfully updated.');
  480. }
  481. currentPassword = '';
  482. newPassword = '';
  483. newPasswordConfirm = '';
  484. } else {
  485. toast.error(
  486. `The passwords you entered don't quite match. Please double-check and try again.`
  487. );
  488. newPassword = '';
  489. newPasswordConfirm = '';
  490. }
  491. };
  492. onMount(async () => {
  493. console.log('settings', $user.role === 'admin');
  494. if ($user.role === 'admin') {
  495. API_BASE_URL = await getOllamaAPIUrl(localStorage.token);
  496. OPENAI_API_BASE_URL = await getOpenAIUrl(localStorage.token);
  497. OPENAI_API_KEY = await getOpenAIKey(localStorage.token);
  498. promptSuggestions = $config?.default_prompt_suggestions;
  499. }
  500. let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
  501. console.log(settings);
  502. theme = localStorage.theme ?? 'dark';
  503. notificationEnabled = settings.notificationEnabled ?? false;
  504. system = settings.system ?? '';
  505. requestFormat = settings.requestFormat ?? '';
  506. options.seed = settings.seed ?? 0;
  507. options.temperature = settings.temperature ?? '';
  508. options.repeat_penalty = settings.repeat_penalty ?? '';
  509. options.top_k = settings.top_k ?? '';
  510. options.top_p = settings.top_p ?? '';
  511. options.num_ctx = settings.num_ctx ?? '';
  512. options = { ...options, ...settings.options };
  513. options.stop = (settings?.options?.stop ?? []).join(',');
  514. titleAutoGenerate = settings.titleAutoGenerate ?? true;
  515. speechAutoSend = settings.speechAutoSend ?? false;
  516. responseAutoCopy = settings.responseAutoCopy ?? false;
  517. titleAutoGenerateModel = settings.titleAutoGenerateModel ?? '';
  518. gravatarEmail = settings.gravatarEmail ?? '';
  519. speakVoice = settings.speakVoice ?? '';
  520. await voices.set(await speechSynthesis.getVoices());
  521. saveChatHistory = settings.saveChatHistory ?? true;
  522. authEnabled = settings.authHeader !== undefined ? true : false;
  523. if (authEnabled) {
  524. authType = settings.authHeader.split(' ')[0];
  525. authContent = settings.authHeader.split(' ')[1];
  526. }
  527. ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => {
  528. return '';
  529. });
  530. });
  531. </script>
  532. <Modal bind:show>
  533. <div>
  534. <div class=" flex justify-between dark:text-gray-300 px-5 py-4">
  535. <div class=" text-lg font-medium self-center">Settings</div>
  536. <button
  537. class="self-center"
  538. on:click={() => {
  539. show = false;
  540. }}
  541. >
  542. <svg
  543. xmlns="http://www.w3.org/2000/svg"
  544. viewBox="0 0 20 20"
  545. fill="currentColor"
  546. class="w-5 h-5"
  547. >
  548. <path
  549. 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"
  550. />
  551. </svg>
  552. </button>
  553. </div>
  554. <hr class=" dark:border-gray-800" />
  555. <div class="flex flex-col md:flex-row w-full p-4 md:space-x-4">
  556. <div
  557. 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"
  558. >
  559. <button
  560. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  561. 'general'
  562. ? 'bg-gray-200 dark:bg-gray-700'
  563. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  564. on:click={() => {
  565. selectedTab = 'general';
  566. }}
  567. >
  568. <div class=" self-center mr-2">
  569. <svg
  570. xmlns="http://www.w3.org/2000/svg"
  571. viewBox="0 0 20 20"
  572. fill="currentColor"
  573. class="w-4 h-4"
  574. >
  575. <path
  576. fill-rule="evenodd"
  577. 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"
  578. clip-rule="evenodd"
  579. />
  580. </svg>
  581. </div>
  582. <div class=" self-center">General</div>
  583. </button>
  584. <button
  585. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  586. 'advanced'
  587. ? 'bg-gray-200 dark:bg-gray-700'
  588. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  589. on:click={() => {
  590. selectedTab = 'advanced';
  591. }}
  592. >
  593. <div class=" self-center mr-2">
  594. <svg
  595. xmlns="http://www.w3.org/2000/svg"
  596. viewBox="0 0 20 20"
  597. fill="currentColor"
  598. class="w-4 h-4"
  599. >
  600. <path
  601. 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"
  602. />
  603. </svg>
  604. </div>
  605. <div class=" self-center">Advanced</div>
  606. </button>
  607. {#if $user?.role === 'admin'}
  608. <button
  609. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  610. 'models'
  611. ? 'bg-gray-200 dark:bg-gray-700'
  612. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  613. on:click={() => {
  614. selectedTab = 'models';
  615. }}
  616. >
  617. <div class=" self-center mr-2">
  618. <svg
  619. xmlns="http://www.w3.org/2000/svg"
  620. viewBox="0 0 20 20"
  621. fill="currentColor"
  622. class="w-4 h-4"
  623. >
  624. <path
  625. fill-rule="evenodd"
  626. 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"
  627. clip-rule="evenodd"
  628. />
  629. </svg>
  630. </div>
  631. <div class=" self-center">Models</div>
  632. </button>
  633. <button
  634. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  635. 'external'
  636. ? 'bg-gray-200 dark:bg-gray-700'
  637. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  638. on:click={() => {
  639. selectedTab = 'external';
  640. }}
  641. >
  642. <div class=" self-center mr-2">
  643. <svg
  644. xmlns="http://www.w3.org/2000/svg"
  645. viewBox="0 0 16 16"
  646. fill="currentColor"
  647. class="w-4 h-4"
  648. >
  649. <path
  650. 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"
  651. />
  652. </svg>
  653. </div>
  654. <div class=" self-center">External</div>
  655. </button>
  656. <button
  657. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  658. 'interface'
  659. ? 'bg-gray-200 dark:bg-gray-700'
  660. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  661. on:click={() => {
  662. selectedTab = 'interface';
  663. }}
  664. >
  665. <div class=" self-center mr-2">
  666. <svg
  667. xmlns="http://www.w3.org/2000/svg"
  668. viewBox="0 0 16 16"
  669. fill="currentColor"
  670. class="w-4 h-4"
  671. >
  672. <path
  673. fill-rule="evenodd"
  674. d="M2 4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4Zm10.5 5.707a.5.5 0 0 0-.146-.353l-1-1a.5.5 0 0 0-.708 0L9.354 9.646a.5.5 0 0 1-.708 0L6.354 7.354a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0-.146.353V12a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5V9.707ZM12 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
  675. clip-rule="evenodd"
  676. />
  677. </svg>
  678. </div>
  679. <div class=" self-center">Interface</div>
  680. </button>
  681. {/if}
  682. <button
  683. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  684. 'addons'
  685. ? 'bg-gray-200 dark:bg-gray-700'
  686. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  687. on:click={() => {
  688. selectedTab = 'addons';
  689. }}
  690. >
  691. <div class=" self-center mr-2">
  692. <svg
  693. xmlns="http://www.w3.org/2000/svg"
  694. viewBox="0 0 20 20"
  695. fill="currentColor"
  696. class="w-4 h-4"
  697. >
  698. <path
  699. 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"
  700. />
  701. </svg>
  702. </div>
  703. <div class=" self-center">Add-ons</div>
  704. </button>
  705. <button
  706. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  707. 'chats'
  708. ? 'bg-gray-200 dark:bg-gray-700'
  709. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  710. on:click={() => {
  711. selectedTab = 'chats';
  712. }}
  713. >
  714. <div class=" self-center mr-2">
  715. <svg
  716. xmlns="http://www.w3.org/2000/svg"
  717. viewBox="0 0 16 16"
  718. fill="currentColor"
  719. class="w-4 h-4"
  720. >
  721. <path
  722. fill-rule="evenodd"
  723. 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"
  724. clip-rule="evenodd"
  725. />
  726. </svg>
  727. </div>
  728. <div class=" self-center">Chats</div>
  729. </button>
  730. <button
  731. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  732. 'account'
  733. ? 'bg-gray-200 dark:bg-gray-700'
  734. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  735. on:click={() => {
  736. selectedTab = 'account';
  737. }}
  738. >
  739. <div class=" self-center mr-2">
  740. <svg
  741. xmlns="http://www.w3.org/2000/svg"
  742. viewBox="0 0 16 16"
  743. fill="currentColor"
  744. class="w-4 h-4"
  745. >
  746. <path
  747. fill-rule="evenodd"
  748. d="M15 8A7 7 0 1 1 1 8a7 7 0 0 1 14 0Zm-5-2a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM8 9c-1.825 0-3.422.977-4.295 2.437A5.49 5.49 0 0 0 8 13.5a5.49 5.49 0 0 0 4.294-2.063A4.997 4.997 0 0 0 8 9Z"
  749. clip-rule="evenodd"
  750. />
  751. </svg>
  752. </div>
  753. <div class=" self-center">Account</div>
  754. </button>
  755. <button
  756. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  757. 'about'
  758. ? 'bg-gray-200 dark:bg-gray-700'
  759. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  760. on:click={() => {
  761. selectedTab = 'about';
  762. }}
  763. >
  764. <div class=" self-center mr-2">
  765. <svg
  766. xmlns="http://www.w3.org/2000/svg"
  767. viewBox="0 0 20 20"
  768. fill="currentColor"
  769. class="w-4 h-4"
  770. >
  771. <path
  772. fill-rule="evenodd"
  773. 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"
  774. clip-rule="evenodd"
  775. />
  776. </svg>
  777. </div>
  778. <div class=" self-center">About</div>
  779. </button>
  780. </div>
  781. <div class="flex-1 md:min-h-[380px]">
  782. {#if selectedTab === 'general'}
  783. <div class="flex flex-col space-y-3">
  784. <div>
  785. <div class=" mb-1 text-sm font-medium">WebUI Settings</div>
  786. <div class=" py-0.5 flex w-full justify-between">
  787. <div class=" self-center text-xs font-medium">Theme</div>
  788. <!-- <button
  789. class="p-1 px-3 text-xs flex rounded transition"
  790. on:click={() => {
  791. toggleTheme();
  792. }}
  793. >
  794. </button> -->
  795. <div class="flex items-center relative">
  796. <div class=" absolute right-16">
  797. {#if theme === 'dark'}
  798. <svg
  799. xmlns="http://www.w3.org/2000/svg"
  800. viewBox="0 0 20 20"
  801. fill="currentColor"
  802. class="w-4 h-4"
  803. >
  804. <path
  805. fill-rule="evenodd"
  806. 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"
  807. clip-rule="evenodd"
  808. />
  809. </svg>
  810. {:else if theme === 'light'}
  811. <svg
  812. xmlns="http://www.w3.org/2000/svg"
  813. viewBox="0 0 20 20"
  814. fill="currentColor"
  815. class="w-4 h-4 self-center"
  816. >
  817. <path
  818. 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"
  819. />
  820. </svg>
  821. {/if}
  822. </div>
  823. <select
  824. class="w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
  825. bind:value={theme}
  826. placeholder="Select a theme"
  827. on:change={(e) => {
  828. localStorage.theme = theme;
  829. themes
  830. .filter((e) => e !== theme)
  831. .forEach((e) => {
  832. e.split(' ').forEach((e) => {
  833. document.documentElement.classList.remove(e);
  834. });
  835. });
  836. theme.split(' ').forEach((e) => {
  837. document.documentElement.classList.add(e);
  838. });
  839. console.log(theme);
  840. }}
  841. >
  842. <option value="dark">Dark</option>
  843. <option value="light">Light</option>
  844. <option value="rose-pine dark">Rosé Pine</option>
  845. <option value="rose-pine-dawn light">Rosé Pine Dawn</option>
  846. </select>
  847. </div>
  848. </div>
  849. <div>
  850. <div class=" py-0.5 flex w-full justify-between">
  851. <div class=" self-center text-xs font-medium">Notification</div>
  852. <button
  853. class="p-1 px-3 text-xs flex rounded transition"
  854. on:click={() => {
  855. toggleNotification();
  856. }}
  857. type="button"
  858. >
  859. {#if notificationEnabled === true}
  860. <span class="ml-2 self-center">On</span>
  861. {:else}
  862. <span class="ml-2 self-center">Off</span>
  863. {/if}
  864. </button>
  865. </div>
  866. </div>
  867. </div>
  868. {#if $user.role === 'admin'}
  869. <hr class=" dark:border-gray-700" />
  870. <div>
  871. <div class=" mb-2.5 text-sm font-medium">Ollama API URL</div>
  872. <div class="flex w-full">
  873. <div class="flex-1 mr-2">
  874. <input
  875. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  876. placeholder="Enter URL (e.g. http://localhost:11434/api)"
  877. bind:value={API_BASE_URL}
  878. />
  879. </div>
  880. <button
  881. class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded transition"
  882. on:click={() => {
  883. updateOllamaAPIUrlHandler();
  884. }}
  885. >
  886. <svg
  887. xmlns="http://www.w3.org/2000/svg"
  888. viewBox="0 0 20 20"
  889. fill="currentColor"
  890. class="w-4 h-4"
  891. >
  892. <path
  893. fill-rule="evenodd"
  894. 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"
  895. clip-rule="evenodd"
  896. />
  897. </svg>
  898. </button>
  899. </div>
  900. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  901. Trouble accessing Ollama?
  902. <a
  903. class=" text-gray-300 font-medium"
  904. href="https://github.com/ollama-webui/ollama-webui#troubleshooting"
  905. target="_blank"
  906. >
  907. Click here for help.
  908. </a>
  909. </div>
  910. </div>
  911. {/if}
  912. <hr class=" dark:border-gray-700" />
  913. <div>
  914. <div class=" mb-2.5 text-sm font-medium">System Prompt</div>
  915. <textarea
  916. bind:value={system}
  917. class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
  918. rows="4"
  919. />
  920. </div>
  921. <div class="flex justify-end pt-3 text-sm font-medium">
  922. <button
  923. class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
  924. on:click={() => {
  925. saveSettings({
  926. system: system !== '' ? system : undefined
  927. });
  928. show = false;
  929. }}
  930. >
  931. Save
  932. </button>
  933. </div>
  934. </div>
  935. {:else if selectedTab === 'advanced'}
  936. <div class="flex flex-col h-full justify-between text-sm">
  937. <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
  938. <div class=" text-sm font-medium">Parameters</div>
  939. <Advanced bind:options />
  940. <hr class=" dark:border-gray-700" />
  941. <div>
  942. <div class=" py-1 flex w-full justify-between">
  943. <div class=" self-center text-sm font-medium">Request Mode</div>
  944. <button
  945. class="p-1 px-3 text-xs flex rounded transition"
  946. on:click={() => {
  947. toggleRequestFormat();
  948. }}
  949. >
  950. {#if requestFormat === ''}
  951. <span class="ml-2 self-center"> Default </span>
  952. {:else if requestFormat === 'json'}
  953. <!-- <svg
  954. xmlns="http://www.w3.org/2000/svg"
  955. viewBox="0 0 20 20"
  956. fill="currentColor"
  957. class="w-4 h-4 self-center"
  958. >
  959. <path
  960. 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"
  961. />
  962. </svg> -->
  963. <span class="ml-2 self-center"> JSON </span>
  964. {/if}
  965. </button>
  966. </div>
  967. </div>
  968. </div>
  969. <div class="flex justify-end pt-3 text-sm font-medium">
  970. <button
  971. class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
  972. on:click={() => {
  973. saveSettings({
  974. options: {
  975. seed: (options.seed !== 0 ? options.seed : undefined) ?? undefined,
  976. stop:
  977. options.stop !== '' ? options.stop.split(',').filter((e) => e) : undefined,
  978. temperature: options.temperature !== '' ? options.temperature : undefined,
  979. repeat_penalty:
  980. options.repeat_penalty !== '' ? options.repeat_penalty : undefined,
  981. repeat_last_n:
  982. options.repeat_last_n !== '' ? options.repeat_last_n : undefined,
  983. mirostat: options.mirostat !== '' ? options.mirostat : undefined,
  984. mirostat_eta: options.mirostat_eta !== '' ? options.mirostat_eta : undefined,
  985. mirostat_tau: options.mirostat_tau !== '' ? options.mirostat_tau : undefined,
  986. top_k: options.top_k !== '' ? options.top_k : undefined,
  987. top_p: options.top_p !== '' ? options.top_p : undefined,
  988. tfs_z: options.tfs_z !== '' ? options.tfs_z : undefined,
  989. num_ctx: options.num_ctx !== '' ? options.num_ctx : undefined,
  990. num_predict: options.num_predict !== '' ? options.num_predict : undefined
  991. }
  992. });
  993. show = false;
  994. }}
  995. >
  996. Save
  997. </button>
  998. </div>
  999. </div>
  1000. {:else if selectedTab === 'models'}
  1001. <div class="flex flex-col h-full justify-between text-sm">
  1002. <div class=" space-y-3 pr-1.5 overflow-y-scroll h-80">
  1003. <div>
  1004. <div class=" mb-2.5 text-sm font-medium">Pull a model from Ollama.ai</div>
  1005. <div class="flex w-full">
  1006. <div class="flex-1 mr-2">
  1007. <input
  1008. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  1009. placeholder="Enter model tag (e.g. mistral:7b)"
  1010. bind:value={modelTag}
  1011. />
  1012. </div>
  1013. <button
  1014. class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded transition"
  1015. on:click={() => {
  1016. pullModelHandler();
  1017. }}
  1018. disabled={modelTransferring}
  1019. >
  1020. {#if modelTransferring}
  1021. <div class="self-center">
  1022. <svg
  1023. class=" w-4 h-4"
  1024. viewBox="0 0 24 24"
  1025. fill="currentColor"
  1026. xmlns="http://www.w3.org/2000/svg"
  1027. ><style>
  1028. .spinner_ajPY {
  1029. transform-origin: center;
  1030. animation: spinner_AtaB 0.75s infinite linear;
  1031. }
  1032. @keyframes spinner_AtaB {
  1033. 100% {
  1034. transform: rotate(360deg);
  1035. }
  1036. }
  1037. </style><path
  1038. 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"
  1039. opacity=".25"
  1040. /><path
  1041. 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"
  1042. class="spinner_ajPY"
  1043. /></svg
  1044. >
  1045. </div>
  1046. {:else}
  1047. <svg
  1048. xmlns="http://www.w3.org/2000/svg"
  1049. viewBox="0 0 16 16"
  1050. fill="currentColor"
  1051. class="w-4 h-4"
  1052. >
  1053. <path
  1054. 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"
  1055. />
  1056. <path
  1057. 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"
  1058. />
  1059. </svg>
  1060. {/if}
  1061. </button>
  1062. </div>
  1063. <div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
  1064. To access the available model names for downloading, <a
  1065. class=" text-gray-500 dark:text-gray-300 font-medium"
  1066. href="https://ollama.ai/library"
  1067. target="_blank">click here.</a
  1068. >
  1069. </div>
  1070. {#if Object.keys(modelDownloadStatus).length > 0}
  1071. {#each Object.keys(modelDownloadStatus) as model}
  1072. <div class="flex flex-col">
  1073. <div class="font-medium mb-1">{model}</div>
  1074. <div class="">
  1075. <div
  1076. class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
  1077. style="width: {Math.max(
  1078. 15,
  1079. modelDownloadStatus[model].pullProgress ?? 0
  1080. )}%"
  1081. >
  1082. {modelDownloadStatus[model].pullProgress ?? 0}%
  1083. </div>
  1084. <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
  1085. {modelDownloadStatus[model].digest}
  1086. </div>
  1087. </div>
  1088. </div>
  1089. {/each}
  1090. {/if}
  1091. </div>
  1092. <hr class=" dark:border-gray-700" />
  1093. <div>
  1094. <div class=" mb-2.5 text-sm font-medium">Delete a model</div>
  1095. <div class="flex w-full">
  1096. <div class="flex-1 mr-2">
  1097. <select
  1098. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  1099. bind:value={deleteModelTag}
  1100. placeholder="Select a model"
  1101. >
  1102. {#if !deleteModelTag}
  1103. <option value="" disabled selected>Select a model</option>
  1104. {/if}
  1105. {#each $models.filter((m) => m.size != null) as model}
  1106. <option value={model.name} class="bg-gray-100 dark:bg-gray-700"
  1107. >{model.name +
  1108. ' (' +
  1109. (model.size / 1024 ** 3).toFixed(1) +
  1110. ' GB)'}</option
  1111. >
  1112. {/each}
  1113. </select>
  1114. </div>
  1115. <button
  1116. class="px-3 bg-red-700 hover:bg-red-800 text-gray-100 rounded transition"
  1117. on:click={() => {
  1118. deleteModelHandler();
  1119. }}
  1120. >
  1121. <svg
  1122. xmlns="http://www.w3.org/2000/svg"
  1123. viewBox="0 0 16 16"
  1124. fill="currentColor"
  1125. class="w-4 h-4"
  1126. >
  1127. <path
  1128. fill-rule="evenodd"
  1129. 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"
  1130. clip-rule="evenodd"
  1131. />
  1132. </svg>
  1133. </button>
  1134. </div>
  1135. </div>
  1136. <hr class=" dark:border-gray-700" />
  1137. <form
  1138. on:submit|preventDefault={() => {
  1139. uploadModelHandler();
  1140. }}
  1141. >
  1142. <div class=" mb-2 flex w-full justify-between">
  1143. <div class=" text-sm font-medium">
  1144. Upload a GGUF model <a
  1145. class=" text-xs font-medium text-gray-500 underline"
  1146. href="https://github.com/jmorganca/ollama/blob/main/README.md#import-from-gguf"
  1147. target="_blank">(Experimental)</a
  1148. >
  1149. </div>
  1150. <button
  1151. class="p-1 px-3 text-xs flex rounded transition"
  1152. on:click={() => {
  1153. if (modelUploadMode === 'file') {
  1154. modelUploadMode = 'url';
  1155. } else {
  1156. modelUploadMode = 'file';
  1157. }
  1158. }}
  1159. type="button"
  1160. >
  1161. {#if modelUploadMode === 'file'}
  1162. <span class="ml-2 self-center">File Mode</span>
  1163. {:else}
  1164. <span class="ml-2 self-center">URL Mode</span>
  1165. {/if}
  1166. </button>
  1167. </div>
  1168. <div class="flex w-full mb-1.5">
  1169. <div class="flex flex-col w-full">
  1170. {#if modelUploadMode === 'file'}
  1171. <div
  1172. class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}"
  1173. >
  1174. <input
  1175. id="model-upload-input"
  1176. type="file"
  1177. bind:files={modelInputFile}
  1178. on:change={() => {
  1179. console.log(modelInputFile);
  1180. }}
  1181. accept=".gguf"
  1182. required
  1183. hidden
  1184. />
  1185. <button
  1186. type="button"
  1187. class="w-full rounded text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-800"
  1188. on:click={() => {
  1189. document.getElementById('model-upload-input').click();
  1190. }}
  1191. >
  1192. {#if modelInputFile && modelInputFile.length > 0}
  1193. {modelInputFile[0].name}
  1194. {:else}
  1195. Click here to select
  1196. {/if}
  1197. </button>
  1198. </div>
  1199. {:else}
  1200. <div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
  1201. <input
  1202. class="w-full rounded text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-800 outline-none {modelFileUrl !==
  1203. ''
  1204. ? 'mr-2'
  1205. : ''}"
  1206. type="url"
  1207. required
  1208. bind:value={modelFileUrl}
  1209. placeholder="Type HuggingFace Resolve (Download) URL"
  1210. />
  1211. </div>
  1212. {/if}
  1213. </div>
  1214. {#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
  1215. <button
  1216. class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded transition"
  1217. type="submit"
  1218. disabled={modelTransferring}
  1219. >
  1220. {#if modelTransferring}
  1221. <div class="self-center">
  1222. <svg
  1223. class=" w-4 h-4"
  1224. viewBox="0 0 24 24"
  1225. fill="currentColor"
  1226. xmlns="http://www.w3.org/2000/svg"
  1227. ><style>
  1228. .spinner_ajPY {
  1229. transform-origin: center;
  1230. animation: spinner_AtaB 0.75s infinite linear;
  1231. }
  1232. @keyframes spinner_AtaB {
  1233. 100% {
  1234. transform: rotate(360deg);
  1235. }
  1236. }
  1237. </style><path
  1238. 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"
  1239. opacity=".25"
  1240. /><path
  1241. 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"
  1242. class="spinner_ajPY"
  1243. /></svg
  1244. >
  1245. </div>
  1246. {:else}
  1247. <svg
  1248. xmlns="http://www.w3.org/2000/svg"
  1249. viewBox="0 0 16 16"
  1250. fill="currentColor"
  1251. class="w-4 h-4"
  1252. >
  1253. <path
  1254. 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"
  1255. />
  1256. <path
  1257. 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"
  1258. />
  1259. </svg>
  1260. {/if}
  1261. </button>
  1262. {/if}
  1263. </div>
  1264. {#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
  1265. <div>
  1266. <div>
  1267. <div class=" my-2.5 text-sm font-medium">Modelfile Content</div>
  1268. <textarea
  1269. bind:value={modelFileContent}
  1270. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
  1271. rows="6"
  1272. />
  1273. </div>
  1274. </div>
  1275. {/if}
  1276. <div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
  1277. To access the GGUF models available for downloading, <a
  1278. class=" text-gray-500 dark:text-gray-300 font-medium"
  1279. href="https://huggingface.co/models?search=gguf"
  1280. target="_blank">click here.</a
  1281. >
  1282. </div>
  1283. {#if uploadProgress !== null}
  1284. <div class="mt-2">
  1285. <div class=" mb-2 text-xs">Upload Progress</div>
  1286. <div class="w-full rounded-full dark:bg-gray-800">
  1287. <div
  1288. class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
  1289. style="width: {Math.max(15, uploadProgress ?? 0)}%"
  1290. >
  1291. {uploadProgress ?? 0}%
  1292. </div>
  1293. </div>
  1294. <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
  1295. {modelFileDigest}
  1296. </div>
  1297. </div>
  1298. {/if}
  1299. </form>
  1300. </div>
  1301. </div>
  1302. {:else if selectedTab === 'external'}
  1303. <form
  1304. class="flex flex-col h-full justify-between space-y-3 text-sm"
  1305. on:submit|preventDefault={() => {
  1306. updateOpenAIHandler();
  1307. // saveSettings({
  1308. // OPENAI_API_KEY: OPENAI_API_KEY !== '' ? OPENAI_API_KEY : undefined,
  1309. // OPENAI_API_BASE_URL: OPENAI_API_BASE_URL !== '' ? OPENAI_API_BASE_URL : undefined
  1310. // });
  1311. show = false;
  1312. }}
  1313. >
  1314. <div class=" space-y-3">
  1315. <div>
  1316. <div class=" mb-2.5 text-sm font-medium">OpenAI API Key</div>
  1317. <div class="flex w-full">
  1318. <div class="flex-1">
  1319. <input
  1320. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  1321. placeholder="Enter OpenAI API Key"
  1322. bind:value={OPENAI_API_KEY}
  1323. autocomplete="off"
  1324. />
  1325. </div>
  1326. </div>
  1327. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  1328. Adds optional support for online models.
  1329. </div>
  1330. </div>
  1331. <hr class=" dark:border-gray-700" />
  1332. <div>
  1333. <div class=" mb-2.5 text-sm font-medium">OpenAI API Base URL</div>
  1334. <div class="flex w-full">
  1335. <div class="flex-1">
  1336. <input
  1337. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  1338. placeholder="Enter OpenAI API Key"
  1339. bind:value={OPENAI_API_BASE_URL}
  1340. autocomplete="off"
  1341. />
  1342. </div>
  1343. </div>
  1344. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  1345. WebUI will make requests to <span class=" text-gray-200"
  1346. >'{OPENAI_API_BASE_URL}/chat'</span
  1347. >
  1348. </div>
  1349. </div>
  1350. </div>
  1351. <div class="flex justify-end pt-3 text-sm font-medium">
  1352. <button
  1353. class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
  1354. type="submit"
  1355. >
  1356. Save
  1357. </button>
  1358. </div>
  1359. </form>
  1360. {:else if selectedTab === 'interface'}
  1361. <form
  1362. class="flex flex-col h-full justify-between space-y-3 text-sm"
  1363. on:submit|preventDefault={() => {
  1364. updateInterfaceHandler();
  1365. show = false;
  1366. }}
  1367. >
  1368. <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
  1369. <div class="flex w-full justify-between mb-2">
  1370. <div class=" self-center text-sm font-semibold">Default Prompt Suggestions</div>
  1371. <button
  1372. class="p-1 px-3 text-xs flex rounded transition"
  1373. type="button"
  1374. on:click={() => {
  1375. if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') {
  1376. promptSuggestions = [...promptSuggestions, { content: '', title: ['', ''] }];
  1377. }
  1378. }}
  1379. >
  1380. <svg
  1381. xmlns="http://www.w3.org/2000/svg"
  1382. viewBox="0 0 20 20"
  1383. fill="currentColor"
  1384. class="w-4 h-4"
  1385. >
  1386. <path
  1387. d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
  1388. />
  1389. </svg>
  1390. </button>
  1391. </div>
  1392. <div class="flex flex-col space-y-1">
  1393. {#each promptSuggestions as prompt, promptIdx}
  1394. <div class=" flex border dark:border-gray-600 rounded-lg">
  1395. <div class="flex flex-col flex-1">
  1396. <div class="flex border-b dark:border-gray-600 w-full">
  1397. <input
  1398. class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
  1399. placeholder="Title (e.g. Tell me a fun fact)"
  1400. bind:value={prompt.title[0]}
  1401. />
  1402. <input
  1403. class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
  1404. placeholder="Subtitle (e.g. about the Roman Empire)"
  1405. bind:value={prompt.title[1]}
  1406. />
  1407. </div>
  1408. <input
  1409. class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
  1410. placeholder="Prompt (e.g. Tell me a fun fact about the Roman Empire)"
  1411. bind:value={prompt.content}
  1412. />
  1413. </div>
  1414. <button
  1415. class="px-2"
  1416. type="button"
  1417. on:click={() => {
  1418. promptSuggestions.splice(promptIdx, 1);
  1419. promptSuggestions = promptSuggestions;
  1420. }}
  1421. >
  1422. <svg
  1423. xmlns="http://www.w3.org/2000/svg"
  1424. viewBox="0 0 20 20"
  1425. fill="currentColor"
  1426. class="w-4 h-4"
  1427. >
  1428. <path
  1429. 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"
  1430. />
  1431. </svg>
  1432. </button>
  1433. </div>
  1434. {/each}
  1435. </div>
  1436. {#if promptSuggestions.length > 0}
  1437. <div class="text-xs text-left w-full mt-2">
  1438. Adjusting these settings will apply changes universally to all users.
  1439. </div>
  1440. {/if}
  1441. </div>
  1442. <div class="flex justify-end pt-3 text-sm font-medium">
  1443. <button
  1444. class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
  1445. type="submit"
  1446. >
  1447. Save
  1448. </button>
  1449. </div>
  1450. </form>
  1451. {:else if selectedTab === 'addons'}
  1452. <form
  1453. class="flex flex-col h-full justify-between space-y-3 text-sm"
  1454. on:submit|preventDefault={() => {
  1455. saveSettings({
  1456. speakVoice: speakVoice !== '' ? speakVoice : undefined
  1457. });
  1458. show = false;
  1459. }}
  1460. >
  1461. <div class=" space-y-3">
  1462. <div>
  1463. <div class=" mb-1 text-sm font-medium">WebUI Add-ons</div>
  1464. <div>
  1465. <div class=" py-0.5 flex w-full justify-between">
  1466. <div class=" self-center text-xs font-medium">Title Auto-Generation</div>
  1467. <button
  1468. class="p-1 px-3 text-xs flex rounded transition"
  1469. on:click={() => {
  1470. toggleTitleAutoGenerate();
  1471. }}
  1472. type="button"
  1473. >
  1474. {#if titleAutoGenerate === true}
  1475. <span class="ml-2 self-center">On</span>
  1476. {:else}
  1477. <span class="ml-2 self-center">Off</span>
  1478. {/if}
  1479. </button>
  1480. </div>
  1481. </div>
  1482. <div>
  1483. <div class=" py-0.5 flex w-full justify-between">
  1484. <div class=" self-center text-xs font-medium">Voice Input Auto-Send</div>
  1485. <button
  1486. class="p-1 px-3 text-xs flex rounded transition"
  1487. on:click={() => {
  1488. toggleSpeechAutoSend();
  1489. }}
  1490. type="button"
  1491. >
  1492. {#if speechAutoSend === true}
  1493. <span class="ml-2 self-center">On</span>
  1494. {:else}
  1495. <span class="ml-2 self-center">Off</span>
  1496. {/if}
  1497. </button>
  1498. </div>
  1499. </div>
  1500. <div>
  1501. <div class=" py-0.5 flex w-full justify-between">
  1502. <div class=" self-center text-xs font-medium">
  1503. Response AutoCopy to Clipboard
  1504. </div>
  1505. <button
  1506. class="p-1 px-3 text-xs flex rounded transition"
  1507. on:click={() => {
  1508. toggleResponseAutoCopy();
  1509. }}
  1510. type="button"
  1511. >
  1512. {#if responseAutoCopy === true}
  1513. <span class="ml-2 self-center">On</span>
  1514. {:else}
  1515. <span class="ml-2 self-center">Off</span>
  1516. {/if}
  1517. </button>
  1518. </div>
  1519. </div>
  1520. </div>
  1521. <hr class=" dark:border-gray-700" />
  1522. <div>
  1523. <div class=" mb-2.5 text-sm font-medium">Set Title Auto-Generation Model</div>
  1524. <div class="flex w-full">
  1525. <div class="flex-1 mr-2">
  1526. <select
  1527. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  1528. bind:value={titleAutoGenerateModel}
  1529. placeholder="Select a model"
  1530. >
  1531. <option value="" selected>Default</option>
  1532. {#each $models.filter((m) => m.size != null) as model}
  1533. <option value={model.name} class="bg-gray-100 dark:bg-gray-700"
  1534. >{model.name +
  1535. ' (' +
  1536. (model.size / 1024 ** 3).toFixed(1) +
  1537. ' GB)'}</option
  1538. >
  1539. {/each}
  1540. </select>
  1541. </div>
  1542. <button
  1543. class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-800 dark:text-gray-100 rounded transition"
  1544. on:click={() => {
  1545. saveSettings({
  1546. titleAutoGenerateModel:
  1547. titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined
  1548. });
  1549. }}
  1550. type="button"
  1551. >
  1552. <svg
  1553. xmlns="http://www.w3.org/2000/svg"
  1554. viewBox="0 0 16 16"
  1555. fill="currentColor"
  1556. class="w-3.5 h-3.5"
  1557. >
  1558. <path
  1559. fill-rule="evenodd"
  1560. d="M13.836 2.477a.75.75 0 0 1 .75.75v3.182a.75.75 0 0 1-.75.75h-3.182a.75.75 0 0 1 0-1.5h1.37l-.84-.841a4.5 4.5 0 0 0-7.08.932.75.75 0 0 1-1.3-.75 6 6 0 0 1 9.44-1.242l.842.84V3.227a.75.75 0 0 1 .75-.75Zm-.911 7.5A.75.75 0 0 1 13.199 11a6 6 0 0 1-9.44 1.241l-.84-.84v1.371a.75.75 0 0 1-1.5 0V9.591a.75.75 0 0 1 .75-.75H5.35a.75.75 0 0 1 0 1.5H3.98l.841.841a4.5 4.5 0 0 0 7.08-.932.75.75 0 0 1 1.025-.273Z"
  1561. clip-rule="evenodd"
  1562. />
  1563. </svg>
  1564. </button>
  1565. </div>
  1566. </div>
  1567. <hr class=" dark:border-gray-700" />
  1568. <div class=" space-y-3">
  1569. <div>
  1570. <div class=" mb-2.5 text-sm font-medium">Set Default Voice</div>
  1571. <div class="flex w-full">
  1572. <div class="flex-1">
  1573. <select
  1574. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  1575. bind:value={speakVoice}
  1576. placeholder="Select a voice"
  1577. >
  1578. <option value="" selected>Default</option>
  1579. {#each $voices.filter((v) => v.localService === true) as voice}
  1580. <option value={voice.name} class="bg-gray-100 dark:bg-gray-700"
  1581. >{voice.name}</option
  1582. >
  1583. {/each}
  1584. </select>
  1585. </div>
  1586. </div>
  1587. </div>
  1588. </div>
  1589. <!--
  1590. <div>
  1591. <div class=" mb-2.5 text-sm font-medium">
  1592. Gravatar Email <span class=" text-gray-400 text-sm">(optional)</span>
  1593. </div>
  1594. <div class="flex w-full">
  1595. <div class="flex-1">
  1596. <input
  1597. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  1598. placeholder="Enter Your Email"
  1599. bind:value={gravatarEmail}
  1600. autocomplete="off"
  1601. type="email"
  1602. />
  1603. </div>
  1604. </div>
  1605. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  1606. Changes user profile image to match your <a
  1607. class=" text-gray-500 dark:text-gray-300 font-medium"
  1608. href="https://gravatar.com/"
  1609. target="_blank">Gravatar.</a
  1610. >
  1611. </div>
  1612. </div> -->
  1613. </div>
  1614. <div class="flex justify-end pt-3 text-sm font-medium">
  1615. <button
  1616. class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
  1617. type="submit"
  1618. >
  1619. Save
  1620. </button>
  1621. </div>
  1622. </form>
  1623. {:else if selectedTab === 'chats'}
  1624. <div class="flex flex-col h-full justify-between space-y-3 text-sm">
  1625. <div class=" space-y-2">
  1626. <div
  1627. class="flex flex-col justify-between rounded-md items-center py-2 px-3.5 w-full transition"
  1628. >
  1629. <div class="flex w-full justify-between">
  1630. <div class=" self-center text-sm font-medium">Chat History</div>
  1631. <button
  1632. class="p-1 px-3 text-xs flex rounded transition"
  1633. type="button"
  1634. on:click={() => {
  1635. toggleSaveChatHistory();
  1636. }}
  1637. >
  1638. {#if saveChatHistory === true}
  1639. <svg
  1640. xmlns="http://www.w3.org/2000/svg"
  1641. viewBox="0 0 16 16"
  1642. fill="currentColor"
  1643. class="w-4 h-4"
  1644. >
  1645. <path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" />
  1646. <path
  1647. fill-rule="evenodd"
  1648. d="M1.38 8.28a.87.87 0 0 1 0-.566 7.003 7.003 0 0 1 13.238.006.87.87 0 0 1 0 .566A7.003 7.003 0 0 1 1.379 8.28ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
  1649. clip-rule="evenodd"
  1650. />
  1651. </svg>
  1652. <span class="ml-2 self-center"> On </span>
  1653. {:else}
  1654. <svg
  1655. xmlns="http://www.w3.org/2000/svg"
  1656. viewBox="0 0 16 16"
  1657. fill="currentColor"
  1658. class="w-4 h-4"
  1659. >
  1660. <path
  1661. fill-rule="evenodd"
  1662. d="M3.28 2.22a.75.75 0 0 0-1.06 1.06l10.5 10.5a.75.75 0 1 0 1.06-1.06l-1.322-1.323a7.012 7.012 0 0 0 2.16-3.11.87.87 0 0 0 0-.567A7.003 7.003 0 0 0 4.82 3.76l-1.54-1.54Zm3.196 3.195 1.135 1.136A1.502 1.502 0 0 1 9.45 8.389l1.136 1.135a3 3 0 0 0-4.109-4.109Z"
  1663. clip-rule="evenodd"
  1664. />
  1665. <path
  1666. d="m7.812 10.994 1.816 1.816A7.003 7.003 0 0 1 1.38 8.28a.87.87 0 0 1 0-.566 6.985 6.985 0 0 1 1.113-2.039l2.513 2.513a3 3 0 0 0 2.806 2.806Z"
  1667. />
  1668. </svg>
  1669. <span class="ml-2 self-center">Off</span>
  1670. {/if}
  1671. </button>
  1672. </div>
  1673. <div class="text-xs text-left w-full font-medium mt-0.5">
  1674. This setting does not sync across browsers or devices.
  1675. </div>
  1676. </div>
  1677. <hr class=" dark:border-gray-700" />
  1678. <div class="flex flex-col">
  1679. <input
  1680. id="chat-import-input"
  1681. bind:files={importFiles}
  1682. type="file"
  1683. accept=".json"
  1684. hidden
  1685. />
  1686. <button
  1687. class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
  1688. on:click={() => {
  1689. document.getElementById('chat-import-input').click();
  1690. }}
  1691. >
  1692. <div class=" self-center mr-3">
  1693. <svg
  1694. xmlns="http://www.w3.org/2000/svg"
  1695. viewBox="0 0 16 16"
  1696. fill="currentColor"
  1697. class="w-4 h-4"
  1698. >
  1699. <path
  1700. fill-rule="evenodd"
  1701. 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"
  1702. clip-rule="evenodd"
  1703. />
  1704. </svg>
  1705. </div>
  1706. <div class=" self-center text-sm font-medium">Import Chats</div>
  1707. </button>
  1708. <button
  1709. class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
  1710. on:click={() => {
  1711. exportChats();
  1712. }}
  1713. >
  1714. <div class=" self-center mr-3">
  1715. <svg
  1716. xmlns="http://www.w3.org/2000/svg"
  1717. viewBox="0 0 16 16"
  1718. fill="currentColor"
  1719. class="w-4 h-4"
  1720. >
  1721. <path
  1722. fill-rule="evenodd"
  1723. 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"
  1724. clip-rule="evenodd"
  1725. />
  1726. </svg>
  1727. </div>
  1728. <div class=" self-center text-sm font-medium">Export Chats</div>
  1729. </button>
  1730. </div>
  1731. <hr class=" dark:border-gray-700" />
  1732. {#if showDeleteConfirm}
  1733. <div
  1734. class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition"
  1735. >
  1736. <div class="flex items-center space-x-3">
  1737. <svg
  1738. xmlns="http://www.w3.org/2000/svg"
  1739. viewBox="0 0 16 16"
  1740. fill="currentColor"
  1741. class="w-4 h-4"
  1742. >
  1743. <path
  1744. d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z"
  1745. />
  1746. <path
  1747. fill-rule="evenodd"
  1748. d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM5.72 7.47a.75.75 0 0 1 1.06 0L8 8.69l1.22-1.22a.75.75 0 1 1 1.06 1.06L9.06 9.75l1.22 1.22a.75.75 0 1 1-1.06 1.06L8 10.81l-1.22 1.22a.75.75 0 0 1-1.06-1.06l1.22-1.22-1.22-1.22a.75.75 0 0 1 0-1.06Z"
  1749. clip-rule="evenodd"
  1750. />
  1751. </svg>
  1752. <span>Are you sure?</span>
  1753. </div>
  1754. <div class="flex space-x-1.5 items-center">
  1755. <button
  1756. class="hover:text-white transition"
  1757. on:click={() => {
  1758. deleteChats();
  1759. showDeleteConfirm = false;
  1760. }}
  1761. >
  1762. <svg
  1763. xmlns="http://www.w3.org/2000/svg"
  1764. viewBox="0 0 20 20"
  1765. fill="currentColor"
  1766. class="w-4 h-4"
  1767. >
  1768. <path
  1769. fill-rule="evenodd"
  1770. 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"
  1771. clip-rule="evenodd"
  1772. />
  1773. </svg>
  1774. </button>
  1775. <button
  1776. class="hover:text-white transition"
  1777. on:click={() => {
  1778. showDeleteConfirm = false;
  1779. }}
  1780. >
  1781. <svg
  1782. xmlns="http://www.w3.org/2000/svg"
  1783. viewBox="0 0 20 20"
  1784. fill="currentColor"
  1785. class="w-4 h-4"
  1786. >
  1787. <path
  1788. 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"
  1789. />
  1790. </svg>
  1791. </button>
  1792. </div>
  1793. </div>
  1794. {:else}
  1795. <button
  1796. class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
  1797. on:click={() => {
  1798. showDeleteConfirm = true;
  1799. }}
  1800. >
  1801. <div class=" self-center mr-3">
  1802. <svg
  1803. xmlns="http://www.w3.org/2000/svg"
  1804. viewBox="0 0 16 16"
  1805. fill="currentColor"
  1806. class="w-4 h-4"
  1807. >
  1808. <path
  1809. d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z"
  1810. />
  1811. <path
  1812. fill-rule="evenodd"
  1813. d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM5.72 7.47a.75.75 0 0 1 1.06 0L8 8.69l1.22-1.22a.75.75 0 1 1 1.06 1.06L9.06 9.75l1.22 1.22a.75.75 0 1 1-1.06 1.06L8 10.81l-1.22 1.22a.75.75 0 0 1-1.06-1.06l1.22-1.22-1.22-1.22a.75.75 0 0 1 0-1.06Z"
  1814. clip-rule="evenodd"
  1815. />
  1816. </svg>
  1817. </div>
  1818. <div class=" self-center text-sm font-medium">Delete All Chats</div>
  1819. </button>
  1820. {/if}
  1821. {#if $user?.role === 'admin'}
  1822. <hr class=" dark:border-gray-700" />
  1823. <button
  1824. class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
  1825. on:click={() => {
  1826. const res = resetVectorDB(localStorage.token).catch((error) => {
  1827. toast.error(error);
  1828. return null;
  1829. });
  1830. if (res) {
  1831. toast.success('Success');
  1832. }
  1833. }}
  1834. >
  1835. <div class=" self-center mr-3">
  1836. <svg
  1837. xmlns="http://www.w3.org/2000/svg"
  1838. viewBox="0 0 16 16"
  1839. fill="currentColor"
  1840. class="w-4 h-4"
  1841. >
  1842. <path
  1843. fill-rule="evenodd"
  1844. d="M3.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z"
  1845. clip-rule="evenodd"
  1846. />
  1847. </svg>
  1848. </div>
  1849. <div class=" self-center text-sm font-medium">Reset Vector Storage</div>
  1850. </button>
  1851. {/if}
  1852. </div>
  1853. </div>
  1854. {:else if selectedTab === 'auth'}
  1855. <form
  1856. class="flex flex-col h-full justify-between space-y-3 text-sm"
  1857. on:submit|preventDefault={() => {
  1858. console.log('auth save');
  1859. saveSettings({
  1860. authHeader: authEnabled ? `${authType} ${authContent}` : undefined
  1861. });
  1862. show = false;
  1863. }}
  1864. >
  1865. <div class=" space-y-3">
  1866. <div>
  1867. <div class=" py-1 flex w-full justify-between">
  1868. <div class=" self-center text-sm font-medium">Authorization Header</div>
  1869. <button
  1870. class="p-1 px-3 text-xs flex rounded transition"
  1871. type="button"
  1872. on:click={() => {
  1873. toggleAuthHeader();
  1874. }}
  1875. >
  1876. {#if authEnabled === true}
  1877. <svg
  1878. xmlns="http://www.w3.org/2000/svg"
  1879. viewBox="0 0 24 24"
  1880. fill="currentColor"
  1881. class="w-4 h-4"
  1882. >
  1883. <path
  1884. fill-rule="evenodd"
  1885. 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"
  1886. clip-rule="evenodd"
  1887. />
  1888. </svg>
  1889. <span class="ml-2 self-center"> On </span>
  1890. {:else}
  1891. <svg
  1892. xmlns="http://www.w3.org/2000/svg"
  1893. viewBox="0 0 24 24"
  1894. fill="currentColor"
  1895. class="w-4 h-4"
  1896. >
  1897. <path
  1898. 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"
  1899. />
  1900. </svg>
  1901. <span class="ml-2 self-center">Off</span>
  1902. {/if}
  1903. </button>
  1904. </div>
  1905. </div>
  1906. {#if authEnabled}
  1907. <hr class=" dark:border-gray-700" />
  1908. <div class="mt-2">
  1909. <div class=" py-1 flex w-full space-x-2">
  1910. <button
  1911. class=" py-1 font-semibold flex rounded transition"
  1912. on:click={() => {
  1913. authType = authType === 'Basic' ? 'Bearer' : 'Basic';
  1914. }}
  1915. type="button"
  1916. >
  1917. {#if authType === 'Basic'}
  1918. <span class="self-center mr-2">Basic</span>
  1919. {:else if authType === 'Bearer'}
  1920. <span class="self-center mr-2">Bearer</span>
  1921. {/if}
  1922. </button>
  1923. <div class="flex-1">
  1924. <input
  1925. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  1926. placeholder="Enter Authorization Header Content"
  1927. bind:value={authContent}
  1928. />
  1929. </div>
  1930. </div>
  1931. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  1932. Toggle between <span class=" text-gray-500 dark:text-gray-300 font-medium"
  1933. >'Basic'</span
  1934. >
  1935. and <span class=" text-gray-500 dark:text-gray-300 font-medium">'Bearer'</span> by
  1936. clicking on the label next to the input.
  1937. </div>
  1938. </div>
  1939. <hr class=" dark:border-gray-700" />
  1940. <div>
  1941. <div class=" mb-2.5 text-sm font-medium">Preview Authorization Header</div>
  1942. <textarea
  1943. value={JSON.stringify({
  1944. Authorization: `${authType} ${authContent}`
  1945. })}
  1946. class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
  1947. rows="2"
  1948. disabled
  1949. />
  1950. </div>
  1951. {/if}
  1952. </div>
  1953. <div class="flex justify-end pt-3 text-sm font-medium">
  1954. <button
  1955. class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
  1956. type="submit"
  1957. >
  1958. Save
  1959. </button>
  1960. </div>
  1961. </form>
  1962. {:else if selectedTab === 'account'}
  1963. <form
  1964. class="flex flex-col h-full text-sm"
  1965. on:submit|preventDefault={() => {
  1966. updatePasswordHandler();
  1967. }}
  1968. >
  1969. <div class=" mb-2.5 font-medium">Change Password</div>
  1970. <div class=" space-y-1.5">
  1971. <div class="flex flex-col w-full">
  1972. <div class=" mb-1 text-xs text-gray-500">Current Password</div>
  1973. <div class="flex-1">
  1974. <input
  1975. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  1976. type="password"
  1977. bind:value={currentPassword}
  1978. autocomplete="current-password"
  1979. required
  1980. />
  1981. </div>
  1982. </div>
  1983. <div class="flex flex-col w-full">
  1984. <div class=" mb-1 text-xs text-gray-500">New Password</div>
  1985. <div class="flex-1">
  1986. <input
  1987. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  1988. type="password"
  1989. bind:value={newPassword}
  1990. autocomplete="new-password"
  1991. required
  1992. />
  1993. </div>
  1994. </div>
  1995. <div class="flex flex-col w-full">
  1996. <div class=" mb-1 text-xs text-gray-500">Confirm Password</div>
  1997. <div class="flex-1">
  1998. <input
  1999. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  2000. type="password"
  2001. bind:value={newPasswordConfirm}
  2002. autocomplete="off"
  2003. required
  2004. />
  2005. </div>
  2006. </div>
  2007. </div>
  2008. <div class="mt-3 flex justify-end">
  2009. <button
  2010. class=" px-4 py-2 text-xs bg-gray-800 hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-800 text-gray-100 transition rounded-md font-medium"
  2011. >
  2012. Update password
  2013. </button>
  2014. </div>
  2015. </form>
  2016. {:else if selectedTab === 'about'}
  2017. <div class="flex flex-col h-full justify-between space-y-3 text-sm mb-6">
  2018. <div class=" space-y-3">
  2019. <div>
  2020. <div class=" mb-2.5 text-sm font-medium">Ollama Web UI Version</div>
  2021. <div class="flex w-full">
  2022. <div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
  2023. {$config && $config.version ? $config.version : WEB_UI_VERSION}
  2024. </div>
  2025. </div>
  2026. </div>
  2027. <hr class=" dark:border-gray-700" />
  2028. <div>
  2029. <div class=" mb-2.5 text-sm font-medium">Ollama Version</div>
  2030. <div class="flex w-full">
  2031. <div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
  2032. {ollamaVersion ?? 'N/A'}
  2033. </div>
  2034. </div>
  2035. </div>
  2036. <hr class=" dark:border-gray-700" />
  2037. <div class="flex space-x-1">
  2038. <a href="https://discord.gg/5rJgQTnV4s" target="_blank">
  2039. <img
  2040. alt="Discord"
  2041. src="https://img.shields.io/badge/Discord-Ollama_Web_UI-blue?logo=discord&logoColor=white"
  2042. />
  2043. </a>
  2044. <a href="https://github.com/ollama-webui/ollama-webui" target="_blank">
  2045. <img
  2046. alt="Github Repo"
  2047. src="https://img.shields.io/github/stars/ollama-webui/ollama-webui?style=social&label=Star us on Github"
  2048. />
  2049. </a>
  2050. </div>
  2051. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  2052. Created by <a
  2053. class=" text-gray-500 dark:text-gray-300 font-medium"
  2054. href="https://github.com/tjbck"
  2055. target="_blank">Timothy J. Baek</a
  2056. >
  2057. </div>
  2058. </div>
  2059. </div>
  2060. {/if}
  2061. </div>
  2062. </div>
  2063. </div>
  2064. </Modal>
  2065. <style>
  2066. input::-webkit-outer-spin-button,
  2067. input::-webkit-inner-spin-button {
  2068. /* display: none; <- Crashes Chrome on hover */
  2069. -webkit-appearance: none;
  2070. margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
  2071. }
  2072. .tabs::-webkit-scrollbar {
  2073. display: none; /* for Chrome, Safari and Opera */
  2074. }
  2075. .tabs {
  2076. -ms-overflow-style: none; /* IE and Edge */
  2077. scrollbar-width: none; /* Firefox */
  2078. }
  2079. input[type='number'] {
  2080. -moz-appearance: textfield; /* Firefox */
  2081. }
  2082. </style>