Models.svelte 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. <script lang="ts">
  2. import { toast } from 'svelte-sonner';
  3. import Sortable from 'sortablejs';
  4. import fileSaver from 'file-saver';
  5. const { saveAs } = fileSaver;
  6. import { onMount, getContext, tick } from 'svelte';
  7. import { WEBUI_NAME, mobile, models, settings, user } from '$lib/stores';
  8. import { addNewModel, deleteModelById, getModelInfos, updateModelById } from '$lib/apis/models';
  9. import { deleteModel } from '$lib/apis/ollama';
  10. import { goto } from '$app/navigation';
  11. import { getModels } from '$lib/apis';
  12. import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
  13. import ModelMenu from './Models/ModelMenu.svelte';
  14. const i18n = getContext('i18n');
  15. let localModelfiles = [];
  16. let importFiles;
  17. let modelsImportInputElement: HTMLInputElement;
  18. let _models = [];
  19. let sortable = null;
  20. let searchValue = '';
  21. const deleteModelHandler = async (model) => {
  22. console.log(model.info);
  23. if (!model?.info) {
  24. toast.error(
  25. $i18n.t('{{ owner }}: You cannot delete a base model', {
  26. owner: model.owned_by.toUpperCase()
  27. })
  28. );
  29. return null;
  30. }
  31. const res = await deleteModelById(localStorage.token, model.id);
  32. if (res) {
  33. toast.success($i18n.t(`Deleted {{name}}`, { name: model.id }));
  34. }
  35. await models.set(await getModels(localStorage.token));
  36. _models = $models;
  37. };
  38. const cloneModelHandler = async (model) => {
  39. if ((model?.info?.base_model_id ?? null) === null) {
  40. toast.error($i18n.t('You cannot clone a base model'));
  41. return;
  42. } else {
  43. sessionStorage.model = JSON.stringify({
  44. ...model,
  45. id: `${model.id}-clone`,
  46. name: `${model.name} (Clone)`
  47. });
  48. goto('/workspace/models/create');
  49. }
  50. };
  51. const shareModelHandler = async (model) => {
  52. toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
  53. const url = 'https://openwebui.com';
  54. const tab = await window.open(`${url}/models/create`, '_blank');
  55. window.addEventListener(
  56. 'message',
  57. (event) => {
  58. if (event.origin !== url) return;
  59. if (event.data === 'loaded') {
  60. tab.postMessage(JSON.stringify(model), '*');
  61. }
  62. },
  63. false
  64. );
  65. };
  66. const hideModelHandler = async (model) => {
  67. let info = model.info;
  68. if (!info) {
  69. info = {
  70. id: model.id,
  71. name: model.name,
  72. meta: {
  73. suggestion_prompts: null
  74. },
  75. params: {}
  76. };
  77. }
  78. info.meta = {
  79. ...info.meta,
  80. hidden: !(info?.meta?.hidden ?? false)
  81. };
  82. console.log(info);
  83. const res = await updateModelById(localStorage.token, info.id, info);
  84. if (res) {
  85. toast.success(
  86. $i18n.t(`Model {{name}} is now {{status}}`, {
  87. name: info.id,
  88. status: info.meta.hidden ? 'hidden' : 'visible'
  89. })
  90. );
  91. }
  92. await models.set(await getModels(localStorage.token));
  93. _models = $models;
  94. };
  95. const downloadModels = async (models) => {
  96. let blob = new Blob([JSON.stringify(models)], {
  97. type: 'application/json'
  98. });
  99. saveAs(blob, `models-export-${Date.now()}.json`);
  100. };
  101. const exportModelHandler = async (model) => {
  102. let blob = new Blob([JSON.stringify([model])], {
  103. type: 'application/json'
  104. });
  105. saveAs(blob, `${model.id}-${Date.now()}.json`);
  106. };
  107. const positionChangeHanlder = async () => {
  108. // Get the new order of the models
  109. const modelIds = Array.from(document.getElementById('model-list').children).map((child) =>
  110. child.id.replace('model-item-', '')
  111. );
  112. // Update the position of the models
  113. for (const [index, id] of modelIds.entries()) {
  114. const model = $models.find((m) => m.id === id);
  115. if (model) {
  116. let info = model.info;
  117. if (!info) {
  118. info = {
  119. id: model.id,
  120. name: model.name,
  121. meta: {
  122. position: index
  123. },
  124. params: {}
  125. };
  126. }
  127. info.meta = {
  128. ...info.meta,
  129. position: index
  130. };
  131. await updateModelById(localStorage.token, info.id, info);
  132. }
  133. }
  134. await tick();
  135. await models.set(await getModels(localStorage.token));
  136. };
  137. onMount(async () => {
  138. // Legacy code to sync localModelfiles with models
  139. _models = $models;
  140. localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]');
  141. if (localModelfiles) {
  142. console.log(localModelfiles);
  143. }
  144. if (!$mobile) {
  145. // SortableJS
  146. sortable = new Sortable(document.getElementById('model-list'), {
  147. animation: 150,
  148. onUpdate: async (event) => {
  149. console.log(event);
  150. positionChangeHanlder();
  151. }
  152. });
  153. }
  154. });
  155. </script>
  156. <svelte:head>
  157. <title>
  158. {$i18n.t('Models')} | {$WEBUI_NAME}
  159. </title>
  160. </svelte:head>
  161. <div class=" text-lg font-semibold mb-3">{$i18n.t('Models')}</div>
  162. <div class=" flex w-full space-x-2">
  163. <div class="flex flex-1">
  164. <div class=" self-center ml-1 mr-3">
  165. <svg
  166. xmlns="http://www.w3.org/2000/svg"
  167. viewBox="0 0 20 20"
  168. fill="currentColor"
  169. class="w-4 h-4"
  170. >
  171. <path
  172. fill-rule="evenodd"
  173. d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
  174. clip-rule="evenodd"
  175. />
  176. </svg>
  177. </div>
  178. <input
  179. class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
  180. bind:value={searchValue}
  181. placeholder={$i18n.t('Search Models')}
  182. />
  183. </div>
  184. <div>
  185. <a
  186. class=" px-2 py-2 rounded-xl border border-gray-200 dark:border-gray-600 dark:border-0 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition font-medium text-sm flex items-center space-x-1"
  187. href="/workspace/models/create"
  188. >
  189. <svg
  190. xmlns="http://www.w3.org/2000/svg"
  191. viewBox="0 0 16 16"
  192. fill="currentColor"
  193. class="w-4 h-4"
  194. >
  195. <path
  196. d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
  197. />
  198. </svg>
  199. </a>
  200. </div>
  201. </div>
  202. <hr class=" dark:border-gray-850 my-2.5" />
  203. <a class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2" href="/workspace/models/create">
  204. <div class=" self-center w-10">
  205. <div
  206. class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
  207. >
  208. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6">
  209. <path
  210. fill-rule="evenodd"
  211. d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
  212. clip-rule="evenodd"
  213. />
  214. </svg>
  215. </div>
  216. </div>
  217. <div class=" self-center">
  218. <div class=" font-bold">{$i18n.t('Create a model')}</div>
  219. <div class=" text-sm">{$i18n.t('Customize models for a specific purpose')}</div>
  220. </div>
  221. </a>
  222. <hr class=" dark:border-gray-850" />
  223. <div class=" my-2 mb-5" id="model-list">
  224. {#each _models.filter((m) => searchValue === '' || m.name
  225. .toLowerCase()
  226. .includes(searchValue.toLowerCase())) as model}
  227. <div
  228. class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
  229. id="model-item-{model.id}"
  230. >
  231. <a
  232. class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
  233. href={`/?models=${encodeURIComponent(model.id)}`}
  234. >
  235. <div class=" self-start w-8 pt-0.5">
  236. <div
  237. class=" rounded-full bg-stone-700 {model?.info?.meta?.hidden ?? false
  238. ? 'brightness-90 dark:brightness-50'
  239. : ''} "
  240. >
  241. <img
  242. src={model?.info?.meta?.profile_image_url ?? '/favicon.png'}
  243. alt="modelfile profile"
  244. class=" rounded-full w-full h-auto object-cover"
  245. />
  246. </div>
  247. </div>
  248. <div
  249. class=" flex-1 self-center {model?.info?.meta?.hidden ?? false ? 'text-gray-500' : ''}"
  250. >
  251. <div class=" font-bold line-clamp-1">{model.name}</div>
  252. <div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
  253. {!!model?.info?.meta?.description ? model?.info?.meta?.description : model.id}
  254. </div>
  255. </div>
  256. </a>
  257. <div class="flex flex-row gap-0.5 self-center">
  258. <a
  259. class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
  260. type="button"
  261. href={`/workspace/models/edit?id=${encodeURIComponent(model.id)}`}
  262. >
  263. <svg
  264. xmlns="http://www.w3.org/2000/svg"
  265. fill="none"
  266. viewBox="0 0 24 24"
  267. stroke-width="1.5"
  268. stroke="currentColor"
  269. class="w-4 h-4"
  270. >
  271. <path
  272. stroke-linecap="round"
  273. stroke-linejoin="round"
  274. d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125"
  275. />
  276. </svg>
  277. </a>
  278. <ModelMenu
  279. {model}
  280. shareHandler={() => {
  281. shareModelHandler(model);
  282. }}
  283. cloneHandler={() => {
  284. cloneModelHandler(model);
  285. }}
  286. exportHandler={() => {
  287. exportModelHandler(model);
  288. }}
  289. hideHandler={() => {
  290. hideModelHandler(model);
  291. }}
  292. deleteHandler={() => {
  293. deleteModelHandler(model);
  294. }}
  295. onClose={() => {}}
  296. >
  297. <button
  298. class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
  299. type="button"
  300. >
  301. <EllipsisHorizontal className="size-5" />
  302. </button>
  303. </ModelMenu>
  304. </div>
  305. </div>
  306. {/each}
  307. </div>
  308. <div class=" flex justify-end w-full mb-3">
  309. <div class="flex space-x-1">
  310. <input
  311. id="models-import-input"
  312. bind:this={modelsImportInputElement}
  313. bind:files={importFiles}
  314. type="file"
  315. accept=".json"
  316. hidden
  317. on:change={() => {
  318. console.log(importFiles);
  319. let reader = new FileReader();
  320. reader.onload = async (event) => {
  321. let savedModels = JSON.parse(event.target.result);
  322. console.log(savedModels);
  323. for (const model of savedModels) {
  324. if (model?.info ?? false) {
  325. if ($models.find((m) => m.id === model.id)) {
  326. await updateModelById(localStorage.token, model.id, model.info).catch((error) => {
  327. return null;
  328. });
  329. } else {
  330. await addNewModel(localStorage.token, model.info).catch((error) => {
  331. return null;
  332. });
  333. }
  334. }
  335. }
  336. await models.set(await getModels(localStorage.token));
  337. };
  338. reader.readAsText(importFiles[0]);
  339. }}
  340. />
  341. <button
  342. class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
  343. on:click={() => {
  344. modelsImportInputElement.click();
  345. }}
  346. >
  347. <div class=" self-center mr-2 font-medium">{$i18n.t('Import Models')}</div>
  348. <div class=" self-center">
  349. <svg
  350. xmlns="http://www.w3.org/2000/svg"
  351. viewBox="0 0 16 16"
  352. fill="currentColor"
  353. class="w-3.5 h-3.5"
  354. >
  355. <path
  356. fill-rule="evenodd"
  357. d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
  358. clip-rule="evenodd"
  359. />
  360. </svg>
  361. </div>
  362. </button>
  363. <button
  364. class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
  365. on:click={async () => {
  366. downloadModels($models);
  367. }}
  368. >
  369. <div class=" self-center mr-2 font-medium">{$i18n.t('Export Models')}</div>
  370. <div class=" self-center">
  371. <svg
  372. xmlns="http://www.w3.org/2000/svg"
  373. viewBox="0 0 16 16"
  374. fill="currentColor"
  375. class="w-3.5 h-3.5"
  376. >
  377. <path
  378. fill-rule="evenodd"
  379. d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
  380. clip-rule="evenodd"
  381. />
  382. </svg>
  383. </div>
  384. </button>
  385. </div>
  386. {#if localModelfiles.length > 0}
  387. <div class="flex">
  388. <div class=" self-center text-sm font-medium mr-4">
  389. {localModelfiles.length} Local Modelfiles Detected
  390. </div>
  391. <div class="flex space-x-1">
  392. <button
  393. class="self-center w-fit text-sm p-1.5 border dark:border-gray-600 rounded-xl flex"
  394. on:click={async () => {
  395. downloadModels(localModelfiles);
  396. localStorage.removeItem('modelfiles');
  397. localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]');
  398. }}
  399. >
  400. <div class=" self-center">
  401. <svg
  402. xmlns="http://www.w3.org/2000/svg"
  403. fill="none"
  404. viewBox="0 0 24 24"
  405. stroke-width="1.5"
  406. stroke="currentColor"
  407. class="w-4 h-4"
  408. >
  409. <path
  410. stroke-linecap="round"
  411. stroke-linejoin="round"
  412. d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
  413. />
  414. </svg>
  415. </div>
  416. </button>
  417. </div>
  418. </div>
  419. {/if}
  420. </div>
  421. <div class=" my-16">
  422. <div class=" text-lg font-semibold mb-3">{$i18n.t('Made by OpenWebUI Community')}</div>
  423. <a
  424. class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
  425. href="https://openwebui.com/"
  426. target="_blank"
  427. >
  428. <div class=" self-center w-10">
  429. <div
  430. class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
  431. >
  432. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6">
  433. <path
  434. fill-rule="evenodd"
  435. d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
  436. clip-rule="evenodd"
  437. />
  438. </svg>
  439. </div>
  440. </div>
  441. <div class=" self-center">
  442. <div class=" font-bold">{$i18n.t('Discover a model')}</div>
  443. <div class=" text-sm">{$i18n.t('Discover, download, and explore model presets')}</div>
  444. </div>
  445. </a>
  446. </div>