SettingsModal.svelte 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812
  1. <script lang="ts">
  2. import sha256 from 'js-sha256';
  3. import Modal from '../common/Modal.svelte';
  4. import { WEB_UI_VERSION, API_BASE_URL as BUILD_TIME_API_BASE_URL } from '$lib/constants';
  5. import toast from 'svelte-french-toast';
  6. export let show = false;
  7. export let saveSettings: Function;
  8. export let getModelTags: Function;
  9. let selectedTab = 'general';
  10. // General
  11. let API_BASE_URL = BUILD_TIME_API_BASE_URL;
  12. let system = '';
  13. let theme = 'dark';
  14. // Models
  15. let modelTag = '';
  16. let deleteModelTag = '';
  17. let digest = '';
  18. let pullProgress = null;
  19. // Advanced
  20. let seed = 0;
  21. let temperature = 0.8;
  22. let repeat_penalty = 1.1;
  23. let top_k = 40;
  24. let top_p = 0.9;
  25. // Addons
  26. let gravatarEmail = '';
  27. let OPENAI_API_KEY = '';
  28. function getGravatarURL(email) {
  29. // Trim leading and trailing whitespace from
  30. // an email address and force all characters
  31. // to lower case
  32. const address = String(email).trim().toLowerCase();
  33. // Create a SHA256 hash of the final string
  34. const hash = sha256(address);
  35. // Grab the actual image URL
  36. return `https://www.gravatar.com/avatar/${hash}`;
  37. }
  38. const splitStream = (splitOn) => {
  39. let buffer = '';
  40. return new TransformStream({
  41. transform(chunk, controller) {
  42. buffer += chunk;
  43. const parts = buffer.split(splitOn);
  44. parts.slice(0, -1).forEach((part) => controller.enqueue(part));
  45. buffer = parts[parts.length - 1];
  46. },
  47. flush(controller) {
  48. if (buffer) controller.enqueue(buffer);
  49. }
  50. });
  51. };
  52. const checkOllamaConnection = async () => {
  53. if (API_BASE_URL === '') {
  54. API_BASE_URL = BUILD_TIME_API_BASE_URL;
  55. }
  56. const res = await getModelTags(API_BASE_URL, 'ollama');
  57. if (res) {
  58. toast.success('Server connection verified');
  59. saveSettings({
  60. API_BASE_URL: API_BASE_URL
  61. });
  62. }
  63. };
  64. const toggleTheme = async () => {
  65. if (theme === 'dark') {
  66. theme = 'light';
  67. } else {
  68. theme = 'dark';
  69. }
  70. localStorage.theme = theme;
  71. document.documentElement.classList.remove(theme === 'dark' ? 'light' : 'dark');
  72. document.documentElement.classList.add(theme);
  73. };
  74. const pullModelHandler = async () => {
  75. const res = await fetch(`${API_BASE_URL}/pull`, {
  76. method: 'POST',
  77. headers: {
  78. 'Content-Type': 'text/event-stream'
  79. },
  80. body: JSON.stringify({
  81. name: modelTag
  82. })
  83. });
  84. const reader = res.body
  85. .pipeThrough(new TextDecoderStream())
  86. .pipeThrough(splitStream('\n'))
  87. .getReader();
  88. while (true) {
  89. const { value, done } = await reader.read();
  90. if (done) break;
  91. try {
  92. let lines = value.split('\n');
  93. for (const line of lines) {
  94. if (line !== '') {
  95. console.log(line);
  96. let data = JSON.parse(line);
  97. console.log(data);
  98. if (data.error) {
  99. throw data.error;
  100. }
  101. if (data.status) {
  102. if (!data.status.includes('downloading')) {
  103. toast.success(data.status);
  104. } else {
  105. digest = data.digest;
  106. if (data.completed) {
  107. pullProgress = Math.round((data.completed / data.total) * 1000) / 10;
  108. } else {
  109. pullProgress = 100;
  110. }
  111. }
  112. }
  113. }
  114. }
  115. } catch (error) {
  116. console.log(error);
  117. toast.error(error);
  118. }
  119. }
  120. modelTag = '';
  121. await getModelTags();
  122. };
  123. const deleteModelHandler = async () => {
  124. const res = await fetch(`${API_BASE_URL}/delete`, {
  125. method: 'DELETE',
  126. headers: {
  127. 'Content-Type': 'text/event-stream'
  128. },
  129. body: JSON.stringify({
  130. name: deleteModelTag
  131. })
  132. });
  133. const reader = res.body
  134. .pipeThrough(new TextDecoderStream())
  135. .pipeThrough(splitStream('\n'))
  136. .getReader();
  137. while (true) {
  138. const { value, done } = await reader.read();
  139. if (done) break;
  140. try {
  141. let lines = value.split('\n');
  142. for (const line of lines) {
  143. if (line !== '' && line !== 'null') {
  144. console.log(line);
  145. let data = JSON.parse(line);
  146. console.log(data);
  147. if (data.error) {
  148. throw data.error;
  149. }
  150. if (data.status) {
  151. }
  152. } else {
  153. toast.success(`Deleted ${deleteModelTag}`);
  154. }
  155. }
  156. } catch (error) {
  157. console.log(error);
  158. toast.error(error);
  159. }
  160. }
  161. deleteModelTag = '';
  162. await getModelTags();
  163. };
  164. $: if (show) {
  165. let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
  166. console.log(settings);
  167. theme = localStorage.theme ?? 'dark';
  168. API_BASE_URL = settings.API_BASE_URL ?? BUILD_TIME_API_BASE_URL;
  169. system = settings.system ?? '';
  170. seed = settings.seed ?? 0;
  171. temperature = settings.temperature ?? 0.8;
  172. repeat_penalty = settings.repeat_penalty ?? 1.1;
  173. top_k = settings.top_k ?? 40;
  174. top_p = settings.top_p ?? 0.9;
  175. OPENAI_API_KEY = settings.OPENAI_API_KEY ?? '';
  176. gravatarEmail = settings.gravatarEmail ?? '';
  177. }
  178. </script>
  179. <Modal bind:show>
  180. <div>
  181. <div class=" flex justify-between dark:text-gray-300 px-5 py-4">
  182. <div class=" text-lg font-medium self-center">Settings</div>
  183. <button
  184. class="self-center"
  185. on:click={() => {
  186. show = false;
  187. }}
  188. >
  189. <svg
  190. xmlns="http://www.w3.org/2000/svg"
  191. viewBox="0 0 20 20"
  192. fill="currentColor"
  193. class="w-5 h-5"
  194. >
  195. <path
  196. 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"
  197. />
  198. </svg>
  199. </button>
  200. </div>
  201. <hr class=" dark:border-gray-800" />
  202. <div class="flex flex-col md:flex-row w-full p-4 md:space-x-4">
  203. <div
  204. 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"
  205. >
  206. <button
  207. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  208. 'general'
  209. ? 'bg-gray-200 dark:bg-gray-700'
  210. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  211. on:click={() => {
  212. selectedTab = 'general';
  213. }}
  214. >
  215. <div class=" self-center mr-2">
  216. <svg
  217. xmlns="http://www.w3.org/2000/svg"
  218. viewBox="0 0 20 20"
  219. fill="currentColor"
  220. class="w-4 h-4"
  221. >
  222. <path
  223. fill-rule="evenodd"
  224. 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"
  225. clip-rule="evenodd"
  226. />
  227. </svg>
  228. </div>
  229. <div class=" self-center">General</div>
  230. </button>
  231. <button
  232. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  233. 'advanced'
  234. ? 'bg-gray-200 dark:bg-gray-700'
  235. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  236. on:click={() => {
  237. selectedTab = 'advanced';
  238. }}
  239. >
  240. <div class=" self-center mr-2">
  241. <svg
  242. xmlns="http://www.w3.org/2000/svg"
  243. viewBox="0 0 20 20"
  244. fill="currentColor"
  245. class="w-4 h-4"
  246. >
  247. <path
  248. 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"
  249. />
  250. </svg>
  251. </div>
  252. <div class=" self-center">Advanced</div>
  253. </button>
  254. <button
  255. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  256. 'models'
  257. ? 'bg-gray-200 dark:bg-gray-700'
  258. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  259. on:click={() => {
  260. selectedTab = 'models';
  261. }}
  262. >
  263. <div class=" self-center mr-2">
  264. <svg
  265. xmlns="http://www.w3.org/2000/svg"
  266. viewBox="0 0 20 20"
  267. fill="currentColor"
  268. class="w-4 h-4"
  269. >
  270. <path
  271. fill-rule="evenodd"
  272. 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"
  273. clip-rule="evenodd"
  274. />
  275. </svg>
  276. </div>
  277. <div class=" self-center">Models</div>
  278. </button>
  279. <button
  280. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  281. 'addons'
  282. ? 'bg-gray-200 dark:bg-gray-700'
  283. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  284. on:click={() => {
  285. selectedTab = 'addons';
  286. }}
  287. >
  288. <div class=" self-center mr-2">
  289. <svg
  290. xmlns="http://www.w3.org/2000/svg"
  291. viewBox="0 0 20 20"
  292. fill="currentColor"
  293. class="w-4 h-4"
  294. >
  295. <path
  296. 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"
  297. />
  298. </svg>
  299. </div>
  300. <div class=" self-center">Add-ons</div>
  301. </button>
  302. <button
  303. class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
  304. 'about'
  305. ? 'bg-gray-200 dark:bg-gray-700'
  306. : ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
  307. on:click={() => {
  308. selectedTab = 'about';
  309. }}
  310. >
  311. <div class=" self-center mr-2">
  312. <svg
  313. xmlns="http://www.w3.org/2000/svg"
  314. viewBox="0 0 20 20"
  315. fill="currentColor"
  316. class="w-4 h-4"
  317. >
  318. <path
  319. fill-rule="evenodd"
  320. 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"
  321. clip-rule="evenodd"
  322. />
  323. </svg>
  324. </div>
  325. <div class=" self-center">About</div>
  326. </button>
  327. </div>
  328. <div class="flex-1 md:min-h-[300px]">
  329. {#if selectedTab === 'general'}
  330. <div class="flex flex-col space-y-3">
  331. <div>
  332. <div class=" py-1 flex w-full justify-between">
  333. <div class=" self-center text-sm font-medium">Theme</div>
  334. <button
  335. class="p-1 px-3 text-xs flex rounded transition"
  336. on:click={() => {
  337. toggleTheme();
  338. }}
  339. >
  340. {#if theme === 'dark'}
  341. <svg
  342. xmlns="http://www.w3.org/2000/svg"
  343. viewBox="0 0 20 20"
  344. fill="currentColor"
  345. class="w-4 h-4"
  346. >
  347. <path
  348. fill-rule="evenodd"
  349. 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"
  350. clip-rule="evenodd"
  351. />
  352. </svg>
  353. <span class="ml-2 self-center"> Dark </span>
  354. {:else}
  355. <svg
  356. xmlns="http://www.w3.org/2000/svg"
  357. viewBox="0 0 20 20"
  358. fill="currentColor"
  359. class="w-4 h-4 self-center"
  360. >
  361. <path
  362. 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"
  363. />
  364. </svg>
  365. <span class="ml-2 self-center"> Light </span>
  366. {/if}
  367. </button>
  368. </div>
  369. </div>
  370. <hr class=" dark:border-gray-700" />
  371. <div>
  372. <div class=" mb-2.5 text-sm font-medium">Ollama Server URL</div>
  373. <div class="flex w-full">
  374. <div class="flex-1 mr-2">
  375. <input
  376. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  377. placeholder="Enter URL (e.g. http://localhost:11434/api)"
  378. bind:value={API_BASE_URL}
  379. />
  380. </div>
  381. <button
  382. class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded transition"
  383. on:click={() => {
  384. checkOllamaConnection();
  385. }}
  386. >
  387. <svg
  388. xmlns="http://www.w3.org/2000/svg"
  389. viewBox="0 0 20 20"
  390. fill="currentColor"
  391. class="w-4 h-4"
  392. >
  393. <path
  394. fill-rule="evenodd"
  395. 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"
  396. clip-rule="evenodd"
  397. />
  398. </svg>
  399. </button>
  400. </div>
  401. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  402. Trouble accessing Ollama? <a
  403. class=" text-gray-500 dark:text-gray-300 font-medium"
  404. href="https://github.com/ollama-webui/ollama-webui#troubleshooting"
  405. target="_blank"
  406. >
  407. Click here for help.
  408. </a>
  409. </div>
  410. </div>
  411. <hr class=" dark:border-gray-700" />
  412. <div>
  413. <div class=" mb-2.5 text-sm font-medium">System Prompt</div>
  414. <textarea
  415. bind:value={system}
  416. class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
  417. rows="4"
  418. />
  419. </div>
  420. <div class="flex justify-end pt-3 text-sm font-medium">
  421. <button
  422. class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
  423. on:click={() => {
  424. saveSettings({
  425. API_BASE_URL: API_BASE_URL === '' ? BUILD_TIME_API_BASE_URL : API_BASE_URL,
  426. system: system !== '' ? system : undefined
  427. });
  428. show = false;
  429. }}
  430. >
  431. Save
  432. </button>
  433. </div>
  434. </div>
  435. {:else if selectedTab === 'advanced'}
  436. <div class="flex flex-col h-full justify-between space-y-3 text-sm">
  437. <div class=" space-y-3">
  438. <div>
  439. <div class=" mb-2.5 text-sm font-medium">Seed</div>
  440. <div class="flex w-full">
  441. <div class="flex-1">
  442. <input
  443. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  444. type="number"
  445. placeholder="Enter Seed"
  446. bind:value={seed}
  447. autocomplete="off"
  448. min="0"
  449. />
  450. </div>
  451. </div>
  452. </div>
  453. <hr class=" dark:border-gray-700" />
  454. <div>
  455. <label for="steps-range" class=" mb-2 text-sm font-medium flex justify-between">
  456. <div>Temperature</div>
  457. <div>
  458. {temperature}
  459. </div></label
  460. >
  461. <input
  462. id="steps-range"
  463. type="range"
  464. min="0"
  465. max="1"
  466. bind:value={temperature}
  467. step="0.05"
  468. class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
  469. />
  470. </div>
  471. <div>
  472. <label for="steps-range" class=" mb-2 text-sm font-medium flex justify-between">
  473. <div>Repeat Penalty</div>
  474. <div>
  475. {repeat_penalty}
  476. </div></label
  477. >
  478. <input
  479. id="steps-range"
  480. type="range"
  481. min="0"
  482. max="2"
  483. bind:value={repeat_penalty}
  484. step="0.05"
  485. class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
  486. />
  487. </div>
  488. <div>
  489. <label for="steps-range" class=" mb-2 text-sm font-medium flex justify-between">
  490. <div>Top K</div>
  491. <div>
  492. {top_k}
  493. </div></label
  494. >
  495. <input
  496. id="steps-range"
  497. type="range"
  498. min="0"
  499. max="100"
  500. bind:value={top_k}
  501. step="0.5"
  502. class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
  503. />
  504. </div>
  505. <div>
  506. <label for="steps-range" class=" mb-2 text-sm font-medium flex justify-between">
  507. <div>Top P</div>
  508. <div>
  509. {top_p}
  510. </div></label
  511. >
  512. <input
  513. id="steps-range"
  514. type="range"
  515. min="0"
  516. max="1"
  517. bind:value={top_p}
  518. step="0.05"
  519. class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
  520. />
  521. </div>
  522. </div>
  523. <div class="flex justify-end pt-3 text-sm font-medium">
  524. <button
  525. class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
  526. on:click={() => {
  527. saveSettings({
  528. seed: (seed !== 0 ? seed : undefined) ?? undefined,
  529. temperature: temperature !== 0.8 ? temperature : undefined,
  530. repeat_penalty: repeat_penalty !== 1.1 ? repeat_penalty : undefined,
  531. top_k: top_k !== 40 ? top_k : undefined,
  532. top_p: top_p !== 0.9 ? top_p : undefined
  533. });
  534. show = false;
  535. }}
  536. >
  537. Save
  538. </button>
  539. </div>
  540. </div>
  541. {:else if selectedTab === 'models'}
  542. <div class="flex flex-col space-y-3 text-sm mb-10">
  543. <div>
  544. <div class=" mb-2.5 text-sm font-medium">Pull a model</div>
  545. <div class="flex w-full">
  546. <div class="flex-1 mr-2">
  547. <input
  548. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  549. placeholder="Enter model tag (e.g. mistral:7b)"
  550. bind:value={modelTag}
  551. />
  552. </div>
  553. <button
  554. class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 rounded transition"
  555. on:click={() => {
  556. pullModelHandler();
  557. }}
  558. >
  559. <svg
  560. xmlns="http://www.w3.org/2000/svg"
  561. viewBox="0 0 20 20"
  562. fill="currentColor"
  563. class="w-4 h-4"
  564. >
  565. <path
  566. d="M10.75 2.75a.75.75 0 00-1.5 0v8.614L6.295 8.235a.75.75 0 10-1.09 1.03l4.25 4.5a.75.75 0 001.09 0l4.25-4.5a.75.75 0 00-1.09-1.03l-2.955 3.129V2.75z"
  567. />
  568. <path
  569. d="M3.5 12.75a.75.75 0 00-1.5 0v2.5A2.75 2.75 0 004.75 18h10.5A2.75 2.75 0 0018 15.25v-2.5a.75.75 0 00-1.5 0v2.5c0 .69-.56 1.25-1.25 1.25H4.75c-.69 0-1.25-.56-1.25-1.25v-2.5z"
  570. />
  571. </svg>
  572. </button>
  573. </div>
  574. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  575. To access the available model names for downloading, <a
  576. class=" text-gray-500 dark:text-gray-300 font-medium"
  577. href="https://ollama.ai/library"
  578. target="_blank">click here.</a
  579. >
  580. </div>
  581. {#if pullProgress !== null}
  582. <div class="mt-2">
  583. <div class=" mb-2 text-xs">Pull Progress</div>
  584. <div class="w-full rounded-full dark:bg-gray-800">
  585. <div
  586. class="dark:bg-gray-600 text-xs font-medium text-blue-100 text-center p-0.5 leading-none rounded-full"
  587. style="width: {Math.max(15, pullProgress ?? 0)}%"
  588. >
  589. {pullProgress ?? 0}%
  590. </div>
  591. </div>
  592. <div class="mt-1 text-xs dark:text-gray-700" style="font-size: 0.5rem;">
  593. {digest}
  594. </div>
  595. </div>
  596. {/if}
  597. </div>
  598. <hr class=" dark:border-gray-700" />
  599. <div>
  600. <div class=" mb-2.5 text-sm font-medium">Delete a model</div>
  601. <div class="flex w-full">
  602. <div class="flex-1 mr-2">
  603. <input
  604. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  605. placeholder="Enter model tag (e.g. mistral:7b)"
  606. bind:value={deleteModelTag}
  607. />
  608. </div>
  609. <button
  610. class="px-3 bg-red-700 hover:bg-red-800 text-gray-100 rounded transition"
  611. on:click={() => {
  612. deleteModelHandler();
  613. }}
  614. >
  615. <svg
  616. xmlns="http://www.w3.org/2000/svg"
  617. viewBox="0 0 20 20"
  618. fill="currentColor"
  619. class="w-4 h-4"
  620. >
  621. <path
  622. fill-rule="evenodd"
  623. d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 002.742-2.53l.841-10.52.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34.06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z"
  624. clip-rule="evenodd"
  625. />
  626. </svg>
  627. </button>
  628. </div>
  629. </div>
  630. </div>
  631. {:else if selectedTab === 'addons'}
  632. <form
  633. class="flex flex-col h-full justify-between space-y-3 text-sm"
  634. on:submit|preventDefault={() => {
  635. saveSettings({
  636. gravatarEmail: gravatarEmail !== '' ? gravatarEmail : undefined,
  637. gravatarUrl: gravatarEmail !== '' ? getGravatarURL(gravatarEmail) : undefined,
  638. OPENAI_API_KEY: OPENAI_API_KEY !== '' ? OPENAI_API_KEY : undefined
  639. });
  640. show = false;
  641. }}
  642. >
  643. <div class=" space-y-3">
  644. <div>
  645. <div class=" mb-2.5 text-sm font-medium">
  646. Gravatar Email <span class=" text-gray-400 text-sm">(optional)</span>
  647. </div>
  648. <div class="flex w-full">
  649. <div class="flex-1">
  650. <input
  651. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  652. placeholder="Enter Your Email"
  653. bind:value={gravatarEmail}
  654. autocomplete="off"
  655. type="email"
  656. />
  657. </div>
  658. </div>
  659. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  660. Changes user profile image to match your <a
  661. class=" text-gray-500 dark:text-gray-300 font-medium"
  662. href="https://gravatar.com/"
  663. target="_blank">Gravatar.</a
  664. >
  665. </div>
  666. </div>
  667. <hr class=" dark:border-gray-700" />
  668. <div>
  669. <div class=" mb-2.5 text-sm font-medium">
  670. OpenAI API Key <span class=" text-gray-400 text-sm">(optional)</span>
  671. </div>
  672. <div class="flex w-full">
  673. <div class="flex-1">
  674. <input
  675. class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
  676. placeholder="Enter OpenAI API Key"
  677. bind:value={OPENAI_API_KEY}
  678. autocomplete="off"
  679. />
  680. </div>
  681. </div>
  682. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  683. Adds optional support for 'gpt-*' models available.
  684. </div>
  685. </div>
  686. </div>
  687. <div class="flex justify-end pt-3 text-sm font-medium">
  688. <button
  689. class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
  690. type="submit"
  691. >
  692. Save
  693. </button>
  694. </div>
  695. </form>
  696. {:else if selectedTab === 'about'}
  697. <div class="flex flex-col h-full justify-between space-y-3 text-sm mb-6">
  698. <div class=" space-y-3">
  699. <div>
  700. <div class=" mb-2.5 text-sm font-medium">Ollama Web UI Version</div>
  701. <div class="flex w-full">
  702. <div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
  703. {WEB_UI_VERSION}
  704. </div>
  705. </div>
  706. </div>
  707. <hr class=" dark:border-gray-700" />
  708. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  709. Created by <a
  710. class=" text-gray-500 dark:text-gray-300 font-medium"
  711. href="https://github.com/tjbck"
  712. target="_blank">Timothy J. Baek</a
  713. >
  714. </div>
  715. <div>
  716. <a href="https://github.com/ollama-webui/ollama-webui">
  717. <img
  718. alt="Github Repo"
  719. src="https://img.shields.io/github/stars/ollama-webui/ollama-webui?style=social&label=Star us on Github"
  720. />
  721. </a>
  722. </div>
  723. </div>
  724. </div>
  725. {/if}
  726. </div>
  727. </div>
  728. </div>
  729. </Modal>
  730. <style>
  731. input::-webkit-outer-spin-button,
  732. input::-webkit-inner-spin-button {
  733. /* display: none; <- Crashes Chrome on hover */
  734. -webkit-appearance: none;
  735. margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
  736. }
  737. .tabs::-webkit-scrollbar {
  738. display: none; /* for Chrome, Safari and Opera */
  739. }
  740. .tabs {
  741. -ms-overflow-style: none; /* IE and Edge */
  742. scrollbar-width: none; /* Firefox */
  743. }
  744. input[type='number'] {
  745. -moz-appearance: textfield; /* Firefox */
  746. }
  747. </style>