Models.svelte 13 KB

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