Models.svelte 27 KB

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