|
|
|
@@ -1,17 +1,8 @@
|
|
|
|
|
<script lang="ts">
|
|
|
|
|
import { getContext, onMount, tick } from 'svelte';
|
|
|
|
|
import { getContext, onDestroy, tick } from 'svelte';
|
|
|
|
|
import { fly } from 'svelte/transition';
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
config,
|
|
|
|
|
user,
|
|
|
|
|
tools as _tools,
|
|
|
|
|
skills as _skills,
|
|
|
|
|
mobile,
|
|
|
|
|
settings,
|
|
|
|
|
toolServers,
|
|
|
|
|
terminalServers
|
|
|
|
|
} from '$lib/stores';
|
|
|
|
|
import { user, tools as _tools, skills as _skills, toolServers } from '$lib/stores';
|
|
|
|
|
|
|
|
|
|
import { initiateOAuthRedirect } from '$lib/apis/configs';
|
|
|
|
|
import { deleteOAuthSession } from '$lib/apis/auths';
|
|
|
|
@@ -26,6 +17,7 @@
|
|
|
|
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
|
|
|
|
import Switch from '$lib/components/common/Switch.svelte';
|
|
|
|
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
|
|
|
|
import SearchInput from './InputMenu/SearchInput.svelte';
|
|
|
|
|
import Wrench from '$lib/components/icons/Wrench.svelte';
|
|
|
|
|
import Cube from '$lib/components/icons/Cube.svelte';
|
|
|
|
|
import Sparkles from '$lib/components/icons/Sparkles.svelte';
|
|
|
|
@@ -36,7 +28,19 @@
|
|
|
|
|
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
|
|
|
|
import LinkSlash from '$lib/components/icons/LinkSlash.svelte';
|
|
|
|
|
|
|
|
|
|
const i18n = getContext('i18n');
|
|
|
|
|
const i18n = getContext('i18n') as any;
|
|
|
|
|
|
|
|
|
|
type IntegrationItem = {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
description?: string;
|
|
|
|
|
enabled: boolean;
|
|
|
|
|
meta?: { description?: string };
|
|
|
|
|
is_active?: boolean;
|
|
|
|
|
authenticated?: boolean;
|
|
|
|
|
has_user_valves?: boolean;
|
|
|
|
|
[key: string]: any;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export let selectedToolIds: string[] = [];
|
|
|
|
|
export let selectedSkillIds: string[] = [];
|
|
|
|
@@ -44,8 +48,13 @@
|
|
|
|
|
export let selectedModels: string[] = [];
|
|
|
|
|
export let fileUploadCapableModels: string[] = [];
|
|
|
|
|
|
|
|
|
|
export let toggleFilters: { id: string; name: string; description?: string; icon?: string }[] =
|
|
|
|
|
[];
|
|
|
|
|
export let toggleFilters: {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
description?: string;
|
|
|
|
|
icon?: string;
|
|
|
|
|
has_user_valves?: boolean;
|
|
|
|
|
}[] = [];
|
|
|
|
|
export let selectedFilterIds: string[] = [];
|
|
|
|
|
|
|
|
|
|
export let showWebSearchButton = false;
|
|
|
|
@@ -63,8 +72,27 @@
|
|
|
|
|
let show = false;
|
|
|
|
|
let tab = '';
|
|
|
|
|
|
|
|
|
|
let tools = null;
|
|
|
|
|
let skills = null;
|
|
|
|
|
let tools: Record<string, IntegrationItem> | null = null;
|
|
|
|
|
let skills: Record<string, IntegrationItem> | null = null;
|
|
|
|
|
let toolQuery = '';
|
|
|
|
|
let skillQuery = '';
|
|
|
|
|
let searchedToolQuery = '';
|
|
|
|
|
let searchedSkillQuery = '';
|
|
|
|
|
let toolSearchDebounceTimer: ReturnType<typeof setTimeout>;
|
|
|
|
|
let skillSearchDebounceTimer: ReturnType<typeof setTimeout>;
|
|
|
|
|
let toolRequestId = 0;
|
|
|
|
|
let skillRequestId = 0;
|
|
|
|
|
|
|
|
|
|
$: toolIds = Object.keys(tools ?? {});
|
|
|
|
|
$: skillIds = Object.keys(skills ?? {});
|
|
|
|
|
|
|
|
|
|
$: if (show && toolQuery !== searchedToolQuery) {
|
|
|
|
|
scheduleToolSearch();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$: if (show && skillQuery !== searchedSkillQuery) {
|
|
|
|
|
scheduleSkillSearch();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$: if (show) {
|
|
|
|
|
init();
|
|
|
|
@@ -76,61 +104,168 @@
|
|
|
|
|
($user?.role === 'admin' || $user?.permissions?.chat?.file_upload);
|
|
|
|
|
|
|
|
|
|
const init = async () => {
|
|
|
|
|
if ($_tools === null) {
|
|
|
|
|
await _tools.set(await getTools(localStorage.token));
|
|
|
|
|
}
|
|
|
|
|
await Promise.all([loadTools(), loadSkills()]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if ($_tools) {
|
|
|
|
|
tools = $_tools.reduce((a, tool, i, arr) => {
|
|
|
|
|
a[tool.id] = {
|
|
|
|
|
name: tool.name,
|
|
|
|
|
description: tool.meta.description,
|
|
|
|
|
enabled: selectedToolIds.includes(tool.id),
|
|
|
|
|
...tool
|
|
|
|
|
};
|
|
|
|
|
return a;
|
|
|
|
|
}, {});
|
|
|
|
|
}
|
|
|
|
|
const setTools = (toolItems: IntegrationItem[] | null, query = '') => {
|
|
|
|
|
const q = query.trim().toLowerCase();
|
|
|
|
|
const items = (toolItems ?? []).reduce<Record<string, IntegrationItem>>((a, tool) => {
|
|
|
|
|
a[tool.id] = {
|
|
|
|
|
...tool,
|
|
|
|
|
name: tool.name,
|
|
|
|
|
description: tool.meta?.description,
|
|
|
|
|
enabled: selectedToolIds.includes(tool.id)
|
|
|
|
|
};
|
|
|
|
|
return a;
|
|
|
|
|
}, {});
|
|
|
|
|
|
|
|
|
|
if ($toolServers) {
|
|
|
|
|
for (const serverIdx in $toolServers) {
|
|
|
|
|
const server = $toolServers[serverIdx];
|
|
|
|
|
if (server.info) {
|
|
|
|
|
tools[`direct_server:${serverIdx}`] = {
|
|
|
|
|
name: server?.info?.title ?? server.url,
|
|
|
|
|
description: server.info.description ?? '',
|
|
|
|
|
enabled: selectedToolIds.includes(`direct_server:${serverIdx}`)
|
|
|
|
|
};
|
|
|
|
|
for (const serverIdx in ($toolServers ?? []) as any[]) {
|
|
|
|
|
const server = (($toolServers ?? []) as any[])[serverIdx];
|
|
|
|
|
if (server.info) {
|
|
|
|
|
const name = server?.info?.title ?? server.url;
|
|
|
|
|
if (q && !name.toLowerCase().includes(q)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
items[`direct_server:${serverIdx}`] = {
|
|
|
|
|
id: `direct_server:${serverIdx}`,
|
|
|
|
|
name,
|
|
|
|
|
description: server.info.description ?? '',
|
|
|
|
|
enabled: selectedToolIds.includes(`direct_server:${serverIdx}`)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
selectedToolIds = selectedToolIds.filter((id) => Object.keys(tools).includes(id));
|
|
|
|
|
tools = items;
|
|
|
|
|
|
|
|
|
|
if (!q) {
|
|
|
|
|
selectedToolIds = selectedToolIds.filter((id) => Object.keys(tools ?? {}).includes(id));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const setSkills = (skillItems: IntegrationItem[] | null, query = '') => {
|
|
|
|
|
skills = (skillItems ?? [])
|
|
|
|
|
.filter((skill) => skill.is_active)
|
|
|
|
|
.reduce<Record<string, IntegrationItem>>((a, skill) => {
|
|
|
|
|
a[skill.id] = {
|
|
|
|
|
...skill,
|
|
|
|
|
name: skill.name,
|
|
|
|
|
description: skill.description,
|
|
|
|
|
enabled: selectedSkillIds.includes(skill.id)
|
|
|
|
|
};
|
|
|
|
|
return a;
|
|
|
|
|
}, {});
|
|
|
|
|
|
|
|
|
|
if (!query.trim()) {
|
|
|
|
|
selectedSkillIds = selectedSkillIds.filter((id) => Object.keys(skills ?? {}).includes(id));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadTools = async (query = toolQuery) => {
|
|
|
|
|
const requestId = ++toolRequestId;
|
|
|
|
|
const q = query.trim();
|
|
|
|
|
searchedToolQuery = query;
|
|
|
|
|
|
|
|
|
|
if (q) {
|
|
|
|
|
const toolItems = await getTools(localStorage.token, q).catch(() => []);
|
|
|
|
|
if (requestId !== toolRequestId) return;
|
|
|
|
|
setTools(toolItems, q);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($_tools === null) {
|
|
|
|
|
await _tools.set(await getTools(localStorage.token));
|
|
|
|
|
}
|
|
|
|
|
if (requestId !== toolRequestId) return;
|
|
|
|
|
setTools($_tools, q);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadSkills = async (query = skillQuery) => {
|
|
|
|
|
const requestId = ++skillRequestId;
|
|
|
|
|
const q = query.trim();
|
|
|
|
|
searchedSkillQuery = query;
|
|
|
|
|
|
|
|
|
|
if (q) {
|
|
|
|
|
const skillItems = await getSkills(localStorage.token, q).catch(() => []);
|
|
|
|
|
if (requestId !== skillRequestId) return;
|
|
|
|
|
setSkills(skillItems, q);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($_skills === null) {
|
|
|
|
|
await _skills.set(await getSkills(localStorage.token));
|
|
|
|
|
}
|
|
|
|
|
if (requestId !== skillRequestId) return;
|
|
|
|
|
setSkills($_skills, q);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if ($_skills) {
|
|
|
|
|
skills = $_skills
|
|
|
|
|
.filter((skill) => skill.is_active)
|
|
|
|
|
.reduce((a, skill) => {
|
|
|
|
|
a[skill.id] = {
|
|
|
|
|
name: skill.name,
|
|
|
|
|
description: skill.description,
|
|
|
|
|
enabled: selectedSkillIds.includes(skill.id),
|
|
|
|
|
...skill
|
|
|
|
|
};
|
|
|
|
|
return a;
|
|
|
|
|
}, {});
|
|
|
|
|
const scheduleToolSearch = () => {
|
|
|
|
|
clearTimeout(toolSearchDebounceTimer);
|
|
|
|
|
toolSearchDebounceTimer = setTimeout(() => {
|
|
|
|
|
loadTools();
|
|
|
|
|
}, 200);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const scheduleSkillSearch = () => {
|
|
|
|
|
clearTimeout(skillSearchDebounceTimer);
|
|
|
|
|
skillSearchDebounceTimer = setTimeout(() => {
|
|
|
|
|
loadSkills();
|
|
|
|
|
}, 200);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const toggleTool = async (toolId: string, e: MouseEvent) => {
|
|
|
|
|
const tool = tools?.[toolId];
|
|
|
|
|
if (!tool) return;
|
|
|
|
|
|
|
|
|
|
if (!(tool.authenticated ?? true)) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
const parts = toolId.split(':');
|
|
|
|
|
initiateOAuthRedirect({
|
|
|
|
|
id: toolId,
|
|
|
|
|
serverId: parts.at(-1) ?? toolId,
|
|
|
|
|
authType: parts.length > 1 ? (parts[0] === 'server' ? parts[1] : parts[0]) : null
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
selectedSkillIds = selectedSkillIds.filter((id) => Object.keys(skills ?? {}).includes(id));
|
|
|
|
|
tool.enabled = !tool.enabled;
|
|
|
|
|
|
|
|
|
|
const state = tool.enabled;
|
|
|
|
|
await tick();
|
|
|
|
|
|
|
|
|
|
if (state) {
|
|
|
|
|
selectedToolIds = [...selectedToolIds, toolId];
|
|
|
|
|
} else {
|
|
|
|
|
selectedToolIds = selectedToolIds.filter((id) => id !== toolId);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const toggleSkill = async (skillId: string) => {
|
|
|
|
|
const skill = skills?.[skillId];
|
|
|
|
|
if (!skill) return;
|
|
|
|
|
|
|
|
|
|
skill.enabled = !skill.enabled;
|
|
|
|
|
|
|
|
|
|
const state = skill.enabled;
|
|
|
|
|
await tick();
|
|
|
|
|
|
|
|
|
|
if (state) {
|
|
|
|
|
selectedSkillIds = [...selectedSkillIds, skillId];
|
|
|
|
|
} else {
|
|
|
|
|
selectedSkillIds = selectedSkillIds.filter((id) => id !== skillId);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onDestroy(() => {
|
|
|
|
|
clearTimeout(toolSearchDebounceTimer);
|
|
|
|
|
clearTimeout(skillSearchDebounceTimer);
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<Dropdown
|
|
|
|
|
bind:show
|
|
|
|
|
{closeOnOutsideClick}
|
|
|
|
|
onOpenChange={(state) => {
|
|
|
|
|
if (state === false) {
|
|
|
|
|
onClose();
|
|
|
|
@@ -141,11 +276,12 @@
|
|
|
|
|
<slot />
|
|
|
|
|
</Tooltip>
|
|
|
|
|
<div slot="content">
|
|
|
|
|
<DropdownMenu
|
|
|
|
|
className="min-w-70 max-w-70 max-h-72 overflow-y-auto overflow-x-hidden scrollbar-thin"
|
|
|
|
|
>
|
|
|
|
|
<DropdownMenu className="min-w-70 max-w-70 max-h-72 overflow-hidden">
|
|
|
|
|
{#if tab === ''}
|
|
|
|
|
<div in:fly={{ x: -20, duration: 150 }}>
|
|
|
|
|
<div
|
|
|
|
|
class="max-h-72 overflow-y-auto overflow-x-hidden scrollbar-thin"
|
|
|
|
|
in:fly={{ x: -20, duration: 150 }}
|
|
|
|
|
>
|
|
|
|
|
{#if tools}
|
|
|
|
|
{#if Object.keys(tools).length > 0}
|
|
|
|
|
<button
|
|
|
|
@@ -342,7 +478,7 @@
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
{:else if tab === 'tools' && tools}
|
|
|
|
|
<div in:fly={{ x: 20, duration: 150 }}>
|
|
|
|
|
<div class="flex max-h-72 min-h-0 flex-col gap-0.5" in:fly={{ x: 20, duration: 150 }}>
|
|
|
|
|
<button
|
|
|
|
|
class="flex w-full justify-between gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer rounded-xl hover:bg-gray-50/40 dark:hover:bg-gray-800/40"
|
|
|
|
|
on:click={() => {
|
|
|
|
@@ -354,121 +490,110 @@
|
|
|
|
|
<div class="flex items-center w-full justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
{$i18n.t('Tools')}
|
|
|
|
|
<span class="ml-0.5 text-gray-500">{Object.keys(tools).length}</span>
|
|
|
|
|
<span class="ml-0.5 text-gray-500">{toolIds.length}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{#each Object.keys(tools) as toolId}
|
|
|
|
|
<button
|
|
|
|
|
class="relative flex w-full justify-between gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer rounded-xl hover:bg-gray-50/40 dark:hover:bg-gray-800/40"
|
|
|
|
|
aria-pressed={(tools[toolId]?.authenticated ?? true)
|
|
|
|
|
? tools[toolId].enabled
|
|
|
|
|
: undefined}
|
|
|
|
|
on:click={async (e) => {
|
|
|
|
|
if (!(tools[toolId]?.authenticated ?? true)) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
<SearchInput bind:value={toolQuery} placeholder={$i18n.t('Search tools')} />
|
|
|
|
|
|
|
|
|
|
const parts = toolId.split(':');
|
|
|
|
|
initiateOAuthRedirect({
|
|
|
|
|
id: toolId,
|
|
|
|
|
serverId: parts.at(-1) ?? toolId,
|
|
|
|
|
authType:
|
|
|
|
|
parts.length > 1 ? (parts[0] === 'server' ? parts[1] : parts[0]) : null
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
tools[toolId].enabled = !tools[toolId].enabled;
|
|
|
|
|
|
|
|
|
|
const state = tools[toolId].enabled;
|
|
|
|
|
await tick();
|
|
|
|
|
|
|
|
|
|
if (state) {
|
|
|
|
|
selectedToolIds = [...selectedToolIds, toolId];
|
|
|
|
|
} else {
|
|
|
|
|
selectedToolIds = selectedToolIds.filter((id) => id !== toolId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{#if !(tools[toolId]?.authenticated ?? true)}
|
|
|
|
|
<!-- make it slighly darker and not clickable -->
|
|
|
|
|
<div class="absolute inset-0 opacity-50 rounded-xl cursor-pointer z-10" />
|
|
|
|
|
{/if}
|
|
|
|
|
<div class="flex-1 truncate">
|
|
|
|
|
<div class="flex flex-1 gap-2 items-center">
|
|
|
|
|
<Tooltip content={tools[toolId]?.name ?? ''} placement="top">
|
|
|
|
|
<div class="shrink-0">
|
|
|
|
|
<Wrench />
|
|
|
|
|
<div class="min-h-0 flex-1 overflow-y-auto overflow-x-hidden scrollbar-thin">
|
|
|
|
|
{#if toolIds.length === 0}
|
|
|
|
|
<div class="text-center text-xs text-gray-500 py-3">{$i18n.t('No tools found')}</div>
|
|
|
|
|
{:else}
|
|
|
|
|
<div class="flex flex-col gap-0.5">
|
|
|
|
|
{#each toolIds as toolId}
|
|
|
|
|
<button
|
|
|
|
|
class="relative flex w-full justify-between gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer rounded-xl hover:bg-gray-50/40 dark:hover:bg-gray-800/40"
|
|
|
|
|
aria-pressed={(tools?.[toolId]?.authenticated ?? true)
|
|
|
|
|
? tools?.[toolId]?.enabled
|
|
|
|
|
: undefined}
|
|
|
|
|
on:click={async (e) => {
|
|
|
|
|
await toggleTool(toolId, e);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{#if !(tools?.[toolId]?.authenticated ?? true)}
|
|
|
|
|
<!-- make it slighly darker and not clickable -->
|
|
|
|
|
<div class="absolute inset-0 opacity-50 rounded-xl cursor-pointer z-10"></div>
|
|
|
|
|
{/if}
|
|
|
|
|
<div class="flex-1 truncate">
|
|
|
|
|
<div class="flex flex-1 gap-2 items-center">
|
|
|
|
|
<Tooltip content={tools?.[toolId]?.name ?? ''} placement="top">
|
|
|
|
|
<div class="shrink-0">
|
|
|
|
|
<Wrench />
|
|
|
|
|
</div>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
<Tooltip content={tools?.[toolId]?.description ?? ''} placement="top-start">
|
|
|
|
|
<div class=" truncate">{tools?.[toolId]?.name}</div>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
<Tooltip content={tools[toolId]?.description ?? ''} placement="top-start">
|
|
|
|
|
<div class=" truncate">{tools[toolId].name}</div>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{#if (tools?.[toolId]?.authenticated ?? true) && toolId.startsWith('server:mcp:')}
|
|
|
|
|
<div class="shrink-0">
|
|
|
|
|
<Tooltip content={$i18n.t('Disconnect OAuth')}>
|
|
|
|
|
<button
|
|
|
|
|
class="self-center w-fit text-sm text-gray-600 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition rounded-full"
|
|
|
|
|
type="button"
|
|
|
|
|
on:click={async (e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
const parts = toolId.split(':');
|
|
|
|
|
const serverId = parts.at(-1) ?? toolId;
|
|
|
|
|
const provider = `mcp:${serverId}`;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await deleteOAuthSession(localStorage.token, provider);
|
|
|
|
|
toast.success($i18n.t('OAuth session disconnected'));
|
|
|
|
|
|
|
|
|
|
// Refresh tools to update authenticated state
|
|
|
|
|
_tools.set(await getTools(localStorage.token));
|
|
|
|
|
selectedToolIds = selectedToolIds.filter((id) => id !== toolId);
|
|
|
|
|
await init();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
toast.error(err ?? $i18n.t('Failed to disconnect'));
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<LinkSlash className="size-3.5" />
|
|
|
|
|
</button>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
{#if tools?.[toolId]?.has_user_valves && ($user?.role === 'admin' || ($user?.permissions?.chat?.valves ?? true))}
|
|
|
|
|
<div class=" shrink-0">
|
|
|
|
|
<Tooltip content={$i18n.t('Valves')}>
|
|
|
|
|
<button
|
|
|
|
|
class="self-center w-fit text-sm text-gray-600 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition rounded-full"
|
|
|
|
|
type="button"
|
|
|
|
|
on:click={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
onShowValves({
|
|
|
|
|
type: 'tool',
|
|
|
|
|
id: toolId
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Knobs />
|
|
|
|
|
</button>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<div class=" shrink-0" inert>
|
|
|
|
|
<Switch state={tools?.[toolId]?.enabled} />
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{#if (tools[toolId]?.authenticated ?? true) && toolId.startsWith('server:mcp:')}
|
|
|
|
|
<div class="shrink-0">
|
|
|
|
|
<Tooltip content={$i18n.t('Disconnect OAuth')}>
|
|
|
|
|
<button
|
|
|
|
|
class="self-center w-fit text-sm text-gray-600 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition rounded-full"
|
|
|
|
|
type="button"
|
|
|
|
|
on:click={async (e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
const parts = toolId.split(':');
|
|
|
|
|
const serverId = parts.at(-1) ?? toolId;
|
|
|
|
|
const provider = `mcp:${serverId}`;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await deleteOAuthSession(localStorage.token, provider);
|
|
|
|
|
toast.success($i18n.t('OAuth session disconnected'));
|
|
|
|
|
|
|
|
|
|
// Refresh tools to update authenticated state
|
|
|
|
|
_tools.set(await getTools(localStorage.token));
|
|
|
|
|
selectedToolIds = selectedToolIds.filter((id) => id !== toolId);
|
|
|
|
|
await init();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
toast.error(err ?? $i18n.t('Failed to disconnect'));
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<LinkSlash className="size-3.5" />
|
|
|
|
|
</button>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
{#if tools[toolId]?.has_user_valves && ($user?.role === 'admin' || ($user?.permissions?.chat?.valves ?? true))}
|
|
|
|
|
<div class=" shrink-0">
|
|
|
|
|
<Tooltip content={$i18n.t('Valves')}>
|
|
|
|
|
<button
|
|
|
|
|
class="self-center w-fit text-sm text-gray-600 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition rounded-full"
|
|
|
|
|
type="button"
|
|
|
|
|
on:click={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
onShowValves({
|
|
|
|
|
type: 'tool',
|
|
|
|
|
id: toolId
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Knobs />
|
|
|
|
|
</button>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<div class=" shrink-0" inert>
|
|
|
|
|
<Switch state={tools[toolId].enabled} />
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
{/each}
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{:else if tab === 'skills' && skills}
|
|
|
|
|
<div in:fly={{ x: 20, duration: 150 }}>
|
|
|
|
|
<div class="flex max-h-72 min-h-0 flex-col gap-0.5" in:fly={{ x: 20, duration: 150 }}>
|
|
|
|
|
<button
|
|
|
|
|
class="flex w-full justify-between gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer rounded-xl hover:bg-gray-50/40 dark:hover:bg-gray-800/40"
|
|
|
|
|
on:click={() => {
|
|
|
|
@@ -480,46 +605,50 @@
|
|
|
|
|
<div class="flex items-center w-full justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
{$i18n.t('Skills')}
|
|
|
|
|
<span class="ml-0.5 text-gray-500">{Object.keys(skills).length}</span>
|
|
|
|
|
<span class="ml-0.5 text-gray-500">{skillIds.length}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{#each Object.keys(skills) as skillId}
|
|
|
|
|
<button
|
|
|
|
|
class="relative flex w-full justify-between gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer rounded-xl hover:bg-gray-50/40 dark:hover:bg-gray-800/40"
|
|
|
|
|
aria-pressed={skills[skillId].enabled}
|
|
|
|
|
on:click={async () => {
|
|
|
|
|
skills[skillId].enabled = !skills[skillId].enabled;
|
|
|
|
|
<SearchInput bind:value={skillQuery} placeholder={$i18n.t('Search skills')} />
|
|
|
|
|
|
|
|
|
|
const state = skills[skillId].enabled;
|
|
|
|
|
await tick();
|
|
|
|
|
|
|
|
|
|
if (state) {
|
|
|
|
|
selectedSkillIds = [...selectedSkillIds, skillId];
|
|
|
|
|
} else {
|
|
|
|
|
selectedSkillIds = selectedSkillIds.filter((id) => id !== skillId);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div class="flex-1 truncate">
|
|
|
|
|
<div class="flex flex-1 gap-2 items-center">
|
|
|
|
|
<Tooltip content={skills[skillId]?.name ?? ''} placement="top">
|
|
|
|
|
<div class="shrink-0">
|
|
|
|
|
<Cube className="size-3.5" strokeWidth="1.75" />
|
|
|
|
|
<div class="min-h-0 flex-1 overflow-y-auto overflow-x-hidden scrollbar-thin">
|
|
|
|
|
{#if skillIds.length === 0}
|
|
|
|
|
<div class="text-center text-xs text-gray-500 py-3">{$i18n.t('No skills found')}</div>
|
|
|
|
|
{:else}
|
|
|
|
|
<div class="flex flex-col gap-0.5">
|
|
|
|
|
{#each skillIds as skillId}
|
|
|
|
|
<button
|
|
|
|
|
class="relative flex w-full justify-between gap-2 items-center h-[1.6875rem] px-2 text-[0.8125rem] font-normal cursor-pointer rounded-xl hover:bg-gray-50/40 dark:hover:bg-gray-800/40"
|
|
|
|
|
aria-pressed={skills?.[skillId]?.enabled}
|
|
|
|
|
on:click={async () => {
|
|
|
|
|
await toggleSkill(skillId);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div class="flex-1 truncate">
|
|
|
|
|
<div class="flex flex-1 gap-2 items-center">
|
|
|
|
|
<Tooltip content={skills?.[skillId]?.name ?? ''} placement="top">
|
|
|
|
|
<div class="shrink-0">
|
|
|
|
|
<Cube className="size-3.5" strokeWidth="1.75" />
|
|
|
|
|
</div>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
<Tooltip
|
|
|
|
|
content={skills?.[skillId]?.description ?? ''}
|
|
|
|
|
placement="top-start"
|
|
|
|
|
>
|
|
|
|
|
<div class=" truncate">{skills?.[skillId]?.name}</div>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
<Tooltip content={skills[skillId]?.description ?? ''} placement="top-start">
|
|
|
|
|
<div class=" truncate">{skills[skillId].name}</div>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class=" shrink-0" inert>
|
|
|
|
|
<Switch state={skills[skillId].enabled} />
|
|
|
|
|
<div class=" shrink-0" inert>
|
|
|
|
|
<Switch state={skills?.[skillId]?.enabled} />
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
{/each}
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
</DropdownMenu>
|
|
|
|
|