Models.svelte 14 KB

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