Models.svelte 14 KB

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