Models.svelte 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078
  1. <script lang="ts">
  2. import { toast } from 'svelte-sonner';
  3. import {
  4. createModel,
  5. deleteModel,
  6. downloadModel,
  7. getOllamaUrls,
  8. getOllamaVersion,
  9. pullModel,
  10. uploadModel,
  11. getOllamaConfig
  12. } from '$lib/apis/ollama';
  13. import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
  14. import { WEBUI_NAME, models, MODEL_DOWNLOAD_POOL, user, config } from '$lib/stores';
  15. import { splitStream } from '$lib/utils';
  16. import { onMount, getContext } from 'svelte';
  17. import Tooltip from '$lib/components/common/Tooltip.svelte';
  18. import Spinner from '$lib/components/common/Spinner.svelte';
  19. import { getModels as _getModels } from '$lib/apis';
  20. const i18n = getContext('i18n');
  21. const getModels = async () => {
  22. return await _getModels(localStorage.token);
  23. };
  24. let modelUploadInputElement: HTMLInputElement;
  25. // Models
  26. let ollamaEnabled = null;
  27. let OLLAMA_URLS = [];
  28. let selectedOllamaUrlIdx: string | null = null;
  29. let updateModelId = null;
  30. let updateProgress = null;
  31. let showExperimentalOllama = false;
  32. let ollamaVersion = null;
  33. const MAX_PARALLEL_DOWNLOADS = 3;
  34. let modelTransferring = false;
  35. let modelTag = '';
  36. let createModelLoading = false;
  37. let createModelTag = '';
  38. let createModelContent = '';
  39. let createModelDigest = '';
  40. let createModelPullProgress = null;
  41. let digest = '';
  42. let pullProgress = null;
  43. let modelUploadMode = 'file';
  44. let modelInputFile: File[] | null = null;
  45. let modelFileUrl = '';
  46. let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`;
  47. let modelFileDigest = '';
  48. let uploadProgress = null;
  49. let uploadMessage = '';
  50. let deleteModelTag = '';
  51. const updateModelsHandler = async () => {
  52. for (const model of $models.filter(
  53. (m) =>
  54. !(m?.preset ?? false) &&
  55. m.owned_by === 'ollama' &&
  56. (selectedOllamaUrlIdx === null
  57. ? true
  58. : (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx))
  59. )) {
  60. console.log(model);
  61. updateModelId = model.id;
  62. const [res, controller] = await pullModel(
  63. localStorage.token,
  64. model.id,
  65. selectedOllamaUrlIdx
  66. ).catch((error) => {
  67. toast.error(error);
  68. return null;
  69. });
  70. if (res) {
  71. const reader = res.body
  72. .pipeThrough(new TextDecoderStream())
  73. .pipeThrough(splitStream('\n'))
  74. .getReader();
  75. while (true) {
  76. try {
  77. const { value, done } = await reader.read();
  78. if (done) break;
  79. let lines = value.split('\n');
  80. for (const line of lines) {
  81. if (line !== '') {
  82. let data = JSON.parse(line);
  83. console.log(data);
  84. if (data.error) {
  85. throw data.error;
  86. }
  87. if (data.detail) {
  88. throw data.detail;
  89. }
  90. if (data.status) {
  91. if (data.digest) {
  92. updateProgress = 0;
  93. if (data.completed) {
  94. updateProgress = Math.round((data.completed / data.total) * 1000) / 10;
  95. } else {
  96. updateProgress = 100;
  97. }
  98. } else {
  99. toast.success(data.status);
  100. }
  101. }
  102. }
  103. }
  104. } catch (error) {
  105. console.log(error);
  106. }
  107. }
  108. }
  109. }
  110. updateModelId = null;
  111. updateProgress = null;
  112. };
  113. const pullModelHandler = async () => {
  114. const sanitizedModelTag = modelTag.trim().replace(/^ollama\s+(run|pull)\s+/, '');
  115. console.log($MODEL_DOWNLOAD_POOL);
  116. if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag]) {
  117. toast.error(
  118. $i18n.t(`Model '{{modelTag}}' is already in queue for downloading.`, {
  119. modelTag: sanitizedModelTag
  120. })
  121. );
  122. return;
  123. }
  124. if (Object.keys($MODEL_DOWNLOAD_POOL).length === MAX_PARALLEL_DOWNLOADS) {
  125. toast.error(
  126. $i18n.t('Maximum of 3 models can be downloaded simultaneously. Please try again later.')
  127. );
  128. return;
  129. }
  130. const [res, controller] = await pullModel(localStorage.token, sanitizedModelTag, '0').catch(
  131. (error) => {
  132. toast.error(error);
  133. return null;
  134. }
  135. );
  136. if (res) {
  137. const reader = res.body
  138. .pipeThrough(new TextDecoderStream())
  139. .pipeThrough(splitStream('\n'))
  140. .getReader();
  141. MODEL_DOWNLOAD_POOL.set({
  142. ...$MODEL_DOWNLOAD_POOL,
  143. [sanitizedModelTag]: {
  144. ...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
  145. abortController: controller,
  146. reader,
  147. done: false
  148. }
  149. });
  150. while (true) {
  151. try {
  152. const { value, done } = await reader.read();
  153. if (done) break;
  154. let lines = value.split('\n');
  155. for (const line of lines) {
  156. if (line !== '') {
  157. let data = JSON.parse(line);
  158. console.log(data);
  159. if (data.error) {
  160. throw data.error;
  161. }
  162. if (data.detail) {
  163. throw data.detail;
  164. }
  165. if (data.status) {
  166. if (data.digest) {
  167. let downloadProgress = 0;
  168. if (data.completed) {
  169. downloadProgress = Math.round((data.completed / data.total) * 1000) / 10;
  170. } else {
  171. downloadProgress = 100;
  172. }
  173. MODEL_DOWNLOAD_POOL.set({
  174. ...$MODEL_DOWNLOAD_POOL,
  175. [sanitizedModelTag]: {
  176. ...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
  177. pullProgress: downloadProgress,
  178. digest: data.digest
  179. }
  180. });
  181. } else {
  182. toast.success(data.status);
  183. MODEL_DOWNLOAD_POOL.set({
  184. ...$MODEL_DOWNLOAD_POOL,
  185. [sanitizedModelTag]: {
  186. ...$MODEL_DOWNLOAD_POOL[sanitizedModelTag],
  187. done: data.status === 'success'
  188. }
  189. });
  190. }
  191. }
  192. }
  193. }
  194. } catch (error) {
  195. console.log(error);
  196. if (typeof error !== 'string') {
  197. error = error.message;
  198. }
  199. toast.error(error);
  200. // opts.callback({ success: false, error, modelName: opts.modelName });
  201. }
  202. }
  203. console.log($MODEL_DOWNLOAD_POOL[sanitizedModelTag]);
  204. if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag].done) {
  205. toast.success(
  206. $i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, {
  207. modelName: sanitizedModelTag
  208. })
  209. );
  210. models.set(await getModels());
  211. } else {
  212. toast.error($i18n.t('Download canceled'));
  213. }
  214. delete $MODEL_DOWNLOAD_POOL[sanitizedModelTag];
  215. MODEL_DOWNLOAD_POOL.set({
  216. ...$MODEL_DOWNLOAD_POOL
  217. });
  218. }
  219. modelTag = '';
  220. modelTransferring = false;
  221. };
  222. const uploadModelHandler = async () => {
  223. modelTransferring = true;
  224. let uploaded = false;
  225. let fileResponse = null;
  226. let name = '';
  227. if (modelUploadMode === 'file') {
  228. const file = modelInputFile ? modelInputFile[0] : null;
  229. if (file) {
  230. uploadMessage = 'Uploading...';
  231. fileResponse = await uploadModel(localStorage.token, file, selectedOllamaUrlIdx).catch(
  232. (error) => {
  233. toast.error(error);
  234. return null;
  235. }
  236. );
  237. }
  238. } else {
  239. uploadProgress = 0;
  240. fileResponse = await downloadModel(
  241. localStorage.token,
  242. modelFileUrl,
  243. selectedOllamaUrlIdx
  244. ).catch((error) => {
  245. toast.error(error);
  246. return null;
  247. });
  248. }
  249. if (fileResponse && fileResponse.ok) {
  250. const reader = fileResponse.body
  251. .pipeThrough(new TextDecoderStream())
  252. .pipeThrough(splitStream('\n'))
  253. .getReader();
  254. while (true) {
  255. const { value, done } = await reader.read();
  256. if (done) break;
  257. try {
  258. let lines = value.split('\n');
  259. for (const line of lines) {
  260. if (line !== '') {
  261. let data = JSON.parse(line.replace(/^data: /, ''));
  262. if (data.progress) {
  263. if (uploadMessage) {
  264. uploadMessage = '';
  265. }
  266. uploadProgress = data.progress;
  267. }
  268. if (data.error) {
  269. throw data.error;
  270. }
  271. if (data.done) {
  272. modelFileDigest = data.blob;
  273. name = data.name;
  274. uploaded = true;
  275. }
  276. }
  277. }
  278. } catch (error) {
  279. console.log(error);
  280. }
  281. }
  282. } else {
  283. const error = await fileResponse?.json();
  284. toast.error(error?.detail ?? error);
  285. }
  286. if (uploaded) {
  287. const res = await createModel(
  288. localStorage.token,
  289. `${name}:latest`,
  290. `FROM @${modelFileDigest}\n${modelFileContent}`
  291. );
  292. if (res && res.ok) {
  293. const reader = res.body
  294. .pipeThrough(new TextDecoderStream())
  295. .pipeThrough(splitStream('\n'))
  296. .getReader();
  297. while (true) {
  298. const { value, done } = await reader.read();
  299. if (done) break;
  300. try {
  301. let lines = value.split('\n');
  302. for (const line of lines) {
  303. if (line !== '') {
  304. console.log(line);
  305. let data = JSON.parse(line);
  306. console.log(data);
  307. if (data.error) {
  308. throw data.error;
  309. }
  310. if (data.detail) {
  311. throw data.detail;
  312. }
  313. if (data.status) {
  314. if (
  315. !data.digest &&
  316. !data.status.includes('writing') &&
  317. !data.status.includes('sha256')
  318. ) {
  319. toast.success(data.status);
  320. } else {
  321. if (data.digest) {
  322. digest = data.digest;
  323. if (data.completed) {
  324. pullProgress = Math.round((data.completed / data.total) * 1000) / 10;
  325. } else {
  326. pullProgress = 100;
  327. }
  328. }
  329. }
  330. }
  331. }
  332. }
  333. } catch (error) {
  334. console.log(error);
  335. toast.error(error);
  336. }
  337. }
  338. }
  339. }
  340. modelFileUrl = '';
  341. if (modelUploadInputElement) {
  342. modelUploadInputElement.value = '';
  343. }
  344. modelInputFile = null;
  345. modelTransferring = false;
  346. uploadProgress = null;
  347. models.set(await getModels());
  348. };
  349. const deleteModelHandler = async () => {
  350. const res = await deleteModel(localStorage.token, deleteModelTag, selectedOllamaUrlIdx).catch(
  351. (error) => {
  352. toast.error(error);
  353. }
  354. );
  355. if (res) {
  356. toast.success($i18n.t(`Deleted {{deleteModelTag}}`, { deleteModelTag }));
  357. }
  358. deleteModelTag = '';
  359. models.set(await getModels());
  360. };
  361. const cancelModelPullHandler = async (model: string) => {
  362. const { reader, abortController } = $MODEL_DOWNLOAD_POOL[model];
  363. if (abortController) {
  364. abortController.abort();
  365. }
  366. if (reader) {
  367. await reader.cancel();
  368. delete $MODEL_DOWNLOAD_POOL[model];
  369. MODEL_DOWNLOAD_POOL.set({
  370. ...$MODEL_DOWNLOAD_POOL
  371. });
  372. await deleteModel(localStorage.token, model);
  373. toast.success(`${model} download has been canceled`);
  374. }
  375. };
  376. const createModelHandler = async () => {
  377. createModelLoading = true;
  378. const res = await createModel(
  379. localStorage.token,
  380. createModelTag,
  381. createModelContent,
  382. selectedOllamaUrlIdx
  383. ).catch((error) => {
  384. toast.error(error);
  385. return null;
  386. });
  387. if (res && res.ok) {
  388. const reader = res.body
  389. .pipeThrough(new TextDecoderStream())
  390. .pipeThrough(splitStream('\n'))
  391. .getReader();
  392. while (true) {
  393. const { value, done } = await reader.read();
  394. if (done) break;
  395. try {
  396. let lines = value.split('\n');
  397. for (const line of lines) {
  398. if (line !== '') {
  399. console.log(line);
  400. let data = JSON.parse(line);
  401. console.log(data);
  402. if (data.error) {
  403. throw data.error;
  404. }
  405. if (data.detail) {
  406. throw data.detail;
  407. }
  408. if (data.status) {
  409. if (
  410. !data.digest &&
  411. !data.status.includes('writing') &&
  412. !data.status.includes('sha256')
  413. ) {
  414. toast.success(data.status);
  415. } else {
  416. if (data.digest) {
  417. createModelDigest = data.digest;
  418. if (data.completed) {
  419. createModelPullProgress =
  420. Math.round((data.completed / data.total) * 1000) / 10;
  421. } else {
  422. createModelPullProgress = 100;
  423. }
  424. }
  425. }
  426. }
  427. }
  428. }
  429. } catch (error) {
  430. console.log(error);
  431. toast.error(error);
  432. }
  433. }
  434. }
  435. models.set(await getModels());
  436. createModelLoading = false;
  437. createModelTag = '';
  438. createModelContent = '';
  439. createModelDigest = '';
  440. createModelPullProgress = null;
  441. };
  442. onMount(async () => {
  443. const ollamaConfig = await getOllamaConfig(localStorage.token);
  444. if (ollamaConfig.ENABLE_OLLAMA_API) {
  445. ollamaEnabled = true;
  446. await Promise.all([
  447. (async () => {
  448. OLLAMA_URLS = await getOllamaUrls(localStorage.token).catch((error) => {
  449. toast.error(error);
  450. return [];
  451. });
  452. if (OLLAMA_URLS.length > 0) {
  453. selectedOllamaUrlIdx = 0;
  454. }
  455. })(),
  456. (async () => {
  457. ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
  458. })()
  459. ]);
  460. } else {
  461. ollamaEnabled = false;
  462. toast.error($i18n.t('Ollama API is disabled'));
  463. }
  464. });
  465. </script>
  466. <div class="flex flex-col h-full justify-between text-sm">
  467. <div class=" space-y-3 pr-1.5 overflow-y-scroll scrollbar-hidden h-full">
  468. {#if ollamaEnabled}
  469. {#if ollamaVersion !== null}
  470. <div class="space-y-2 pr-1.5">
  471. <div class="text-sm font-medium">{$i18n.t('Manage Ollama Models')}</div>
  472. {#if OLLAMA_URLS.length > 0}
  473. <div class="flex gap-2">
  474. <div class="flex-1 pb-1">
  475. <select
  476. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  477. bind:value={selectedOllamaUrlIdx}
  478. placeholder={$i18n.t('Select an Ollama instance')}
  479. >
  480. {#each OLLAMA_URLS as url, idx}
  481. <option value={idx} class="bg-gray-100 dark:bg-gray-700">{url}</option>
  482. {/each}
  483. </select>
  484. </div>
  485. <div>
  486. <div class="flex w-full justify-end">
  487. <Tooltip content="Update All Models" placement="top">
  488. <button
  489. class="p-2.5 flex gap-2 items-center bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
  490. on:click={() => {
  491. updateModelsHandler();
  492. }}
  493. >
  494. <svg
  495. xmlns="http://www.w3.org/2000/svg"
  496. viewBox="0 0 16 16"
  497. fill="currentColor"
  498. class="w-4 h-4"
  499. >
  500. <path
  501. d="M7 1a.75.75 0 0 1 .75.75V6h-1.5V1.75A.75.75 0 0 1 7 1ZM6.25 6v2.94L5.03 7.72a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06L7.75 8.94V6H10a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.25Z"
  502. />
  503. <path
  504. d="M4.268 14A2 2 0 0 0 6 15h6a2 2 0 0 0 2-2v-3a2 2 0 0 0-1-1.732V11a3 3 0 0 1-3 3H4.268Z"
  505. />
  506. </svg>
  507. </button>
  508. </Tooltip>
  509. </div>
  510. </div>
  511. </div>
  512. {#if updateModelId}
  513. Updating "{updateModelId}" {updateProgress ? `(${updateProgress}%)` : ''}
  514. {/if}
  515. {/if}
  516. <div class="space-y-2">
  517. <div>
  518. <div class=" mb-2 text-sm font-medium">{$i18n.t('Pull a model from Ollama.com')}</div>
  519. <div class="flex w-full">
  520. <div class="flex-1 mr-2">
  521. <input
  522. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  523. placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
  524. modelTag: 'mistral:7b'
  525. })}
  526. bind:value={modelTag}
  527. />
  528. </div>
  529. <button
  530. class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
  531. on:click={() => {
  532. pullModelHandler();
  533. }}
  534. disabled={modelTransferring}
  535. >
  536. {#if modelTransferring}
  537. <div class="self-center">
  538. <svg
  539. class=" w-4 h-4"
  540. viewBox="0 0 24 24"
  541. fill="currentColor"
  542. xmlns="http://www.w3.org/2000/svg"
  543. >
  544. <style>
  545. .spinner_ajPY {
  546. transform-origin: center;
  547. animation: spinner_AtaB 0.75s infinite linear;
  548. }
  549. @keyframes spinner_AtaB {
  550. 100% {
  551. transform: rotate(360deg);
  552. }
  553. }
  554. </style>
  555. <path
  556. 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"
  557. opacity=".25"
  558. />
  559. <path
  560. 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"
  561. class="spinner_ajPY"
  562. />
  563. </svg>
  564. </div>
  565. {:else}
  566. <svg
  567. xmlns="http://www.w3.org/2000/svg"
  568. viewBox="0 0 16 16"
  569. fill="currentColor"
  570. class="w-4 h-4"
  571. >
  572. <path
  573. 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"
  574. />
  575. <path
  576. 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"
  577. />
  578. </svg>
  579. {/if}
  580. </button>
  581. </div>
  582. <div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
  583. {$i18n.t('To access the available model names for downloading,')}
  584. <a
  585. class=" text-gray-500 dark:text-gray-300 font-medium underline"
  586. href="https://ollama.com/library"
  587. target="_blank">{$i18n.t('click here.')}</a
  588. >
  589. </div>
  590. {#if Object.keys($MODEL_DOWNLOAD_POOL).length > 0}
  591. {#each Object.keys($MODEL_DOWNLOAD_POOL) as model}
  592. {#if 'pullProgress' in $MODEL_DOWNLOAD_POOL[model]}
  593. <div class="flex flex-col">
  594. <div class="font-medium mb-1">{model}</div>
  595. <div class="">
  596. <div class="flex flex-row justify-between space-x-4 pr-2">
  597. <div class=" flex-1">
  598. <div
  599. class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
  600. style="width: {Math.max(
  601. 15,
  602. $MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0
  603. )}%"
  604. >
  605. {$MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0}%
  606. </div>
  607. </div>
  608. <Tooltip content={$i18n.t('Cancel')}>
  609. <button
  610. class="text-gray-800 dark:text-gray-100"
  611. on:click={() => {
  612. cancelModelPullHandler(model);
  613. }}
  614. >
  615. <svg
  616. class="w-4 h-4 text-gray-800 dark:text-white"
  617. aria-hidden="true"
  618. xmlns="http://www.w3.org/2000/svg"
  619. width="24"
  620. height="24"
  621. fill="currentColor"
  622. viewBox="0 0 24 24"
  623. >
  624. <path
  625. stroke="currentColor"
  626. stroke-linecap="round"
  627. stroke-linejoin="round"
  628. stroke-width="2"
  629. d="M6 18 17.94 6M18 18 6.06 6"
  630. />
  631. </svg>
  632. </button>
  633. </Tooltip>
  634. </div>
  635. {#if 'digest' in $MODEL_DOWNLOAD_POOL[model]}
  636. <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
  637. {$MODEL_DOWNLOAD_POOL[model].digest}
  638. </div>
  639. {/if}
  640. </div>
  641. </div>
  642. {/if}
  643. {/each}
  644. {/if}
  645. </div>
  646. <div>
  647. <div class=" mb-2 text-sm font-medium">{$i18n.t('Delete a model')}</div>
  648. <div class="flex w-full">
  649. <div class="flex-1 mr-2">
  650. <select
  651. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  652. bind:value={deleteModelTag}
  653. placeholder={$i18n.t('Select a model')}
  654. >
  655. {#if !deleteModelTag}
  656. <option value="" disabled selected>{$i18n.t('Select a model')}</option>
  657. {/if}
  658. {#each $models.filter((m) => !(m?.preset ?? false) && m.owned_by === 'ollama' && (selectedOllamaUrlIdx === null ? true : (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
  659. <option value={model.name} class="bg-gray-100 dark:bg-gray-700"
  660. >{model.name +
  661. ' (' +
  662. (model.ollama.size / 1024 ** 3).toFixed(1) +
  663. ' GB)'}</option
  664. >
  665. {/each}
  666. </select>
  667. </div>
  668. <button
  669. class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
  670. on:click={() => {
  671. deleteModelHandler();
  672. }}
  673. >
  674. <svg
  675. xmlns="http://www.w3.org/2000/svg"
  676. viewBox="0 0 16 16"
  677. fill="currentColor"
  678. class="w-4 h-4"
  679. >
  680. <path
  681. fill-rule="evenodd"
  682. 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"
  683. clip-rule="evenodd"
  684. />
  685. </svg>
  686. </button>
  687. </div>
  688. </div>
  689. <div>
  690. <div class=" mb-2 text-sm font-medium">{$i18n.t('Create a model')}</div>
  691. <div class="flex w-full">
  692. <div class="flex-1 mr-2 flex flex-col gap-2">
  693. <input
  694. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  695. placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
  696. modelTag: 'my-modelfile'
  697. })}
  698. bind:value={createModelTag}
  699. disabled={createModelLoading}
  700. />
  701. <textarea
  702. bind:value={createModelContent}
  703. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-100 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none scrollbar-hidden"
  704. rows="6"
  705. placeholder={`TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`}
  706. disabled={createModelLoading}
  707. />
  708. </div>
  709. <div class="flex self-start">
  710. <button
  711. class="px-2.5 py-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition disabled:cursor-not-allowed"
  712. on:click={() => {
  713. createModelHandler();
  714. }}
  715. disabled={createModelLoading}
  716. >
  717. <svg
  718. xmlns="http://www.w3.org/2000/svg"
  719. viewBox="0 0 16 16"
  720. fill="currentColor"
  721. class="size-4"
  722. >
  723. <path
  724. 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"
  725. />
  726. <path
  727. 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"
  728. />
  729. </svg>
  730. </button>
  731. </div>
  732. </div>
  733. {#if createModelDigest !== ''}
  734. <div class="flex flex-col mt-1">
  735. <div class="font-medium mb-1">{createModelTag}</div>
  736. <div class="">
  737. <div class="flex flex-row justify-between space-x-4 pr-2">
  738. <div class=" flex-1">
  739. <div
  740. class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
  741. style="width: {Math.max(15, createModelPullProgress ?? 0)}%"
  742. >
  743. {createModelPullProgress ?? 0}%
  744. </div>
  745. </div>
  746. </div>
  747. {#if createModelDigest}
  748. <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
  749. {createModelDigest}
  750. </div>
  751. {/if}
  752. </div>
  753. </div>
  754. {/if}
  755. </div>
  756. <div class="pt-1">
  757. <div class="flex justify-between items-center text-xs">
  758. <div class=" text-sm font-medium">{$i18n.t('Experimental')}</div>
  759. <button
  760. class=" text-xs font-medium text-gray-500"
  761. type="button"
  762. on:click={() => {
  763. showExperimentalOllama = !showExperimentalOllama;
  764. }}>{showExperimentalOllama ? $i18n.t('Hide') : $i18n.t('Show')}</button
  765. >
  766. </div>
  767. </div>
  768. {#if showExperimentalOllama}
  769. <form
  770. on:submit|preventDefault={() => {
  771. uploadModelHandler();
  772. }}
  773. >
  774. <div class=" mb-2 flex w-full justify-between">
  775. <div class=" text-sm font-medium">{$i18n.t('Upload a GGUF model')}</div>
  776. <button
  777. class="p-1 px-3 text-xs flex rounded transition"
  778. on:click={() => {
  779. if (modelUploadMode === 'file') {
  780. modelUploadMode = 'url';
  781. } else {
  782. modelUploadMode = 'file';
  783. }
  784. }}
  785. type="button"
  786. >
  787. {#if modelUploadMode === 'file'}
  788. <span class="ml-2 self-center">{$i18n.t('File Mode')}</span>
  789. {:else}
  790. <span class="ml-2 self-center">{$i18n.t('URL Mode')}</span>
  791. {/if}
  792. </button>
  793. </div>
  794. <div class="flex w-full mb-1.5">
  795. <div class="flex flex-col w-full">
  796. {#if modelUploadMode === 'file'}
  797. <div
  798. class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}"
  799. >
  800. <input
  801. id="model-upload-input"
  802. bind:this={modelUploadInputElement}
  803. type="file"
  804. bind:files={modelInputFile}
  805. on:change={() => {
  806. console.log(modelInputFile);
  807. }}
  808. accept=".gguf,.safetensors"
  809. required
  810. hidden
  811. />
  812. <button
  813. type="button"
  814. class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850"
  815. on:click={() => {
  816. modelUploadInputElement.click();
  817. }}
  818. >
  819. {#if modelInputFile && modelInputFile.length > 0}
  820. {modelInputFile[0].name}
  821. {:else}
  822. {$i18n.t('Click here to select')}
  823. {/if}
  824. </button>
  825. </div>
  826. {:else}
  827. <div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
  828. <input
  829. class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
  830. ''
  831. ? 'mr-2'
  832. : ''}"
  833. type="url"
  834. required
  835. bind:value={modelFileUrl}
  836. placeholder={$i18n.t('Type Hugging Face Resolve (Download) URL')}
  837. />
  838. </div>
  839. {/if}
  840. </div>
  841. {#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
  842. <button
  843. class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg disabled:cursor-not-allowed transition"
  844. type="submit"
  845. disabled={modelTransferring}
  846. >
  847. {#if modelTransferring}
  848. <div class="self-center">
  849. <svg
  850. class=" w-4 h-4"
  851. viewBox="0 0 24 24"
  852. fill="currentColor"
  853. xmlns="http://www.w3.org/2000/svg"
  854. >
  855. <style>
  856. .spinner_ajPY {
  857. transform-origin: center;
  858. animation: spinner_AtaB 0.75s infinite linear;
  859. }
  860. @keyframes spinner_AtaB {
  861. 100% {
  862. transform: rotate(360deg);
  863. }
  864. }
  865. </style>
  866. <path
  867. 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"
  868. opacity=".25"
  869. />
  870. <path
  871. 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"
  872. class="spinner_ajPY"
  873. />
  874. </svg>
  875. </div>
  876. {:else}
  877. <svg
  878. xmlns="http://www.w3.org/2000/svg"
  879. viewBox="0 0 16 16"
  880. fill="currentColor"
  881. class="w-4 h-4"
  882. >
  883. <path
  884. 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"
  885. />
  886. <path
  887. 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"
  888. />
  889. </svg>
  890. {/if}
  891. </button>
  892. {/if}
  893. </div>
  894. {#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
  895. <div>
  896. <div>
  897. <div class=" my-2.5 text-sm font-medium">{$i18n.t('Modelfile Content')}</div>
  898. <textarea
  899. bind:value={modelFileContent}
  900. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-100 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none"
  901. rows="6"
  902. />
  903. </div>
  904. </div>
  905. {/if}
  906. <div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
  907. {$i18n.t('To access the GGUF models available for downloading,')}
  908. <a
  909. class=" text-gray-500 dark:text-gray-300 font-medium underline"
  910. href="https://huggingface.co/models?search=gguf"
  911. target="_blank">{$i18n.t('click here.')}</a
  912. >
  913. </div>
  914. {#if uploadMessage}
  915. <div class="mt-2">
  916. <div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
  917. <div class="w-full rounded-full dark:bg-gray-800">
  918. <div
  919. class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
  920. style="width: 100%"
  921. >
  922. {uploadMessage}
  923. </div>
  924. </div>
  925. <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
  926. {modelFileDigest}
  927. </div>
  928. </div>
  929. {:else if uploadProgress !== null}
  930. <div class="mt-2">
  931. <div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
  932. <div class="w-full rounded-full dark:bg-gray-800">
  933. <div
  934. class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
  935. style="width: {Math.max(15, uploadProgress ?? 0)}%"
  936. >
  937. {uploadProgress ?? 0}%
  938. </div>
  939. </div>
  940. <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
  941. {modelFileDigest}
  942. </div>
  943. </div>
  944. {/if}
  945. </form>
  946. {/if}
  947. </div>
  948. </div>
  949. {:else if ollamaVersion === false}
  950. <div>Ollama Not Detected</div>
  951. {:else}
  952. <div class="flex h-full justify-center">
  953. <div class="my-auto">
  954. <Spinner className="size-6" />
  955. </div>
  956. </div>
  957. {/if}
  958. {:else if ollamaEnabled === false}
  959. <div>{$i18n.t('Ollama API is disabled')}</div>
  960. {:else}
  961. <div class="flex h-full justify-center">
  962. <div class="my-auto">
  963. <Spinner className="size-6" />
  964. </div>
  965. </div>
  966. {/if}
  967. </div>
  968. </div>