ModelEditor.svelte 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781
  1. <script lang="ts">
  2. import { onMount, getContext, tick } from 'svelte';
  3. import { models, tools, functions, knowledge as knowledgeCollections, user } from '$lib/stores';
  4. import { WEBUI_BASE_URL } from '$lib/constants';
  5. import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte';
  6. import Tags from '$lib/components/common/Tags.svelte';
  7. import Knowledge from '$lib/components/workspace/Models/Knowledge.svelte';
  8. import ToolsSelector from '$lib/components/workspace/Models/ToolsSelector.svelte';
  9. import FiltersSelector from '$lib/components/workspace/Models/FiltersSelector.svelte';
  10. import ActionsSelector from '$lib/components/workspace/Models/ActionsSelector.svelte';
  11. import Capabilities from '$lib/components/workspace/Models/Capabilities.svelte';
  12. import Textarea from '$lib/components/common/Textarea.svelte';
  13. import { getTools } from '$lib/apis/tools';
  14. import { getFunctions } from '$lib/apis/functions';
  15. import { getKnowledgeBases } from '$lib/apis/knowledge';
  16. import AccessControl from '../common/AccessControl.svelte';
  17. import { stringify } from 'postcss';
  18. import { toast } from 'svelte-sonner';
  19. import Spinner from '$lib/components/common/Spinner.svelte';
  20. import XMark from '$lib/components/icons/XMark.svelte';
  21. const i18n = getContext('i18n');
  22. export let onSubmit: Function;
  23. export let onBack: null | Function = null;
  24. export let model = null;
  25. export let edit = false;
  26. export let preset = true;
  27. let loading = false;
  28. let success = false;
  29. let filesInputElement;
  30. let inputFiles;
  31. let showAdvanced = false;
  32. let showPreview = false;
  33. let loaded = false;
  34. // ///////////
  35. // model
  36. // ///////////
  37. let id = '';
  38. let name = '';
  39. let enableDescription = true;
  40. $: if (!edit) {
  41. if (name) {
  42. id = name
  43. .replace(/\s+/g, '-')
  44. .replace(/[^a-zA-Z0-9-]/g, '')
  45. .toLowerCase();
  46. }
  47. }
  48. let system = '';
  49. let info = {
  50. id: '',
  51. base_model_id: null,
  52. name: '',
  53. meta: {
  54. profile_image_url: `${WEBUI_BASE_URL}/static/favicon.png`,
  55. description: '',
  56. suggestion_prompts: null,
  57. tags: []
  58. },
  59. params: {
  60. system: ''
  61. }
  62. };
  63. let params = {
  64. system: ''
  65. };
  66. let capabilities = {
  67. vision: true,
  68. file_upload: true,
  69. web_search: true,
  70. image_generation: true,
  71. code_interpreter: true,
  72. citations: true,
  73. usage: undefined
  74. };
  75. let knowledge = [];
  76. let toolIds = [];
  77. let filterIds = [];
  78. let actionIds = [];
  79. let accessControl = {};
  80. const addUsage = (base_model_id) => {
  81. const baseModel = $models.find((m) => m.id === base_model_id);
  82. if (baseModel) {
  83. if (baseModel.owned_by === 'openai') {
  84. capabilities.usage = baseModel?.meta?.capabilities?.usage ?? false;
  85. } else {
  86. delete capabilities.usage;
  87. }
  88. capabilities = capabilities;
  89. }
  90. };
  91. const submitHandler = async () => {
  92. loading = true;
  93. info.id = id;
  94. info.name = name;
  95. if (id === '') {
  96. toast.error('Model ID is required.');
  97. }
  98. if (name === '') {
  99. toast.error('Model Name is required.');
  100. }
  101. info.params = { ...info.params, ...params };
  102. info.access_control = accessControl;
  103. info.meta.capabilities = capabilities;
  104. if (enableDescription) {
  105. info.meta.description = info.meta.description.trim() === '' ? null : info.meta.description;
  106. } else {
  107. info.meta.description = null;
  108. }
  109. if (knowledge.length > 0) {
  110. info.meta.knowledge = knowledge;
  111. } else {
  112. if (info.meta.knowledge) {
  113. delete info.meta.knowledge;
  114. }
  115. }
  116. if (toolIds.length > 0) {
  117. info.meta.toolIds = toolIds;
  118. } else {
  119. if (info.meta.toolIds) {
  120. delete info.meta.toolIds;
  121. }
  122. }
  123. if (filterIds.length > 0) {
  124. info.meta.filterIds = filterIds;
  125. } else {
  126. if (info.meta.filterIds) {
  127. delete info.meta.filterIds;
  128. }
  129. }
  130. if (actionIds.length > 0) {
  131. info.meta.actionIds = actionIds;
  132. } else {
  133. if (info.meta.actionIds) {
  134. delete info.meta.actionIds;
  135. }
  136. }
  137. info.params.system = system.trim() === '' ? null : system;
  138. info.params.stop = params.stop ? params.stop.split(',').filter((s) => s.trim()) : null;
  139. Object.keys(info.params).forEach((key) => {
  140. if (info.params[key] === '' || info.params[key] === null) {
  141. delete info.params[key];
  142. }
  143. });
  144. await onSubmit(info);
  145. loading = false;
  146. success = false;
  147. };
  148. onMount(async () => {
  149. await tools.set(await getTools(localStorage.token));
  150. await functions.set(await getFunctions(localStorage.token));
  151. await knowledgeCollections.set(await getKnowledgeBases(localStorage.token));
  152. // Scroll to top 'workspace-container' element
  153. const workspaceContainer = document.getElementById('workspace-container');
  154. if (workspaceContainer) {
  155. workspaceContainer.scrollTop = 0;
  156. }
  157. if (model) {
  158. name = model.name;
  159. await tick();
  160. id = model.id;
  161. enableDescription = model?.meta?.description !== null;
  162. if (model.base_model_id) {
  163. const base_model = $models
  164. .filter((m) => !m?.preset && !(m?.arena ?? false))
  165. .find((m) => [model.base_model_id, `${model.base_model_id}:latest`].includes(m.id));
  166. console.log('base_model', base_model);
  167. if (base_model) {
  168. model.base_model_id = base_model.id;
  169. } else {
  170. model.base_model_id = null;
  171. }
  172. }
  173. system = model?.params?.system ?? '';
  174. params = { ...params, ...model?.params };
  175. params.stop = params?.stop
  176. ? (typeof params.stop === 'string' ? params.stop.split(',') : (params?.stop ?? [])).join(
  177. ','
  178. )
  179. : null;
  180. toolIds = model?.meta?.toolIds ?? [];
  181. filterIds = model?.meta?.filterIds ?? [];
  182. actionIds = model?.meta?.actionIds ?? [];
  183. knowledge = (model?.meta?.knowledge ?? []).map((item) => {
  184. if (item?.collection_name) {
  185. return {
  186. id: item.collection_name,
  187. name: item.name,
  188. legacy: true
  189. };
  190. } else if (item?.collection_names) {
  191. return {
  192. name: item.name,
  193. type: 'collection',
  194. collection_names: item.collection_names,
  195. legacy: true
  196. };
  197. } else {
  198. return item;
  199. }
  200. });
  201. capabilities = { ...capabilities, ...(model?.meta?.capabilities ?? {}) };
  202. if ('access_control' in model) {
  203. accessControl = model.access_control;
  204. } else {
  205. accessControl = {};
  206. }
  207. console.log(model?.access_control);
  208. console.log(accessControl);
  209. info = {
  210. ...info,
  211. ...JSON.parse(
  212. JSON.stringify(
  213. model
  214. ? model
  215. : {
  216. id: model.id,
  217. name: model.name
  218. }
  219. )
  220. )
  221. };
  222. console.log(model);
  223. }
  224. loaded = true;
  225. });
  226. </script>
  227. {#if loaded}
  228. {#if onBack}
  229. <button
  230. class="flex space-x-1"
  231. on:click={() => {
  232. onBack();
  233. }}
  234. >
  235. <div class=" self-center">
  236. <svg
  237. xmlns="http://www.w3.org/2000/svg"
  238. viewBox="0 0 20 20"
  239. fill="currentColor"
  240. class="h-4 w-4"
  241. >
  242. <path
  243. fill-rule="evenodd"
  244. d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z"
  245. clip-rule="evenodd"
  246. />
  247. </svg>
  248. </div>
  249. <div class=" self-center text-sm font-medium">{'Back'}</div>
  250. </button>
  251. {/if}
  252. <div class="w-full max-h-full flex justify-center">
  253. <input
  254. bind:this={filesInputElement}
  255. bind:files={inputFiles}
  256. type="file"
  257. hidden
  258. accept="image/*"
  259. on:change={() => {
  260. let reader = new FileReader();
  261. reader.onload = (event) => {
  262. let originalImageUrl = `${event.target.result}`;
  263. const img = new Image();
  264. img.src = originalImageUrl;
  265. img.onload = function () {
  266. const canvas = document.createElement('canvas');
  267. const ctx = canvas.getContext('2d');
  268. // Calculate the aspect ratio of the image
  269. const aspectRatio = img.width / img.height;
  270. // Calculate the new width and height to fit within 100x100
  271. let newWidth, newHeight;
  272. if (aspectRatio > 1) {
  273. newWidth = 250 * aspectRatio;
  274. newHeight = 250;
  275. } else {
  276. newWidth = 250;
  277. newHeight = 250 / aspectRatio;
  278. }
  279. // Set the canvas size
  280. canvas.width = 250;
  281. canvas.height = 250;
  282. // Calculate the position to center the image
  283. const offsetX = (250 - newWidth) / 2;
  284. const offsetY = (250 - newHeight) / 2;
  285. // Draw the image on the canvas
  286. ctx.drawImage(img, offsetX, offsetY, newWidth, newHeight);
  287. // Get the base64 representation of the compressed image
  288. const compressedSrc = canvas.toDataURL();
  289. // Display the compressed image
  290. info.meta.profile_image_url = compressedSrc;
  291. inputFiles = null;
  292. filesInputElement.value = '';
  293. };
  294. };
  295. if (
  296. inputFiles &&
  297. inputFiles.length > 0 &&
  298. ['image/gif', 'image/webp', 'image/jpeg', 'image/png', 'image/svg+xml'].includes(
  299. inputFiles[0]['type']
  300. )
  301. ) {
  302. reader.readAsDataURL(inputFiles[0]);
  303. } else {
  304. console.log(`Unsupported File Type '${inputFiles[0]['type']}'.`);
  305. inputFiles = null;
  306. }
  307. }}
  308. />
  309. {#if !edit || (edit && model)}
  310. <form
  311. class="flex flex-col md:flex-row w-full gap-3 md:gap-6"
  312. on:submit|preventDefault={() => {
  313. submitHandler();
  314. }}
  315. >
  316. <div class="self-center md:self-start flex justify-center my-2 shrink-0">
  317. <div class="self-center">
  318. <button
  319. class="rounded-xl flex shrink-0 items-center {info.meta.profile_image_url !==
  320. `${WEBUI_BASE_URL}/static/favicon.png`
  321. ? 'bg-transparent'
  322. : 'bg-white'} shadow-xl group relative"
  323. type="button"
  324. on:click={() => {
  325. filesInputElement.click();
  326. }}
  327. >
  328. {#if info.meta.profile_image_url}
  329. <img
  330. src={info.meta.profile_image_url}
  331. alt="model profile"
  332. class="rounded-xl size-72 md:size-60 object-cover shrink-0"
  333. />
  334. {:else}
  335. <img
  336. src="{WEBUI_BASE_URL}/static/favicon.png"
  337. alt="model profile"
  338. class=" rounded-xl size-72 md:size-60 object-cover shrink-0"
  339. />
  340. {/if}
  341. <div class="absolute bottom-0 right-0 z-10">
  342. <div class="m-1.5">
  343. <div
  344. class="shadow-xl p-1 rounded-full border-2 border-white bg-gray-800 text-white group-hover:bg-gray-600 transition dark:border-black dark:bg-white dark:group-hover:bg-gray-200 dark:text-black"
  345. >
  346. <svg
  347. xmlns="http://www.w3.org/2000/svg"
  348. viewBox="0 0 16 16"
  349. fill="currentColor"
  350. class="size-5"
  351. >
  352. <path
  353. fill-rule="evenodd"
  354. d="M2 4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4Zm10.5 5.707a.5.5 0 0 0-.146-.353l-1-1a.5.5 0 0 0-.708 0L9.354 9.646a.5.5 0 0 1-.708 0L6.354 7.354a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0-.146.353V12a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5V9.707ZM12 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
  355. clip-rule="evenodd"
  356. />
  357. </svg>
  358. </div>
  359. </div>
  360. </div>
  361. <div
  362. class="absolute top-0 bottom-0 left-0 right-0 bg-white dark:bg-black rounded-lg opacity-0 group-hover:opacity-20 transition"
  363. ></div>
  364. </button>
  365. <div class="flex w-full mt-1 justify-end">
  366. <button
  367. class="px-2 py-1 text-gray-500 rounded-lg text-xs"
  368. on:click={() => {
  369. info.meta.profile_image_url = `${WEBUI_BASE_URL}/static/favicon.png`;
  370. }}
  371. type="button"
  372. >
  373. Reset Image</button
  374. >
  375. </div>
  376. </div>
  377. </div>
  378. <div class="w-full">
  379. <div class="mt-2 my-2 flex flex-col">
  380. <div class="flex-1">
  381. <div>
  382. <input
  383. class="text-3xl font-semibold w-full bg-transparent outline-hidden"
  384. placeholder={$i18n.t('Model Name')}
  385. bind:value={name}
  386. required
  387. />
  388. </div>
  389. </div>
  390. <div class="flex-1">
  391. <div>
  392. <input
  393. class="text-xs w-full bg-transparent text-gray-500 outline-hidden"
  394. placeholder={$i18n.t('Model ID')}
  395. bind:value={id}
  396. disabled={edit}
  397. required
  398. />
  399. </div>
  400. </div>
  401. </div>
  402. {#if preset}
  403. <div class="my-1">
  404. <div class=" text-sm font-semibold mb-1">{$i18n.t('Base Model (From)')}</div>
  405. <div>
  406. <select
  407. class="text-sm w-full bg-transparent outline-hidden"
  408. placeholder="Select a base model (e.g. llama3, gpt-4o)"
  409. bind:value={info.base_model_id}
  410. on:change={(e) => {
  411. addUsage(e.target.value);
  412. }}
  413. required
  414. >
  415. <option value={null} class=" text-gray-900"
  416. >{$i18n.t('Select a base model')}</option
  417. >
  418. {#each $models.filter((m) => (model ? m.id !== model.id : true) && !m?.preset && m?.owned_by !== 'arena') as model}
  419. <option value={model.id} class=" text-gray-900">{model.name}</option>
  420. {/each}
  421. </select>
  422. </div>
  423. </div>
  424. {/if}
  425. <div class="my-1">
  426. <div class="mb-1 flex w-full justify-between items-center">
  427. <div class=" self-center text-sm font-semibold">{$i18n.t('Description')}</div>
  428. <button
  429. class="p-1 text-xs flex rounded-sm transition"
  430. type="button"
  431. aria-pressed={enableDescription ? 'true' : 'false'}
  432. aria-label={enableDescription
  433. ? $i18n.t('Custom description enabled')
  434. : $i18n.t('Default description enabled')}
  435. on:click={() => {
  436. enableDescription = !enableDescription;
  437. }}
  438. >
  439. {#if !enableDescription}
  440. <span class="ml-2 self-center">{$i18n.t('Default')}</span>
  441. {:else}
  442. <span class="ml-2 self-center">{$i18n.t('Custom')}</span>
  443. {/if}
  444. </button>
  445. </div>
  446. {#if enableDescription}
  447. <Textarea
  448. className=" text-sm w-full bg-transparent outline-hidden resize-none overflow-y-hidden "
  449. placeholder={$i18n.t('Add a short description about what this model does')}
  450. bind:value={info.meta.description}
  451. />
  452. {/if}
  453. </div>
  454. <div class=" mt-2 my-1">
  455. <div class="">
  456. <Tags
  457. tags={info?.meta?.tags ?? []}
  458. on:delete={(e) => {
  459. const tagName = e.detail;
  460. info.meta.tags = info.meta.tags.filter((tag) => tag.name !== tagName);
  461. }}
  462. on:add={(e) => {
  463. const tagName = e.detail;
  464. if (!(info?.meta?.tags ?? null)) {
  465. info.meta.tags = [{ name: tagName }];
  466. } else {
  467. info.meta.tags = [...info.meta.tags, { name: tagName }];
  468. }
  469. }}
  470. />
  471. </div>
  472. </div>
  473. <div class="my-2">
  474. <div class="px-3 py-2 bg-gray-50 dark:bg-gray-950 rounded-lg">
  475. <AccessControl
  476. bind:accessControl
  477. accessRoles={['read', 'write']}
  478. allowPublic={$user?.permissions?.sharing?.public_models || $user?.role === 'admin'}
  479. />
  480. </div>
  481. </div>
  482. <hr class=" border-gray-100 dark:border-gray-850 my-1.5" />
  483. <div class="my-2">
  484. <div class="flex w-full justify-between">
  485. <div class=" self-center text-sm font-semibold">{$i18n.t('Model Params')}</div>
  486. </div>
  487. <div class="mt-2">
  488. <div class="my-1">
  489. <div class=" text-xs font-semibold mb-2">{$i18n.t('System Prompt')}</div>
  490. <div>
  491. <Textarea
  492. className=" text-sm w-full bg-transparent outline-hidden resize-none overflow-y-hidden "
  493. placeholder={`Write your model system prompt content here\ne.g.) You are Mario from Super Mario Bros, acting as an assistant.`}
  494. rows={4}
  495. bind:value={system}
  496. />
  497. </div>
  498. </div>
  499. <div class="flex w-full justify-between">
  500. <div class=" self-center text-xs font-semibold">
  501. {$i18n.t('Advanced Params')}
  502. </div>
  503. <button
  504. class="p-1 px-3 text-xs flex rounded-sm transition"
  505. type="button"
  506. on:click={() => {
  507. showAdvanced = !showAdvanced;
  508. }}
  509. >
  510. {#if showAdvanced}
  511. <span class="ml-2 self-center">{$i18n.t('Hide')}</span>
  512. {:else}
  513. <span class="ml-2 self-center">{$i18n.t('Show')}</span>
  514. {/if}
  515. </button>
  516. </div>
  517. {#if showAdvanced}
  518. <div class="my-2">
  519. <AdvancedParams admin={true} custom={true} bind:params />
  520. </div>
  521. {/if}
  522. </div>
  523. </div>
  524. <hr class=" border-gray-100 dark:border-gray-850 my-1" />
  525. <div class="my-2">
  526. <div class="flex w-full justify-between items-center">
  527. <div class="flex w-full justify-between items-center">
  528. <div class=" self-center text-sm font-semibold">
  529. {$i18n.t('Prompt suggestions')}
  530. </div>
  531. <button
  532. class="p-1 text-xs flex rounded-sm transition"
  533. type="button"
  534. on:click={() => {
  535. if ((info?.meta?.suggestion_prompts ?? null) === null) {
  536. info.meta.suggestion_prompts = [{ content: '' }];
  537. } else {
  538. info.meta.suggestion_prompts = null;
  539. }
  540. }}
  541. >
  542. {#if (info?.meta?.suggestion_prompts ?? null) === null}
  543. <span class="ml-2 self-center">{$i18n.t('Default')}</span>
  544. {:else}
  545. <span class="ml-2 self-center">{$i18n.t('Custom')}</span>
  546. {/if}
  547. </button>
  548. </div>
  549. {#if (info?.meta?.suggestion_prompts ?? null) !== null}
  550. <button
  551. class="p-1 px-2 text-xs flex rounded-sm transition"
  552. type="button"
  553. on:click={() => {
  554. if (
  555. info.meta.suggestion_prompts.length === 0 ||
  556. info.meta.suggestion_prompts.at(-1).content !== ''
  557. ) {
  558. info.meta.suggestion_prompts = [
  559. ...info.meta.suggestion_prompts,
  560. { content: '' }
  561. ];
  562. }
  563. }}
  564. >
  565. <svg
  566. xmlns="http://www.w3.org/2000/svg"
  567. viewBox="0 0 20 20"
  568. fill="currentColor"
  569. class="w-4 h-4"
  570. >
  571. <path
  572. d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
  573. />
  574. </svg>
  575. </button>
  576. {/if}
  577. </div>
  578. {#if info?.meta?.suggestion_prompts}
  579. <div class="flex flex-col space-y-1 mt-1 mb-3">
  580. {#if info.meta.suggestion_prompts.length > 0}
  581. {#each info.meta.suggestion_prompts as prompt, promptIdx}
  582. <div class=" flex rounded-lg">
  583. <input
  584. class=" text-sm w-full bg-transparent outline-hidden border-r border-gray-100 dark:border-gray-850"
  585. placeholder={$i18n.t('Write a prompt suggestion (e.g. Who are you?)')}
  586. bind:value={prompt.content}
  587. />
  588. <button
  589. class="px-2"
  590. type="button"
  591. on:click={() => {
  592. info.meta.suggestion_prompts.splice(promptIdx, 1);
  593. info.meta.suggestion_prompts = info.meta.suggestion_prompts;
  594. }}
  595. >
  596. <XMark className={'size-4'} />
  597. </button>
  598. </div>
  599. {/each}
  600. {:else}
  601. <div class="text-xs text-center">No suggestion prompts</div>
  602. {/if}
  603. </div>
  604. {/if}
  605. </div>
  606. <hr class=" border-gray-100 dark:border-gray-850 my-1.5" />
  607. <div class="my-2">
  608. <Knowledge bind:selectedKnowledge={knowledge} collections={$knowledgeCollections} />
  609. </div>
  610. <div class="my-2">
  611. <ToolsSelector bind:selectedToolIds={toolIds} tools={$tools} />
  612. </div>
  613. <div class="my-2">
  614. <FiltersSelector
  615. bind:selectedFilterIds={filterIds}
  616. filters={$functions.filter((func) => func.type === 'filter')}
  617. />
  618. </div>
  619. <div class="my-2">
  620. <ActionsSelector
  621. bind:selectedActionIds={actionIds}
  622. actions={$functions.filter((func) => func.type === 'action')}
  623. />
  624. </div>
  625. <div class="my-2">
  626. <Capabilities bind:capabilities />
  627. </div>
  628. <div class="my-2 text-gray-300 dark:text-gray-700">
  629. <div class="flex w-full justify-between mb-2">
  630. <div class=" self-center text-sm font-semibold">{$i18n.t('JSON Preview')}</div>
  631. <button
  632. class="p-1 px-3 text-xs flex rounded-sm transition"
  633. type="button"
  634. on:click={() => {
  635. showPreview = !showPreview;
  636. }}
  637. >
  638. {#if showPreview}
  639. <span class="ml-2 self-center">{$i18n.t('Hide')}</span>
  640. {:else}
  641. <span class="ml-2 self-center">{$i18n.t('Show')}</span>
  642. {/if}
  643. </button>
  644. </div>
  645. {#if showPreview}
  646. <div>
  647. <textarea
  648. class="text-sm w-full bg-transparent outline-hidden resize-none"
  649. rows="10"
  650. value={JSON.stringify(info, null, 2)}
  651. disabled
  652. readonly
  653. />
  654. </div>
  655. {/if}
  656. </div>
  657. <div class="my-2 flex justify-end pb-20">
  658. <button
  659. class=" text-sm px-3 py-2 transition rounded-lg {loading
  660. ? ' cursor-not-allowed bg-black hover:bg-gray-900 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black'
  661. : 'bg-black hover:bg-gray-900 text-white dark:bg-white dark:hover:bg-gray-100 dark:text-black'} flex w-full justify-center"
  662. type="submit"
  663. disabled={loading}
  664. >
  665. <div class=" self-center font-medium">
  666. {#if edit}
  667. {$i18n.t('Save & Update')}
  668. {:else}
  669. {$i18n.t('Save & Create')}
  670. {/if}
  671. </div>
  672. {#if loading}
  673. <div class="ml-1.5 self-center">
  674. <Spinner />
  675. </div>
  676. {/if}
  677. </button>
  678. </div>
  679. </div>
  680. </form>
  681. {/if}
  682. </div>
  683. {/if}