Models.svelte 31 KB

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