open-webui/src/lib/components/AddToolServerModal.svelte

798 lines
23 KiB
Svelte
Raw Normal View History

<script lang="ts">
2025-09-27 03:53:02 +00:00
import { v4 as uuidv4 } from 'uuid';
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { toast } from 'svelte-sonner';
import { getContext, onMount } from 'svelte';
const i18n = getContext('i18n');
import { settings } from '$lib/stores';
import Modal from '$lib/components/common/Modal.svelte';
import Plus from '$lib/components/icons/Plus.svelte';
import Minus from '$lib/components/icons/Minus.svelte';
import PencilSolid from '$lib/components/icons/PencilSolid.svelte';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Switch from '$lib/components/common/Switch.svelte';
import Tags from './common/Tags.svelte';
2025-04-05 10:05:52 +00:00
import { getToolServerData } from '$lib/apis';
import { verifyToolServerConnection, registerOAuthClient } from '$lib/apis/configs';
2025-04-05 10:40:01 +00:00
import AccessControl from './workspace/common/AccessControl.svelte';
2025-06-25 22:44:45 +00:00
import Spinner from '$lib/components/common/Spinner.svelte';
import XMark from '$lib/components/icons/XMark.svelte';
export let onSubmit: Function = () => {};
export let onDelete: Function = () => {};
export let show = false;
export let edit = false;
2025-04-05 10:05:52 +00:00
export let direct = false;
export let connection = null;
2025-09-27 03:53:02 +00:00
let inputElement = null;
let type = 'openapi'; // 'openapi', 'mcp'
let url = '';
let spec_type = 'url'; // 'url', 'json'
let spec = ''; // used when spec_type is 'json'
let path = 'openapi.json';
2025-09-21 07:12:24 +00:00
let auth_type = 'bearer';
let key = '';
let accessControl = {};
2025-04-05 10:40:01 +00:00
let id = '';
let name = '';
let description = '';
let oauthClientInfo = null;
let enable = true;
let loading = false;
const registerOAuthClientHandler = async () => {
if (url === '') {
toast.error($i18n.t('Please enter a valid URL'));
return;
}
if (id === '') {
toast.error($i18n.t('Please enter a valid ID'));
return;
}
2025-09-25 06:49:16 +00:00
const res = await registerOAuthClient(
localStorage.token,
{
url: url,
client_id: id
},
'mcp'
).catch((err) => {
toast.error($i18n.t('Registration failed'));
return null;
});
if (res) {
2025-09-25 06:49:16 +00:00
toast.warning(
$i18n.t(
'Please save the connection to persist the OAuth client information and do not change the ID'
)
);
toast.success($i18n.t('Registration successful'));
2025-09-25 06:49:16 +00:00
console.debug('Registration successful', res);
oauthClientInfo = res?.oauth_client_info ?? null;
}
};
2025-04-05 10:05:52 +00:00
const verifyHandler = async () => {
if (url === '') {
toast.error($i18n.t('Please enter a valid URL'));
return;
}
0.6.33 (#18118) * feat: improve ollama model management experience This commit introduces several improvements to the Ollama model management modal: - Adds a cancel button to the model pulling operation, using the existing 'x' button pattern. - Adds a cancel button to the "Update All" models operation, allowing the user to cancel the update for the currently processing model. - Cleans up toast notifications when updating all models. A single toast is now shown at the beginning and a summary toast at the end, preventing notification spam. - Refactors the `ManageOllama.svelte` component to support these new cancellation features. - Adds tooltips to all buttons in the modal to improve clarity. - Disables buttons when their corresponding input fields are empty to prevent accidental clicks. * fix * i18n: improve Chinese translation * fix: handle non‑UTF8 chars in third‑party responses without error * German translation of new strings in i18n * log web search queries only with level 'debug' instead of 'info' * Tool calls now only include text and dont inlcude other content like image b64 * fix onedrive * fix: discovery url * fix: default permissions not being loaded * fix: ai hallucination * fix: non rich text input copy * refac: rm print statements * refac: disable direct models from model editors * refac/fix: do not process xlsx files with azure doc intelligence * Update pull_request_template.md * Update generated image translation in DE-de * added missing danish translations * feat(onedrive): Enable search and "My Organization" pivot * style(onedrive): Formatting fix * feat: Implement toggling for vertical and horizontal flow layouts This commit introduces the necessary logic and UI controls to allow users to switch the Flow component layout between vertical and horizontal orientations. * **`Flow.svelte` Refactoring:** * Updates logic for calculating level offsets and node positions to consistently respect the current flow orientation. * Adds a control panel using `<Controls>` and `<SwitchButton>` components. * Provides user interface elements to easily switch the flow layout between horizontal and vertical orientations. * build(deps): bump pydantic from 2.11.7 to 2.11.9 in /backend Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.11.7 to 2.11.9. - [Release notes](https://github.com/pydantic/pydantic/releases) - [Changelog](https://github.com/pydantic/pydantic/blob/v2.11.9/HISTORY.md) - [Commits](https://github.com/pydantic/pydantic/compare/v2.11.7...v2.11.9) --- updated-dependencies: - dependency-name: pydantic dependency-version: 2.11.9 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> * build(deps): bump black from 25.1.0 to 25.9.0 in /backend Bumps [black](https://github.com/psf/black) from 25.1.0 to 25.9.0. - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/compare/25.1.0...25.9.0) --- updated-dependencies: - dependency-name: black dependency-version: 25.9.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * build(deps): bump markdown from 3.8.2 to 3.9 in /backend Bumps [markdown](https://github.com/Python-Markdown/markdown) from 3.8.2 to 3.9. - [Release notes](https://github.com/Python-Markdown/markdown/releases) - [Changelog](https://github.com/Python-Markdown/markdown/blob/master/docs/changelog.md) - [Commits](https://github.com/Python-Markdown/markdown/compare/3.8.2...3.9.0) --- updated-dependencies: - dependency-name: markdown dependency-version: '3.9' dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * build(deps): bump chromadb from 1.0.20 to 1.1.0 in /backend Bumps [chromadb](https://github.com/chroma-core/chroma) from 1.0.20 to 1.1.0. - [Release notes](https://github.com/chroma-core/chroma/releases) - [Changelog](https://github.com/chroma-core/chroma/blob/main/RELEASE_PROCESS.md) - [Commits](https://github.com/chroma-core/chroma/compare/1.0.20...1.1.0) --- updated-dependencies: - dependency-name: chromadb dependency-version: 1.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * build(deps): bump opentelemetry-api from 1.36.0 to 1.37.0 Bumps [opentelemetry-api](https://github.com/open-telemetry/opentelemetry-python) from 1.36.0 to 1.37.0. - [Release notes](https://github.com/open-telemetry/opentelemetry-python/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-python/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-python/compare/v1.36.0...v1.37.0) --- updated-dependencies: - dependency-name: opentelemetry-api dependency-version: 1.37.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * refac: ollama embed form data * fix: non rich text handling * fix: oauth client registration * refac * chore: dep bump * chore: fastapi bump * chore/refac: bump bcrypt and remove passlib * Improving Korean Translation * refac * Improving Korean Translation * feat: PWA share_target implementation Co-Authored-By: gjveld <19951982+gjveld@users.noreply.github.com> * refac: message input mobile detection behaviour * feat: model_ids per folder * Update translation.json (pt-BR) inclusion of new translations of items that have been added * refac * refac * refac * refac * refac/fix: temp chat * refac * refac: stop task * refac/fix: azure audio escape * refac: external tool validation * refac/enh: start.sh additional args support * refac * refac: styling * refac/fix: direct connection floating action buttons * refac/fix: system prompt duplication * refac/enh: openai tts additional params support * refac * feat: load data in parallel to accelerate page loading speed * i18n: improve Chinese translation * refac * refac: model selector * UPD: i18n es-ES Translation v0.6.33 UPD: i18n es-ES Translation v0.6.33 Updated new strings. * refac * improved query pref by querying only relevant columns * refac/enh: docling params * refac * refac: openai additional headers support * refac * FEAT: Add Vega Char Visualizer Renderer ### FEAT: Add Vega Char Visualizer Renderer Feature required in https://github.com/open-webui/open-webui/discussions/18022 Added npm vega lib to package.json Added function for visualization renderer to src/libs/utils/index.ts Added logic to src/lib/components/chat/Messages/CodeBlock.svelte The treatment is similar as for mermaid diagrams. Reference: https://vega.github.io/vega/ * refac * chore * refac * FEAT: Add Vega-Lite Char Visualizer Renderer ### FEAT: Add Vega Char Visualizer Renderer Add suport for Vega-Lite Specifications. Vega-Lite is a "compiled" version of Vega Char Visualizer. For be rendered with Vega it have to be compiled. This PR add the check and compile if necessary, is a complement of recent Vega Renderer Feature added. * refac * refac/fix: switch * enh/refac: url input handling * refac * refac: styling * UPD: Add Validators & Error Toast for Mermaid & Vega diagrams ### UPD: Feat: Add Validators & Error Toast for Mermaid & Vega diagrams Description: As many time the diagrams generated or entered have syntax errors the diagrams are not rendered due to that errors, but as there isn't any notification is difficult to know what happend. This PR add validator and toast notification when error on Mermaid and Vega/Vega-Lite diagrams, helping the user to fix its. * removed redundant knowledge API call * Fix Code Format * refac: model workspace view * refac * refac: knowledge * refac: prompts * refac: tools * refac * feat: attach folder * refac: make tencentcloud-sdk-python optional * refac/fix: oauth * enh: ENABLE_OAUTH_EMAIL_FALLBACK * refac/fix: folders * Update requirements.txt * Update pyproject.toml * UPD: Add Validators & Error Toast for Mermaid & Vega diagrams ### UPD: Feat: Add Validators & Error Toast for Mermaid & Vega diagrams Description: As many time the diagrams generated or entered have syntax errors the diagrams are not rendered due to that errors, but as there isn't any notification is difficult to know what happend. This PR add validator and toast notification when error on Mermaid and Vega/Vega-Lite diagrams, helping the user to fix its. Note: Another possibility of integrating this Graph Visualizer is through its svelte component: https://github.com/vega/svelte-vega/tree/main/packages/svelte-vega * Removed unused toast import & Code Format * refac * refac: external tool server view * refac * refac: overview * refac: styling * refac * Update bug_report.yaml * refac * refac * refac * refac * refac: oauth client fallback * Fixed: Cannot handle batch sizes > 1 if no padding token is defined Fixes Cannot handle batch sizes > 1 if no padding token is defined For reranker models that do not have this defined in their config by using the eos_token_id if present as pad_token_id. * refac: fallback to reasoning content * fix(i18n): corrected typo in Spanish translation for "Reasoning Tags" Typo fixed in Spanish translation file at line 1240 of `open-webui/src/lib/i18n/locales/es-ES/translation.json`: - Incorrect: "Eriquetas de Razonamiento" - Correct: "Etiquetas de Razonamiento" This improves clarity and consistency in the UI. * refac/fix: ENABLE_STAR_SESSIONS_MIDDLEWARE * refac/fix: redirect * refac * refac * refac * refac: web search error handling * refac: source parsing * refac: functions * refac * refac/enh: note pdf export * refac/fix: mcp oauth2.1 * chore: format * chore: Changelog (#17995) * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * refac * chore: dep bump --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: silentoplayz <jacwoo21@outlook.com> Co-authored-by: Shirasawa <764798966@qq.com> Co-authored-by: Jan Kessler <jakessle@uni-mainz.de> Co-authored-by: Jacob Leksan <jacob.leksan@expedient.com> Co-authored-by: Classic298 <27028174+Classic298@users.noreply.github.com> Co-authored-by: sinejespersen <sinejespersen@protonmail.com> Co-authored-by: Selene Blok <selene.blok@rws.nl> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Cyp <cypher9715@naver.com> Co-authored-by: gjveld <19951982+gjveld@users.noreply.github.com> Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com> Co-authored-by: _00_ <131402327+rgaricano@users.noreply.github.com> Co-authored-by: expruc <eygabi01@gmail.com> Co-authored-by: YetheSamartaka <55753928+YetheSamartaka@users.noreply.github.com> Co-authored-by: Akutangulo <akutangulo@gmail.com>
2025-10-07 21:20:27 +00:00
if (['openapi', ''].includes(type)) {
if (spec_type === 'json' && spec === '') {
toast.error($i18n.t('Please enter a valid JSON spec'));
return;
}
if (spec_type === 'url' && path === '') {
toast.error($i18n.t('Please enter a valid path'));
return;
}
2025-04-05 10:05:52 +00:00
}
if (direct) {
const res = await getToolServerData(
auth_type === 'bearer' ? key : localStorage.token,
2025-05-26 20:20:47 +00:00
path.includes('://') ? path : `${url}${path.startsWith('/') ? '' : '/'}${path}`
2025-04-05 10:05:52 +00:00
).catch((err) => {
toast.error($i18n.t('Connection failed'));
});
if (res) {
toast.success($i18n.t('Connection successful'));
console.debug('Connection successful', res);
}
} else {
const res = await verifyToolServerConnection(localStorage.token, {
url,
path,
2025-09-22 18:46:47 +00:00
type,
2025-04-05 10:05:52 +00:00
auth_type,
key,
config: {
2025-04-05 10:40:01 +00:00
enable: enable,
access_control: accessControl
},
info: {
id,
name,
description
2025-04-05 10:05:52 +00:00
}
}).catch((err) => {
toast.error($i18n.t('Connection failed'));
});
if (res) {
toast.success($i18n.t('Connection successful'));
console.debug('Connection successful', res);
}
}
};
2025-09-27 03:53:02 +00:00
const importHandler = async (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
const json = event.target.result;
console.log('importHandler', json);
try {
2025-09-27 03:57:38 +00:00
let data = JSON.parse(json);
// validate data
if (Array.isArray(data)) {
if (data.length === 0) {
toast.error($i18n.t('Please select a valid JSON file'));
return;
}
data = data[0];
}
2025-09-27 03:53:02 +00:00
if (data.type) type = data.type;
if (data.url) url = data.url;
if (data.spec_type) spec_type = data.spec_type;
if (data.spec) spec = data.spec;
if (data.path) path = data.path;
if (data.auth_type) auth_type = data.auth_type;
if (data.key) key = data.key;
if (data.info) {
id = data.info.id ?? '';
name = data.info.name ?? '';
description = data.info.description ?? '';
}
if (data.config) {
enable = data.config.enable ?? true;
accessControl = data.config.access_control ?? {};
}
toast.success($i18n.t('Import successful'));
} catch (error) {
toast.error($i18n.t('Please select a valid JSON file'));
}
};
reader.readAsText(file);
};
const exportHandler = async () => {
// export current connection as json file
2025-09-27 03:57:38 +00:00
const json = JSON.stringify([
{
type,
url,
2025-09-27 03:53:02 +00:00
2025-09-27 03:57:38 +00:00
spec_type,
spec,
path,
2025-09-27 03:53:02 +00:00
2025-09-27 03:57:38 +00:00
auth_type,
key,
2025-09-27 03:53:02 +00:00
2025-09-27 03:57:38 +00:00
info: {
id: id,
name: name,
description: description
}
2025-09-27 03:53:02 +00:00
}
2025-09-27 03:57:38 +00:00
]);
2025-09-27 03:53:02 +00:00
const blob = new Blob([json], {
type: 'application/json'
});
2025-09-27 03:53:57 +00:00
saveAs(blob, `tool-server-${id || name || 'export'}.json`);
2025-09-27 03:53:02 +00:00
};
const submitHandler = async () => {
loading = true;
2025-03-27 08:42:33 +00:00
// remove trailing slash from url
url = url.replace(/\/$/, '');
2025-09-23 06:03:26 +00:00
if (id.includes(':') || id.includes('|')) {
toast.error($i18n.t('ID cannot contain ":" or "|" characters'));
loading = false;
return;
}
2025-03-27 08:42:33 +00:00
if (type === 'mcp' && auth_type === 'oauth_2.1' && !oauthClientInfo) {
toast.error($i18n.t('Please register the OAuth client'));
loading = false;
return;
}
// validate spec
if (spec_type === 'json') {
try {
const specJSON = JSON.parse(spec);
spec = JSON.stringify(specJSON, null, 2);
} catch (e) {
toast.error($i18n.t('Please enter a valid JSON spec'));
loading = false;
return;
}
}
const connection = {
type,
url,
spec_type,
spec,
2025-04-03 03:42:23 +00:00
path,
2025-04-03 03:42:23 +00:00
auth_type,
key,
config: {
2025-04-05 10:40:01 +00:00
enable: enable,
access_control: accessControl
},
info: {
id: id,
name: name,
description: description,
...(oauthClientInfo ? { oauth_client_info: oauthClientInfo } : {})
}
};
await onSubmit(connection);
loading = false;
show = false;
2025-09-22 18:46:47 +00:00
// reset form
type = 'openapi';
url = '';
spec_type = 'url';
spec = '';
2025-04-05 10:05:52 +00:00
path = 'openapi.json';
2025-09-22 18:46:47 +00:00
2025-04-04 14:31:13 +00:00
key = '';
2025-04-03 03:42:23 +00:00
auth_type = 'bearer';
id = '';
name = '';
description = '';
oauthClientInfo = null;
enable = true;
2025-04-05 10:40:01 +00:00
accessControl = null;
};
const init = () => {
if (connection) {
type = connection?.type ?? 'openapi';
url = connection.url;
spec_type = connection?.spec_type ?? 'url';
spec = connection?.spec ?? '';
2025-04-05 10:05:52 +00:00
path = connection?.path ?? 'openapi.json';
2025-04-03 03:42:23 +00:00
auth_type = connection?.auth_type ?? 'bearer';
key = connection?.key ?? '';
id = connection.info?.id ?? '';
name = connection.info?.name ?? '';
description = connection.info?.description ?? '';
oauthClientInfo = connection.info?.oauth_client_info ?? null;
enable = connection.config?.enable ?? true;
2025-04-05 10:40:01 +00:00
accessControl = connection.config?.access_control ?? null;
}
};
$: if (show) {
init();
}
onMount(() => {
init();
});
</script>
<Modal size="sm" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 pb-2">
<h1 class=" text-lg font-medium self-center font-primary">
{#if edit}
{$i18n.t('Edit Connection')}
{:else}
{$i18n.t('Add Connection')}
{/if}
</h1>
2025-09-27 03:53:02 +00:00
<div class="flex items-center gap-3">
<div class="flex gap-1.5 text-xs justify-end">
<button
class=" hover:underline"
type="button"
on:click={() => {
inputElement?.click();
}}
>
{$i18n.t('Import')}
</button>
<button class=" hover:underline" type="button" on:click={exportHandler}>
{$i18n.t('Export')}
</button>
</div>
<button
class="self-center"
aria-label={$i18n.t('Close Configure Connection Modal')}
on:click={() => {
show = false;
}}
>
<XMark className={'size-5'} />
</button>
</div>
</div>
<div class="flex flex-col md:flex-row w-full px-4 pb-4 md:space-x-4 dark:text-gray-200">
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
2025-09-27 03:53:02 +00:00
<input
bind:this={inputElement}
type="file"
hidden
accept=".json"
on:change={(e) => {
importHandler(e);
}}
/>
<form
class="flex flex-col w-full"
on:submit={(e) => {
e.preventDefault();
submitHandler();
}}
>
<div class="px-1">
2025-09-21 07:12:24 +00:00
{#if !direct}
<div class="flex gap-2 mb-1.5">
<div class="flex w-full justify-between items-center">
<div class=" text-xs text-gray-500">{$i18n.t('Type')}</div>
<div class="">
<button
on:click={() => {
type = ['', 'openapi'].includes(type) ? 'mcp' : 'openapi';
}}
type="button"
class=" text-xs text-gray-700 dark:text-gray-300"
>
{#if ['', 'openapi'].includes(type)}
{$i18n.t('OpenAPI')}
{:else if type === 'mcp'}
{$i18n.t('MCP')}
2025-09-23 06:03:26 +00:00
<span class="text-gray-500">{$i18n.t('Streamable HTTP')}</span>
2025-09-21 07:12:24 +00:00
{/if}
</button>
</div>
</div>
</div>
{/if}
<div class="flex gap-2">
<div class="flex flex-col w-full">
2025-04-05 10:05:52 +00:00
<div class="flex justify-between mb-0.5">
<label
for="api-base-url"
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
>{$i18n.t('URL')}</label
>
2025-04-05 10:05:52 +00:00
</div>
2025-04-05 10:05:52 +00:00
<div class="flex flex-1 items-center">
<input
2025-06-17 07:53:49 +00:00
id="api-base-url"
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'}`}
type="text"
bind:value={url}
placeholder={$i18n.t('API Base URL')}
autocomplete="off"
required
/>
2025-04-05 10:05:52 +00:00
<Tooltip
content={$i18n.t('Verify Connection')}
className="shrink-0 flex items-center mr-1"
>
<button
class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
on:click={() => {
verifyHandler();
}}
2025-06-17 07:55:26 +00:00
aria-label={$i18n.t('Verify Connection')}
2025-04-05 10:05:52 +00:00
type="button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
aria-hidden="true"
2025-04-05 10:05:52 +00:00
>
<path
fill-rule="evenodd"
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"
clip-rule="evenodd"
/>
</svg>
</button>
</Tooltip>
<Tooltip content={enable ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
<Switch bind:state={enable} />
</Tooltip>
</div>
</div>
</div>
2025-09-21 07:12:24 +00:00
{#if ['', 'openapi'].includes(type)}
<div class="flex gap-2 mt-2">
<div class="flex flex-col w-full">
<div class="flex justify-between items-center mb-0.5">
<div class="flex gap-2 items-center">
<div
for="select-bearer-or-session"
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
>
{$i18n.t('OpenAPI Spec')}
</div>
</div>
</div>
<div class="flex gap-2">
<div class="flex-shrink-0 self-start">
<select
id="select-bearer-or-session"
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'}`}
bind:value={spec_type}
>
<option value="url">{$i18n.t('URL')}</option>
<option value="json">{$i18n.t('JSON')}</option>
</select>
</div>
<div class="flex flex-1 items-center">
{#if spec_type === 'url'}
<div class="flex-1 flex items-center">
<label for="url-or-path" class="sr-only"
>{$i18n.t('openapi.json URL or Path')}</label
>
<input
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'}`}
type="text"
id="url-or-path"
bind:value={path}
placeholder={$i18n.t('openapi.json URL or Path')}
autocomplete="off"
required
/>
</div>
{:else if spec_type === 'json'}
<div
class={`text-xs w-full self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
>
<label for="url-or-path" class="sr-only">{$i18n.t('JSON Spec')}</label>
<textarea
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'}`}
bind:value={spec}
placeholder={$i18n.t('JSON Spec')}
autocomplete="off"
required
rows="5"
/>
</div>
{/if}
</div>
</div>
{#if ['', 'url'].includes(spec_type)}
<div
class={`text-xs mt-1 ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
>
{$i18n.t(`WebUI will make requests to "{{url}}"`, {
url: path.includes('://')
? path
: `${url}${path.startsWith('/') ? '' : '/'}${path}`
})}
</div>
{/if}
</div>
2025-09-21 07:12:24 +00:00
</div>
{/if}
<div class="flex gap-2 mt-2">
<div class="flex flex-col w-full">
<div class="flex justify-between items-center">
<div class="flex gap-2 items-center">
<div
for="select-bearer-or-session"
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
>
{$i18n.t('Auth')}
</div>
</div>
{#if auth_type === 'oauth_2.1'}
<div class="flex items-center gap-2">
<div class="flex flex-col justify-end items-center shrink-0">
<Tooltip
content={oauthClientInfo
? $i18n.t('Register Again')
: $i18n.t('Register Client')}
>
<button
class=" text-xs underline dark:text-gray-500 dark:hover:text-gray-200 text-gray-700 hover:text-gray-900 transition"
type="button"
on:click={() => {
registerOAuthClientHandler();
}}
>
{$i18n.t('Register Client')}
</button>
</Tooltip>
</div>
2025-09-25 06:53:10 +00:00
{#if !oauthClientInfo}
<div
class="text-xs font-medium px-1.5 rounded-md bg-yellow-500/20 text-yellow-700 dark:text-yellow-200"
>
{$i18n.t('Not Registered')}
</div>
{:else}
<div
class="text-xs font-medium px-1.5 rounded-md bg-green-500/20 text-green-700 dark:text-green-200"
>
{$i18n.t('Registered')}
</div>
{/if}
</div>
{/if}
</div>
<div class="flex gap-2">
<div class="flex-shrink-0 self-start">
<select
2025-06-17 07:53:49 +00:00
id="select-bearer-or-session"
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'}`}
bind:value={auth_type}
>
2025-09-08 15:09:26 +00:00
<option value="none">{$i18n.t('None')}</option>
2025-08-14 00:15:16 +00:00
<option value="bearer">{$i18n.t('Bearer')}</option>
<option value="session">{$i18n.t('Session')}</option>
{#if !direct}
2025-09-08 15:53:44 +00:00
<option value="system_oauth">{$i18n.t('OAuth')}</option>
{#if type === 'mcp'}
<option value="oauth_2.1">{$i18n.t('OAuth 2.1')}</option>
{/if}
{/if}
</select>
</div>
<div class="flex flex-1 items-center">
{#if auth_type === 'bearer'}
<SensitiveInput
bind:value={key}
placeholder={$i18n.t('API Key')}
required={false}
/>
2025-09-08 15:09:26 +00:00
{:else if auth_type === 'none'}
<div
class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
>
{$i18n.t('No authentication')}
</div>
{:else if auth_type === 'session'}
<div
class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
>
{$i18n.t('Forwards system user session credentials to authenticate')}
</div>
2025-09-08 16:25:19 +00:00
{:else if auth_type === 'system_oauth'}
<div
class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
>
2025-09-08 14:18:04 +00:00
{$i18n.t('Forwards system user OAuth access token to authenticate')}
</div>
{:else if auth_type === 'oauth_2.1'}
<div
class={`flex items-center text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
>
2025-09-25 16:43:10 +00:00
{$i18n.t('Uses OAuth 2.1 Dynamic Client Registration')}
</div>
{/if}
</div>
</div>
</div>
</div>
2025-04-05 10:40:01 +00:00
{#if !direct}
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
<div class="flex gap-2">
<div class="flex flex-col w-full">
<label
for="enter-id"
class={`mb-0.5 text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
>{$i18n.t('ID')}
2025-09-23 06:03:26 +00:00
{#if type !== 'mcp'}
<span class="text-xs text-gray-200 dark:text-gray-800 ml-0.5"
>{$i18n.t('Optional')}</span
>
{/if}
</label>
<div class="flex-1">
<input
id="enter-id"
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'}`}
type="text"
bind:value={id}
placeholder={$i18n.t('Enter ID')}
autocomplete="off"
2025-09-23 06:03:26 +00:00
required={type === 'mcp'}
/>
</div>
</div>
</div>
<div class="flex gap-2 mt-2">
<div class="flex flex-col w-full">
<label
for="enter-name"
class={`mb-0.5 text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
>{$i18n.t('Name')}
</label>
<div class="flex-1">
<input
2025-06-17 07:53:49 +00:00
id="enter-name"
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'}`}
type="text"
bind:value={name}
placeholder={$i18n.t('Enter name')}
autocomplete="off"
required
/>
</div>
</div>
</div>
<div class="flex flex-col w-full mt-2">
<label
for="description"
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'}`}
2025-06-17 07:53:49 +00:00
>{$i18n.t('Description')}</label
>
<div class="flex-1">
<input
2025-06-17 07:53:49 +00:00
id="description"
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'}`}
type="text"
bind:value={description}
placeholder={$i18n.t('Enter description')}
autocomplete="off"
/>
</div>
</div>
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
2025-04-05 10:40:01 +00:00
<div class="my-2 -mx-2">
2025-09-21 07:12:24 +00:00
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-950 rounded-3xl">
2025-04-05 10:40:01 +00:00
<AccessControl bind:accessControl />
</div>
</div>
{/if}
</div>
{#if type === 'mcp'}
<div
class=" bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-2xl text-xs px-4 py-3 mb-2"
>
<span class="font-medium">
{$i18n.t('Warning')}:
</span>
{$i18n.t(
'MCP support is experimental and its specification changes often, which can lead to incompatibilities. OpenAPI specification support is directly maintained by the CyberLover team, making it the more reliable option for compatibility.'
)}
<a
class="font-medium underline"
href="https://docs.openwebui.com/features/mcp"
target="_blank">{$i18n.t('Read more →')}</a
>
</div>
{/if}
2025-09-27 03:53:02 +00:00
<div class="flex justify-between pt-3 text-sm font-medium gap-1.5">
<div></div>
<div class="flex gap-1.5">
{#if edit}
<button
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"
type="button"
on:click={() => {
onDelete();
show = false;
}}
>
{$i18n.t('Delete')}
</button>
{/if}
<button
2025-09-27 03:53:02 +00:00
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
? ' cursor-not-allowed'
: ''}"
type="submit"
disabled={loading}
>
2025-09-27 03:53:02 +00:00
{$i18n.t('Save')}
2025-09-27 03:53:02 +00:00
{#if loading}
<div class="ml-2 self-center">
<Spinner />
</div>
{/if}
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</Modal>