AddToolServerModal.svelte 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779
  1. <script lang="ts">
  2. import { v4 as uuidv4 } from 'uuid';
  3. import fileSaver from 'file-saver';
  4. const { saveAs } = fileSaver;
  5. import { toast } from 'svelte-sonner';
  6. import { getContext, onMount } from 'svelte';
  7. const i18n = getContext('i18n');
  8. import { settings } from '$lib/stores';
  9. import Modal from '$lib/components/common/Modal.svelte';
  10. import Plus from '$lib/components/icons/Plus.svelte';
  11. import Minus from '$lib/components/icons/Minus.svelte';
  12. import PencilSolid from '$lib/components/icons/PencilSolid.svelte';
  13. import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
  14. import Tooltip from '$lib/components/common/Tooltip.svelte';
  15. import Switch from '$lib/components/common/Switch.svelte';
  16. import Tags from './common/Tags.svelte';
  17. import { getToolServerData } from '$lib/apis';
  18. import { verifyToolServerConnection, registerOAuthClient } from '$lib/apis/configs';
  19. import AccessControl from './workspace/common/AccessControl.svelte';
  20. import Spinner from '$lib/components/common/Spinner.svelte';
  21. import XMark from '$lib/components/icons/XMark.svelte';
  22. export let onSubmit: Function = () => {};
  23. export let onDelete: Function = () => {};
  24. export let show = false;
  25. export let edit = false;
  26. export let direct = false;
  27. export let connection = null;
  28. let inputElement = null;
  29. let type = 'openapi'; // 'openapi', 'mcp'
  30. let url = '';
  31. let spec_type = 'url'; // 'url', 'json'
  32. let spec = ''; // used when spec_type is 'json'
  33. let path = 'openapi.json';
  34. let auth_type = 'bearer';
  35. let key = '';
  36. let accessControl = {};
  37. let id = '';
  38. let name = '';
  39. let description = '';
  40. let oauthClientInfo = null;
  41. let enable = true;
  42. let loading = false;
  43. const registerOAuthClientHandler = async () => {
  44. if (url === '') {
  45. toast.error($i18n.t('Please enter a valid URL'));
  46. return;
  47. }
  48. if (id === '') {
  49. toast.error($i18n.t('Please enter a valid ID'));
  50. return;
  51. }
  52. const res = await registerOAuthClient(
  53. localStorage.token,
  54. {
  55. url: url,
  56. client_id: id
  57. },
  58. 'mcp'
  59. ).catch((err) => {
  60. toast.error($i18n.t('Registration failed'));
  61. return null;
  62. });
  63. if (res) {
  64. toast.warning(
  65. $i18n.t(
  66. 'Please save the connection to persist the OAuth client information and do not change the ID'
  67. )
  68. );
  69. toast.success($i18n.t('Registration successful'));
  70. console.debug('Registration successful', res);
  71. oauthClientInfo = res?.oauth_client_info ?? null;
  72. }
  73. };
  74. const verifyHandler = async () => {
  75. if (url === '') {
  76. toast.error($i18n.t('Please enter a valid URL'));
  77. return;
  78. }
  79. if (path === '') {
  80. toast.error($i18n.t('Please enter a valid path'));
  81. return;
  82. }
  83. if (direct) {
  84. const res = await getToolServerData(
  85. auth_type === 'bearer' ? key : localStorage.token,
  86. path.includes('://') ? path : `${url}${path.startsWith('/') ? '' : '/'}${path}`
  87. ).catch((err) => {
  88. toast.error($i18n.t('Connection failed'));
  89. });
  90. if (res) {
  91. toast.success($i18n.t('Connection successful'));
  92. console.debug('Connection successful', res);
  93. }
  94. } else {
  95. const res = await verifyToolServerConnection(localStorage.token, {
  96. url,
  97. path,
  98. type,
  99. auth_type,
  100. key,
  101. config: {
  102. enable: enable,
  103. access_control: accessControl
  104. },
  105. info: {
  106. id,
  107. name,
  108. description
  109. }
  110. }).catch((err) => {
  111. toast.error($i18n.t('Connection failed'));
  112. });
  113. if (res) {
  114. toast.success($i18n.t('Connection successful'));
  115. console.debug('Connection successful', res);
  116. }
  117. }
  118. };
  119. const importHandler = async (e) => {
  120. const file = e.target.files[0];
  121. if (!file) return;
  122. const reader = new FileReader();
  123. reader.onload = (event) => {
  124. const json = event.target.result;
  125. console.log('importHandler', json);
  126. try {
  127. const data = JSON.parse(json);
  128. if (data.type) type = data.type;
  129. if (data.url) url = data.url;
  130. if (data.spec_type) spec_type = data.spec_type;
  131. if (data.spec) spec = data.spec;
  132. if (data.path) path = data.path;
  133. if (data.auth_type) auth_type = data.auth_type;
  134. if (data.key) key = data.key;
  135. if (data.info) {
  136. id = data.info.id ?? '';
  137. name = data.info.name ?? '';
  138. description = data.info.description ?? '';
  139. }
  140. if (data.config) {
  141. enable = data.config.enable ?? true;
  142. accessControl = data.config.access_control ?? {};
  143. }
  144. toast.success($i18n.t('Import successful'));
  145. } catch (error) {
  146. toast.error($i18n.t('Please select a valid JSON file'));
  147. }
  148. };
  149. reader.readAsText(file);
  150. };
  151. const exportHandler = async () => {
  152. // export current connection as json file
  153. const json = JSON.stringify({
  154. type,
  155. url,
  156. spec_type,
  157. spec,
  158. path,
  159. auth_type,
  160. key,
  161. info: {
  162. id: id,
  163. name: name,
  164. description: description
  165. }
  166. });
  167. const blob = new Blob([json], {
  168. type: 'application/json'
  169. });
  170. saveAs(blob, `tool-server-${id || name || 'export'}.json`);
  171. };
  172. const submitHandler = async () => {
  173. loading = true;
  174. // remove trailing slash from url
  175. url = url.replace(/\/$/, '');
  176. if (id.includes(':') || id.includes('|')) {
  177. toast.error($i18n.t('ID cannot contain ":" or "|" characters'));
  178. loading = false;
  179. return;
  180. }
  181. if (type === 'mcp' && auth_type === 'oauth_2.1' && !oauthClientInfo) {
  182. toast.error($i18n.t('Please register the OAuth client'));
  183. loading = false;
  184. return;
  185. }
  186. // validate spec
  187. if (spec_type === 'json') {
  188. try {
  189. const specJSON = JSON.parse(spec);
  190. spec = JSON.stringify(specJSON, null, 2);
  191. } catch (e) {
  192. toast.error($i18n.t('Please enter a valid JSON spec'));
  193. loading = false;
  194. return;
  195. }
  196. }
  197. const connection = {
  198. type,
  199. url,
  200. spec_type,
  201. spec,
  202. path,
  203. auth_type,
  204. key,
  205. config: {
  206. enable: enable,
  207. access_control: accessControl
  208. },
  209. info: {
  210. id: id,
  211. name: name,
  212. description: description,
  213. ...(oauthClientInfo ? { oauth_client_info: oauthClientInfo } : {})
  214. }
  215. };
  216. await onSubmit(connection);
  217. loading = false;
  218. show = false;
  219. // reset form
  220. type = 'openapi';
  221. url = '';
  222. spec_type = 'url';
  223. spec = '';
  224. path = 'openapi.json';
  225. key = '';
  226. auth_type = 'bearer';
  227. id = '';
  228. name = '';
  229. description = '';
  230. oauthClientInfo = null;
  231. enable = true;
  232. accessControl = null;
  233. };
  234. const init = () => {
  235. if (connection) {
  236. type = connection?.type ?? 'openapi';
  237. url = connection.url;
  238. spec_type = connection?.spec_type ?? 'url';
  239. spec = connection?.spec ?? '';
  240. path = connection?.path ?? 'openapi.json';
  241. auth_type = connection?.auth_type ?? 'bearer';
  242. key = connection?.key ?? '';
  243. id = connection.info?.id ?? '';
  244. name = connection.info?.name ?? '';
  245. description = connection.info?.description ?? '';
  246. oauthClientInfo = connection.info?.oauth_client_info ?? null;
  247. enable = connection.config?.enable ?? true;
  248. accessControl = connection.config?.access_control ?? null;
  249. }
  250. };
  251. $: if (show) {
  252. init();
  253. }
  254. onMount(() => {
  255. init();
  256. });
  257. </script>
  258. <Modal size="sm" bind:show>
  259. <div>
  260. <div class=" flex justify-between dark:text-gray-100 px-5 pt-4 pb-2">
  261. <h1 class=" text-lg font-medium self-center font-primary">
  262. {#if edit}
  263. {$i18n.t('Edit Connection')}
  264. {:else}
  265. {$i18n.t('Add Connection')}
  266. {/if}
  267. </h1>
  268. <div class="flex items-center gap-3">
  269. <div class="flex gap-1.5 text-xs justify-end">
  270. <button
  271. class=" hover:underline"
  272. type="button"
  273. on:click={() => {
  274. inputElement?.click();
  275. }}
  276. >
  277. {$i18n.t('Import')}
  278. </button>
  279. <button class=" hover:underline" type="button" on:click={exportHandler}>
  280. {$i18n.t('Export')}
  281. </button>
  282. </div>
  283. <button
  284. class="self-center"
  285. aria-label={$i18n.t('Close Configure Connection Modal')}
  286. on:click={() => {
  287. show = false;
  288. }}
  289. >
  290. <XMark className={'size-5'} />
  291. </button>
  292. </div>
  293. </div>
  294. <div class="flex flex-col md:flex-row w-full px-4 pb-4 md:space-x-4 dark:text-gray-200">
  295. <div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
  296. <input
  297. bind:this={inputElement}
  298. type="file"
  299. hidden
  300. accept=".json"
  301. on:change={(e) => {
  302. importHandler(e);
  303. }}
  304. />
  305. <form
  306. class="flex flex-col w-full"
  307. on:submit={(e) => {
  308. e.preventDefault();
  309. submitHandler();
  310. }}
  311. >
  312. <div class="px-1">
  313. {#if !direct}
  314. <div class="flex gap-2 mb-1.5">
  315. <div class="flex w-full justify-between items-center">
  316. <div class=" text-xs text-gray-500">{$i18n.t('Type')}</div>
  317. <div class="">
  318. <button
  319. on:click={() => {
  320. type = ['', 'openapi'].includes(type) ? 'mcp' : 'openapi';
  321. }}
  322. type="button"
  323. class=" text-xs text-gray-700 dark:text-gray-300"
  324. >
  325. {#if ['', 'openapi'].includes(type)}
  326. {$i18n.t('OpenAPI')}
  327. {:else if type === 'mcp'}
  328. {$i18n.t('MCP')}
  329. <span class="text-gray-500">{$i18n.t('Streamable HTTP')}</span>
  330. {/if}
  331. </button>
  332. </div>
  333. </div>
  334. </div>
  335. {/if}
  336. <div class="flex gap-2">
  337. <div class="flex flex-col w-full">
  338. <div class="flex justify-between mb-0.5">
  339. <label
  340. for="api-base-url"
  341. class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
  342. >{$i18n.t('URL')}</label
  343. >
  344. </div>
  345. <div class="flex flex-1 items-center">
  346. <input
  347. id="api-base-url"
  348. class={`w-full flex-1 text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
  349. type="text"
  350. bind:value={url}
  351. placeholder={$i18n.t('API Base URL')}
  352. autocomplete="off"
  353. required
  354. />
  355. <Tooltip
  356. content={$i18n.t('Verify Connection')}
  357. className="shrink-0 flex items-center mr-1"
  358. >
  359. <button
  360. class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
  361. on:click={() => {
  362. verifyHandler();
  363. }}
  364. aria-label={$i18n.t('Verify Connection')}
  365. type="button"
  366. >
  367. <svg
  368. xmlns="http://www.w3.org/2000/svg"
  369. viewBox="0 0 20 20"
  370. fill="currentColor"
  371. class="w-4 h-4"
  372. aria-hidden="true"
  373. >
  374. <path
  375. fill-rule="evenodd"
  376. d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
  377. clip-rule="evenodd"
  378. />
  379. </svg>
  380. </button>
  381. </Tooltip>
  382. <Tooltip content={enable ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
  383. <Switch bind:state={enable} />
  384. </Tooltip>
  385. </div>
  386. </div>
  387. </div>
  388. {#if ['', 'openapi'].includes(type)}
  389. <div class="flex gap-2 mt-2">
  390. <div class="flex flex-col w-full">
  391. <div class="flex justify-between items-center mb-0.5">
  392. <div class="flex gap-2 items-center">
  393. <div
  394. for="select-bearer-or-session"
  395. class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
  396. >
  397. {$i18n.t('OpenAPI Spec')}
  398. </div>
  399. </div>
  400. </div>
  401. <div class="flex gap-2">
  402. <div class="flex-shrink-0 self-start">
  403. <select
  404. id="select-bearer-or-session"
  405. class={`w-full text-sm bg-transparent pr-5 ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
  406. bind:value={spec_type}
  407. >
  408. <option value="url">{$i18n.t('URL')}</option>
  409. <option value="json">{$i18n.t('JSON')}</option>
  410. </select>
  411. </div>
  412. <div class="flex flex-1 items-center">
  413. {#if spec_type === 'url'}
  414. <div class="flex-1 flex items-center">
  415. <label for="url-or-path" class="sr-only"
  416. >{$i18n.t('openapi.json URL or Path')}</label
  417. >
  418. <input
  419. class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
  420. type="text"
  421. id="url-or-path"
  422. bind:value={path}
  423. placeholder={$i18n.t('openapi.json URL or Path')}
  424. autocomplete="off"
  425. required
  426. />
  427. </div>
  428. {:else if spec_type === 'json'}
  429. <div
  430. class={`text-xs w-full self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
  431. >
  432. <label for="url-or-path" class="sr-only">{$i18n.t('JSON Spec')}</label>
  433. <textarea
  434. class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700 text-black dark:text-white'}`}
  435. bind:value={spec}
  436. placeholder={$i18n.t('JSON Spec')}
  437. autocomplete="off"
  438. required
  439. rows="5"
  440. />
  441. </div>
  442. {/if}
  443. </div>
  444. </div>
  445. {#if ['', 'url'].includes(spec_type)}
  446. <div
  447. class={`text-xs mt-1 ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
  448. >
  449. {$i18n.t(`WebUI will make requests to "{{url}}"`, {
  450. url: path.includes('://')
  451. ? path
  452. : `${url}${path.startsWith('/') ? '' : '/'}${path}`
  453. })}
  454. </div>
  455. {/if}
  456. </div>
  457. </div>
  458. {/if}
  459. <div class="flex gap-2 mt-2">
  460. <div class="flex flex-col w-full">
  461. <div class="flex justify-between items-center">
  462. <div class="flex gap-2 items-center">
  463. <div
  464. for="select-bearer-or-session"
  465. class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
  466. >
  467. {$i18n.t('Auth')}
  468. </div>
  469. </div>
  470. {#if auth_type === 'oauth_2.1'}
  471. <div class="flex items-center gap-2">
  472. <div class="flex flex-col justify-end items-center shrink-0">
  473. <Tooltip
  474. content={oauthClientInfo
  475. ? $i18n.t('Register Again')
  476. : $i18n.t('Register Client')}
  477. >
  478. <button
  479. class=" text-xs underline dark:text-gray-500 dark:hover:text-gray-200 text-gray-700 hover:text-gray-900 transition"
  480. type="button"
  481. on:click={() => {
  482. registerOAuthClientHandler();
  483. }}
  484. >
  485. {$i18n.t('Register Client')}
  486. </button>
  487. </Tooltip>
  488. </div>
  489. {#if !oauthClientInfo}
  490. <div
  491. class="text-xs font-medium px-1.5 rounded-md bg-yellow-500/20 text-yellow-700 dark:text-yellow-200"
  492. >
  493. {$i18n.t('Not Registered')}
  494. </div>
  495. {:else}
  496. <div
  497. class="text-xs font-medium px-1.5 rounded-md bg-green-500/20 text-green-700 dark:text-green-200"
  498. >
  499. {$i18n.t('Registered')}
  500. </div>
  501. {/if}
  502. </div>
  503. {/if}
  504. </div>
  505. <div class="flex gap-2">
  506. <div class="flex-shrink-0 self-start">
  507. <select
  508. id="select-bearer-or-session"
  509. class={`w-full text-sm bg-transparent pr-5 ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
  510. bind:value={auth_type}
  511. >
  512. <option value="none">{$i18n.t('None')}</option>
  513. <option value="bearer">{$i18n.t('Bearer')}</option>
  514. <option value="session">{$i18n.t('Session')}</option>
  515. {#if !direct}
  516. <option value="system_oauth">{$i18n.t('OAuth')}</option>
  517. {#if type === 'mcp'}
  518. <option value="oauth_2.1">{$i18n.t('OAuth 2.1')}</option>
  519. {/if}
  520. {/if}
  521. </select>
  522. </div>
  523. <div class="flex flex-1 items-center">
  524. {#if auth_type === 'bearer'}
  525. <SensitiveInput
  526. bind:value={key}
  527. placeholder={$i18n.t('API Key')}
  528. required={false}
  529. />
  530. {:else if auth_type === 'none'}
  531. <div
  532. class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
  533. >
  534. {$i18n.t('No authentication')}
  535. </div>
  536. {:else if auth_type === 'session'}
  537. <div
  538. class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
  539. >
  540. {$i18n.t('Forwards system user session credentials to authenticate')}
  541. </div>
  542. {:else if auth_type === 'system_oauth'}
  543. <div
  544. class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
  545. >
  546. {$i18n.t('Forwards system user OAuth access token to authenticate')}
  547. </div>
  548. {:else if auth_type === 'oauth_2.1'}
  549. <div
  550. class={`flex items-center text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
  551. >
  552. {$i18n.t('Uses OAuth 2.1 Dynamic Client Registration')}
  553. </div>
  554. {/if}
  555. </div>
  556. </div>
  557. </div>
  558. </div>
  559. {#if !direct}
  560. <hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
  561. <div class="flex gap-2">
  562. <div class="flex flex-col w-full">
  563. <label
  564. for="enter-id"
  565. class={`mb-0.5 text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
  566. >{$i18n.t('ID')}
  567. {#if type !== 'mcp'}
  568. <span class="text-xs text-gray-200 dark:text-gray-800 ml-0.5"
  569. >{$i18n.t('Optional')}</span
  570. >
  571. {/if}
  572. </label>
  573. <div class="flex-1">
  574. <input
  575. id="enter-id"
  576. class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
  577. type="text"
  578. bind:value={id}
  579. placeholder={$i18n.t('Enter ID')}
  580. autocomplete="off"
  581. required={type === 'mcp'}
  582. />
  583. </div>
  584. </div>
  585. </div>
  586. <div class="flex gap-2 mt-2">
  587. <div class="flex flex-col w-full">
  588. <label
  589. for="enter-name"
  590. class={`mb-0.5 text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
  591. >{$i18n.t('Name')}
  592. </label>
  593. <div class="flex-1">
  594. <input
  595. id="enter-name"
  596. class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
  597. type="text"
  598. bind:value={name}
  599. placeholder={$i18n.t('Enter name')}
  600. autocomplete="off"
  601. required
  602. />
  603. </div>
  604. </div>
  605. </div>
  606. <div class="flex flex-col w-full mt-2">
  607. <label
  608. for="description"
  609. class={`mb-1 text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100 placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700 text-gray-500'}`}
  610. >{$i18n.t('Description')}</label
  611. >
  612. <div class="flex-1">
  613. <input
  614. id="description"
  615. class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
  616. type="text"
  617. bind:value={description}
  618. placeholder={$i18n.t('Enter description')}
  619. autocomplete="off"
  620. />
  621. </div>
  622. </div>
  623. <hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
  624. <div class="my-2 -mx-2">
  625. <div class="px-4 py-3 bg-gray-50 dark:bg-gray-950 rounded-3xl">
  626. <AccessControl bind:accessControl />
  627. </div>
  628. </div>
  629. {/if}
  630. </div>
  631. {#if type === 'mcp'}
  632. <div
  633. class=" bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-2xl text-xs px-4 py-3 mb-2"
  634. >
  635. <span class="font-medium">
  636. {$i18n.t('Warning')}:
  637. </span>
  638. {$i18n.t(
  639. 'MCP support is experimental and its specification changes often, which can lead to incompatibilities. OpenAPI specification support is directly maintained by the Open WebUI team, making it the more reliable option for compatibility.'
  640. )}
  641. <a
  642. class="font-medium underline"
  643. href="https://docs.openwebui.com/features/mcp"
  644. target="_blank">{$i18n.t('Read more →')}</a
  645. >
  646. </div>
  647. {/if}
  648. <div class="flex justify-between pt-3 text-sm font-medium gap-1.5">
  649. <div></div>
  650. <div class="flex gap-1.5">
  651. {#if edit}
  652. <button
  653. class="px-3.5 py-1.5 text-sm font-medium dark:bg-black dark:hover:bg-gray-900 dark:text-white bg-white text-black hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center"
  654. type="button"
  655. on:click={() => {
  656. onDelete();
  657. show = false;
  658. }}
  659. >
  660. {$i18n.t('Delete')}
  661. </button>
  662. {/if}
  663. <button
  664. class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center {loading
  665. ? ' cursor-not-allowed'
  666. : ''}"
  667. type="submit"
  668. disabled={loading}
  669. >
  670. {$i18n.t('Save')}
  671. {#if loading}
  672. <div class="ml-2 self-center">
  673. <Spinner />
  674. </div>
  675. {/if}
  676. </button>
  677. </div>
  678. </div>
  679. </form>
  680. </div>
  681. </div>
  682. </div>
  683. </Modal>