Models.svelte 25 KB

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