SettingsModal.svelte 68 KB

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