ManageOllama.svelte 29 KB

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