ModelEditor.svelte 23 KB

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