ManageOllama.svelte 29 KB

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