Models.svelte 32 KB

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