Models.svelte 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896
  1. <script lang="ts">
  2. import { toast } from 'svelte-sonner';
  3. import {
  4. createModel,
  5. deleteModel,
  6. downloadModel,
  7. getOllamaUrls,
  8. getOllamaVersion,
  9. pullModel,
  10. uploadModel
  11. } from '$lib/apis/ollama';
  12. import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
  13. import { WEBUI_NAME, models, MODEL_DOWNLOAD_POOL, user, config } from '$lib/stores';
  14. import { splitStream } from '$lib/utils';
  15. import { onMount, getContext } from 'svelte';
  16. import Tooltip from '$lib/components/common/Tooltip.svelte';
  17. import Spinner from '$lib/components/common/Spinner.svelte';
  18. const i18n = getContext('i18n');
  19. export let getModels: Function;
  20. let modelUploadInputElement: HTMLInputElement;
  21. // Models
  22. let OLLAMA_URLS = [];
  23. let selectedOllamaUrlIdx: string | null = null;
  24. let updateModelId = null;
  25. let updateProgress = null;
  26. let showExperimentalOllama = false;
  27. let ollamaVersion = null;
  28. const MAX_PARALLEL_DOWNLOADS = 3;
  29. let modelTransferring = false;
  30. let modelTag = '';
  31. let digest = '';
  32. let pullProgress = null;
  33. let modelUploadMode = 'file';
  34. let modelInputFile: File[] | null = null;
  35. let modelFileUrl = '';
  36. let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`;
  37. let modelFileDigest = '';
  38. let uploadProgress = null;
  39. let uploadMessage = '';
  40. let deleteModelTag = '';
  41. const updateModelsHandler = async () => {
  42. for (const model of $models.filter(
  43. (m) =>
  44. !(m?.preset ?? false) &&
  45. m.owned_by === 'ollama' &&
  46. (selectedOllamaUrlIdx === null
  47. ? true
  48. : (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx))
  49. )) {
  50. console.log(model);
  51. updateModelId = model.id;
  52. const [res, controller] = await pullModel(
  53. localStorage.token,
  54. model.id,
  55. selectedOllamaUrlIdx
  56. ).catch((error) => {
  57. toast.error(error);
  58. return null;
  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, '0').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(await getModels(localStorage.token));
  201. } else {
  202. toast.error($i18n.t('Download canceled'));
  203. }
  204. delete $MODEL_DOWNLOAD_POOL[sanitizedModelTag];
  205. MODEL_DOWNLOAD_POOL.set({
  206. ...$MODEL_DOWNLOAD_POOL
  207. });
  208. }
  209. modelTag = '';
  210. modelTransferring = false;
  211. };
  212. const uploadModelHandler = async () => {
  213. modelTransferring = true;
  214. let uploaded = false;
  215. let fileResponse = null;
  216. let name = '';
  217. if (modelUploadMode === 'file') {
  218. const file = modelInputFile ? modelInputFile[0] : null;
  219. if (file) {
  220. uploadMessage = 'Uploading...';
  221. fileResponse = await uploadModel(localStorage.token, file, selectedOllamaUrlIdx).catch(
  222. (error) => {
  223. toast.error(error);
  224. return null;
  225. }
  226. );
  227. }
  228. } else {
  229. uploadProgress = 0;
  230. fileResponse = await downloadModel(
  231. localStorage.token,
  232. modelFileUrl,
  233. selectedOllamaUrlIdx
  234. ).catch((error) => {
  235. toast.error(error);
  236. return null;
  237. });
  238. }
  239. if (fileResponse && fileResponse.ok) {
  240. const reader = fileResponse.body
  241. .pipeThrough(new TextDecoderStream())
  242. .pipeThrough(splitStream('\n'))
  243. .getReader();
  244. while (true) {
  245. const { value, done } = await reader.read();
  246. if (done) break;
  247. try {
  248. let lines = value.split('\n');
  249. for (const line of lines) {
  250. if (line !== '') {
  251. let data = JSON.parse(line.replace(/^data: /, ''));
  252. if (data.progress) {
  253. if (uploadMessage) {
  254. uploadMessage = '';
  255. }
  256. uploadProgress = data.progress;
  257. }
  258. if (data.error) {
  259. throw data.error;
  260. }
  261. if (data.done) {
  262. modelFileDigest = data.blob;
  263. name = data.name;
  264. uploaded = true;
  265. }
  266. }
  267. }
  268. } catch (error) {
  269. console.log(error);
  270. }
  271. }
  272. } else {
  273. const error = await fileResponse?.json();
  274. toast.error(error?.detail ?? error);
  275. }
  276. if (uploaded) {
  277. const res = await createModel(
  278. localStorage.token,
  279. `${name}:latest`,
  280. `FROM @${modelFileDigest}\n${modelFileContent}`
  281. );
  282. if (res && res.ok) {
  283. const reader = res.body
  284. .pipeThrough(new TextDecoderStream())
  285. .pipeThrough(splitStream('\n'))
  286. .getReader();
  287. while (true) {
  288. const { value, done } = await reader.read();
  289. if (done) break;
  290. try {
  291. let lines = value.split('\n');
  292. for (const line of lines) {
  293. if (line !== '') {
  294. console.log(line);
  295. let data = JSON.parse(line);
  296. console.log(data);
  297. if (data.error) {
  298. throw data.error;
  299. }
  300. if (data.detail) {
  301. throw data.detail;
  302. }
  303. if (data.status) {
  304. if (
  305. !data.digest &&
  306. !data.status.includes('writing') &&
  307. !data.status.includes('sha256')
  308. ) {
  309. toast.success(data.status);
  310. } else {
  311. if (data.digest) {
  312. digest = data.digest;
  313. if (data.completed) {
  314. pullProgress = Math.round((data.completed / data.total) * 1000) / 10;
  315. } else {
  316. pullProgress = 100;
  317. }
  318. }
  319. }
  320. }
  321. }
  322. }
  323. } catch (error) {
  324. console.log(error);
  325. toast.error(error);
  326. }
  327. }
  328. }
  329. }
  330. modelFileUrl = '';
  331. if (modelUploadInputElement) {
  332. modelUploadInputElement.value = '';
  333. }
  334. modelInputFile = null;
  335. modelTransferring = false;
  336. uploadProgress = null;
  337. models.set(await getModels());
  338. };
  339. const deleteModelHandler = async () => {
  340. const res = await deleteModel(localStorage.token, deleteModelTag, selectedOllamaUrlIdx).catch(
  341. (error) => {
  342. toast.error(error);
  343. }
  344. );
  345. if (res) {
  346. toast.success($i18n.t(`Deleted {{deleteModelTag}}`, { deleteModelTag }));
  347. }
  348. deleteModelTag = '';
  349. models.set(await getModels());
  350. };
  351. const cancelModelPullHandler = async (model: string) => {
  352. const { reader, abortController } = $MODEL_DOWNLOAD_POOL[model];
  353. if (abortController) {
  354. abortController.abort();
  355. }
  356. if (reader) {
  357. await reader.cancel();
  358. delete $MODEL_DOWNLOAD_POOL[model];
  359. MODEL_DOWNLOAD_POOL.set({
  360. ...$MODEL_DOWNLOAD_POOL
  361. });
  362. await deleteModel(localStorage.token, model);
  363. toast.success(`${model} download has been canceled`);
  364. }
  365. };
  366. onMount(async () => {
  367. await Promise.all([
  368. (async () => {
  369. OLLAMA_URLS = await getOllamaUrls(localStorage.token).catch((error) => {
  370. toast.error(error);
  371. return [];
  372. });
  373. if (OLLAMA_URLS.length > 0) {
  374. selectedOllamaUrlIdx = 0;
  375. }
  376. })(),
  377. (async () => {
  378. ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
  379. })()
  380. ]);
  381. });
  382. </script>
  383. <div class="flex flex-col h-full justify-between text-sm">
  384. <div class=" space-y-3 pr-1.5 overflow-y-scroll h-[24rem]">
  385. {#if ollamaVersion !== null}
  386. <div class="space-y-2 pr-1.5">
  387. <div class="text-sm font-medium">{$i18n.t('Manage Ollama Models')}</div>
  388. {#if OLLAMA_URLS.length > 0}
  389. <div class="flex gap-2">
  390. <div class="flex-1 pb-1">
  391. <select
  392. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  393. bind:value={selectedOllamaUrlIdx}
  394. placeholder={$i18n.t('Select an Ollama instance')}
  395. >
  396. {#each OLLAMA_URLS as url, idx}
  397. <option value={idx} class="bg-gray-100 dark:bg-gray-700">{url}</option>
  398. {/each}
  399. </select>
  400. </div>
  401. <div>
  402. <div class="flex w-full justify-end">
  403. <Tooltip content="Update All Models" placement="top">
  404. <button
  405. class="p-2.5 flex gap-2 items-center bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
  406. on:click={() => {
  407. updateModelsHandler();
  408. }}
  409. >
  410. <svg
  411. xmlns="http://www.w3.org/2000/svg"
  412. viewBox="0 0 16 16"
  413. fill="currentColor"
  414. class="w-4 h-4"
  415. >
  416. <path
  417. 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"
  418. />
  419. <path
  420. 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"
  421. />
  422. </svg>
  423. </button>
  424. </Tooltip>
  425. </div>
  426. </div>
  427. </div>
  428. {#if updateModelId}
  429. Updating "{updateModelId}" {updateProgress ? `(${updateProgress}%)` : ''}
  430. {/if}
  431. {/if}
  432. <div class="space-y-2">
  433. <div>
  434. <div class=" mb-2 text-sm font-medium">{$i18n.t('Pull a model from Ollama.com')}</div>
  435. <div class="flex w-full">
  436. <div class="flex-1 mr-2">
  437. <input
  438. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  439. placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
  440. modelTag: 'mistral:7b'
  441. })}
  442. bind:value={modelTag}
  443. />
  444. </div>
  445. <button
  446. class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
  447. on:click={() => {
  448. pullModelHandler();
  449. }}
  450. disabled={modelTransferring}
  451. >
  452. {#if modelTransferring}
  453. <div class="self-center">
  454. <svg
  455. class=" w-4 h-4"
  456. viewBox="0 0 24 24"
  457. fill="currentColor"
  458. xmlns="http://www.w3.org/2000/svg"
  459. >
  460. <style>
  461. .spinner_ajPY {
  462. transform-origin: center;
  463. animation: spinner_AtaB 0.75s infinite linear;
  464. }
  465. @keyframes spinner_AtaB {
  466. 100% {
  467. transform: rotate(360deg);
  468. }
  469. }
  470. </style>
  471. <path
  472. 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"
  473. opacity=".25"
  474. />
  475. <path
  476. 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"
  477. class="spinner_ajPY"
  478. />
  479. </svg>
  480. </div>
  481. {:else}
  482. <svg
  483. xmlns="http://www.w3.org/2000/svg"
  484. viewBox="0 0 16 16"
  485. fill="currentColor"
  486. class="w-4 h-4"
  487. >
  488. <path
  489. 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"
  490. />
  491. <path
  492. 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"
  493. />
  494. </svg>
  495. {/if}
  496. </button>
  497. </div>
  498. <div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
  499. {$i18n.t('To access the available model names for downloading,')}
  500. <a
  501. class=" text-gray-500 dark:text-gray-300 font-medium underline"
  502. href="https://ollama.com/library"
  503. target="_blank">{$i18n.t('click here.')}</a
  504. >
  505. </div>
  506. {#if Object.keys($MODEL_DOWNLOAD_POOL).length > 0}
  507. {#each Object.keys($MODEL_DOWNLOAD_POOL) as model}
  508. {#if 'pullProgress' in $MODEL_DOWNLOAD_POOL[model]}
  509. <div class="flex flex-col">
  510. <div class="font-medium mb-1">{model}</div>
  511. <div class="">
  512. <div class="flex flex-row justify-between space-x-4 pr-2">
  513. <div class=" flex-1">
  514. <div
  515. class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
  516. style="width: {Math.max(
  517. 15,
  518. $MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0
  519. )}%"
  520. >
  521. {$MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0}%
  522. </div>
  523. </div>
  524. <Tooltip content={$i18n.t('Cancel')}>
  525. <button
  526. class="text-gray-800 dark:text-gray-100"
  527. on:click={() => {
  528. cancelModelPullHandler(model);
  529. }}
  530. >
  531. <svg
  532. class="w-4 h-4 text-gray-800 dark:text-white"
  533. aria-hidden="true"
  534. xmlns="http://www.w3.org/2000/svg"
  535. width="24"
  536. height="24"
  537. fill="currentColor"
  538. viewBox="0 0 24 24"
  539. >
  540. <path
  541. stroke="currentColor"
  542. stroke-linecap="round"
  543. stroke-linejoin="round"
  544. stroke-width="2"
  545. d="M6 18 17.94 6M18 18 6.06 6"
  546. />
  547. </svg>
  548. </button>
  549. </Tooltip>
  550. </div>
  551. {#if 'digest' in $MODEL_DOWNLOAD_POOL[model]}
  552. <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
  553. {$MODEL_DOWNLOAD_POOL[model].digest}
  554. </div>
  555. {/if}
  556. </div>
  557. </div>
  558. {/if}
  559. {/each}
  560. {/if}
  561. </div>
  562. <div>
  563. <div class=" mb-2 text-sm font-medium">{$i18n.t('Delete a model')}</div>
  564. <div class="flex w-full">
  565. <div class="flex-1 mr-2">
  566. <select
  567. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  568. bind:value={deleteModelTag}
  569. placeholder={$i18n.t('Select a model')}
  570. >
  571. {#if !deleteModelTag}
  572. <option value="" disabled selected>{$i18n.t('Select a model')}</option>
  573. {/if}
  574. {#each $models.filter((m) => !(m?.preset ?? false) && m.owned_by === 'ollama' && (selectedOllamaUrlIdx === null ? true : (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
  575. <option value={model.name} class="bg-gray-100 dark:bg-gray-700"
  576. >{model.name +
  577. ' (' +
  578. (model.ollama.size / 1024 ** 3).toFixed(1) +
  579. ' GB)'}</option
  580. >
  581. {/each}
  582. </select>
  583. </div>
  584. <button
  585. class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
  586. on:click={() => {
  587. deleteModelHandler();
  588. }}
  589. >
  590. <svg
  591. xmlns="http://www.w3.org/2000/svg"
  592. viewBox="0 0 16 16"
  593. fill="currentColor"
  594. class="w-4 h-4"
  595. >
  596. <path
  597. fill-rule="evenodd"
  598. 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"
  599. clip-rule="evenodd"
  600. />
  601. </svg>
  602. </button>
  603. </div>
  604. </div>
  605. <div class="pt-1">
  606. <div class="flex justify-between items-center text-xs">
  607. <div class=" text-sm font-medium">{$i18n.t('Experimental')}</div>
  608. <button
  609. class=" text-xs font-medium text-gray-500"
  610. type="button"
  611. on:click={() => {
  612. showExperimentalOllama = !showExperimentalOllama;
  613. }}>{showExperimentalOllama ? $i18n.t('Hide') : $i18n.t('Show')}</button
  614. >
  615. </div>
  616. </div>
  617. {#if showExperimentalOllama}
  618. <form
  619. on:submit|preventDefault={() => {
  620. uploadModelHandler();
  621. }}
  622. >
  623. <div class=" mb-2 flex w-full justify-between">
  624. <div class=" text-sm font-medium">{$i18n.t('Upload a GGUF model')}</div>
  625. <button
  626. class="p-1 px-3 text-xs flex rounded transition"
  627. on:click={() => {
  628. if (modelUploadMode === 'file') {
  629. modelUploadMode = 'url';
  630. } else {
  631. modelUploadMode = 'file';
  632. }
  633. }}
  634. type="button"
  635. >
  636. {#if modelUploadMode === 'file'}
  637. <span class="ml-2 self-center">{$i18n.t('File Mode')}</span>
  638. {:else}
  639. <span class="ml-2 self-center">{$i18n.t('URL Mode')}</span>
  640. {/if}
  641. </button>
  642. </div>
  643. <div class="flex w-full mb-1.5">
  644. <div class="flex flex-col w-full">
  645. {#if modelUploadMode === 'file'}
  646. <div class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}">
  647. <input
  648. id="model-upload-input"
  649. bind:this={modelUploadInputElement}
  650. type="file"
  651. bind:files={modelInputFile}
  652. on:change={() => {
  653. console.log(modelInputFile);
  654. }}
  655. accept=".gguf,.safetensors"
  656. required
  657. hidden
  658. />
  659. <button
  660. type="button"
  661. class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850"
  662. on:click={() => {
  663. modelUploadInputElement.click();
  664. }}
  665. >
  666. {#if modelInputFile && modelInputFile.length > 0}
  667. {modelInputFile[0].name}
  668. {:else}
  669. {$i18n.t('Click here to select')}
  670. {/if}
  671. </button>
  672. </div>
  673. {:else}
  674. <div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
  675. <input
  676. class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
  677. ''
  678. ? 'mr-2'
  679. : ''}"
  680. type="url"
  681. required
  682. bind:value={modelFileUrl}
  683. placeholder={$i18n.t('Type Hugging Face Resolve (Download) URL')}
  684. />
  685. </div>
  686. {/if}
  687. </div>
  688. {#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
  689. <button
  690. class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg disabled:cursor-not-allowed transition"
  691. type="submit"
  692. disabled={modelTransferring}
  693. >
  694. {#if modelTransferring}
  695. <div class="self-center">
  696. <svg
  697. class=" w-4 h-4"
  698. viewBox="0 0 24 24"
  699. fill="currentColor"
  700. xmlns="http://www.w3.org/2000/svg"
  701. >
  702. <style>
  703. .spinner_ajPY {
  704. transform-origin: center;
  705. animation: spinner_AtaB 0.75s infinite linear;
  706. }
  707. @keyframes spinner_AtaB {
  708. 100% {
  709. transform: rotate(360deg);
  710. }
  711. }
  712. </style>
  713. <path
  714. 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"
  715. opacity=".25"
  716. />
  717. <path
  718. 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"
  719. class="spinner_ajPY"
  720. />
  721. </svg>
  722. </div>
  723. {:else}
  724. <svg
  725. xmlns="http://www.w3.org/2000/svg"
  726. viewBox="0 0 16 16"
  727. fill="currentColor"
  728. class="w-4 h-4"
  729. >
  730. <path
  731. 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"
  732. />
  733. <path
  734. 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"
  735. />
  736. </svg>
  737. {/if}
  738. </button>
  739. {/if}
  740. </div>
  741. {#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
  742. <div>
  743. <div>
  744. <div class=" my-2.5 text-sm font-medium">{$i18n.t('Modelfile Content')}</div>
  745. <textarea
  746. bind:value={modelFileContent}
  747. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-100 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none"
  748. rows="6"
  749. />
  750. </div>
  751. </div>
  752. {/if}
  753. <div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
  754. {$i18n.t('To access the GGUF models available for downloading,')}
  755. <a
  756. class=" text-gray-500 dark:text-gray-300 font-medium underline"
  757. href="https://huggingface.co/models?search=gguf"
  758. target="_blank">{$i18n.t('click here.')}</a
  759. >
  760. </div>
  761. {#if uploadMessage}
  762. <div class="mt-2">
  763. <div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
  764. <div class="w-full rounded-full dark:bg-gray-800">
  765. <div
  766. class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
  767. style="width: 100%"
  768. >
  769. {uploadMessage}
  770. </div>
  771. </div>
  772. <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
  773. {modelFileDigest}
  774. </div>
  775. </div>
  776. {:else if uploadProgress !== null}
  777. <div class="mt-2">
  778. <div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div>
  779. <div class="w-full rounded-full dark:bg-gray-800">
  780. <div
  781. class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
  782. style="width: {Math.max(15, uploadProgress ?? 0)}%"
  783. >
  784. {uploadProgress ?? 0}%
  785. </div>
  786. </div>
  787. <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
  788. {modelFileDigest}
  789. </div>
  790. </div>
  791. {/if}
  792. </form>
  793. {/if}
  794. </div>
  795. </div>
  796. {:else if ollamaVersion === false}
  797. <div>Ollama Not Detected</div>
  798. {:else}
  799. <div class="flex h-full justify-center">
  800. <div class="my-auto">
  801. <Spinner className="size-6" />
  802. </div>
  803. </div>
  804. {/if}
  805. </div>
  806. </div>