From a9da0545722101c908998e2363e854fbae372ea0 Mon Sep 17 00:00:00 2001 From: "Mr. Meowgi" Date: Mon, 1 Jun 2026 22:53:30 +0300 Subject: [PATCH] feat: add skills management to chat component (#25037) - Introduced skills functionality in Chat.svelte, MessageInput.svelte, and related components. - Added SkillsModal for displaying and managing available skills. - Updated state management to include selectedSkillIds and integrate skills API. - Enhanced UI to show available skills and their descriptions. - Updated translations to support skills-related text. --- src/lib/components/chat/Chat.svelte | 34 +++++- src/lib/components/chat/MessageInput.svelte | 38 ++++++- .../chat/MessageInput/IntegrationsMenu.svelte | 100 ++++++++++++++++++ src/lib/components/chat/Placeholder.svelte | 2 + src/lib/components/chat/SkillsModal.svelte | 60 +++++++++++ src/lib/i18n/locales/en-US/translation.json | 2 + 6 files changed, 233 insertions(+), 3 deletions(-) create mode 100644 src/lib/components/chat/SkillsModal.svelte diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 6f47c1dadf..a1243020cf 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -38,6 +38,7 @@ showArtifacts, artifactContents, tools, + skills, toolServers, terminalServers, functions, @@ -92,6 +93,7 @@ getTaskIdsByChatId } from '$lib/apis'; import { getTools } from '$lib/apis/tools'; + import { getSkills } from '$lib/apis/skills'; import { uploadFile } from '$lib/apis/files'; import { createOpenAITextStream } from '$lib/apis/streaming'; import { getFunctions } from '$lib/apis/functions'; @@ -150,6 +152,7 @@ } let selectedToolIds = []; + let selectedSkillIds = []; let selectedFilterIds = []; let pendingOAuthTools = []; @@ -208,6 +211,7 @@ files = []; selectedToolIds = []; + selectedSkillIds = []; selectedFilterIds = []; webSearchEnabled = false; imageGenerationEnabled = false; @@ -243,6 +247,7 @@ messageInput?.setText(input.prompt); files = input.files; selectedToolIds = input.selectedToolIds; + selectedSkillIds = input.selectedSkillIds ?? []; selectedFilterIds = input.selectedFilterIds; webSearchEnabled = input.webSearchEnabled; imageGenerationEnabled = input.imageGenerationEnabled; @@ -303,6 +308,7 @@ const resetInput = async () => { selectedToolIds = []; + selectedSkillIds = []; selectedFilterIds = []; pendingOAuthTools = []; webSearchEnabled = false; @@ -329,6 +335,9 @@ if (!$functions) { functions.set(await getFunctions(localStorage.token)); } + if (!$skills) { + skills.set(await getSkills(localStorage.token)); + } if (selectedModels.length !== 1 && !atSelectedModel) { return; } @@ -366,6 +375,19 @@ selectedToolIds = selectedToolIds.filter((id) => !id.startsWith('direct_server:')); } + // Set Default Skills + if (model?.info?.meta?.skillIds) { + selectedSkillIds = [ + ...new Set( + [...(model?.info?.meta?.skillIds ?? [])].filter((id) => + ($skills ?? []).find((s) => s.id === id && s.is_active) + ) + ) + ]; + } else { + selectedSkillIds = []; + } + // Set Default Filters (Toggleable only) if (model?.info?.meta?.defaultFilterIds) { selectedFilterIds = model.info.meta.defaultFilterIds.filter((id) => @@ -830,6 +852,7 @@ files = []; selectedToolIds = []; + selectedSkillIds = []; selectedFilterIds = []; webSearchEnabled = false; imageGenerationEnabled = false; @@ -842,6 +865,7 @@ messageInput?.setText(input.prompt); files = input.files; selectedToolIds = input.selectedToolIds; + selectedSkillIds = input.selectedSkillIds ?? []; selectedFilterIds = input.selectedFilterIds; webSearchEnabled = input.webSearchEnabled; imageGenerationEnabled = input.imageGenerationEnabled; @@ -2365,11 +2389,15 @@ // Parse skill mentions (<$skillId|label>) from user messages const skillMentionRegex = /<\$([^|>]+)\|?[^>]*>/g; - const skillIds = []; + const skillIds = [...selectedSkillIds]; + const mentionSkillIds = []; for (const message of messages) { const content = typeof message.content === 'string' ? message.content : (message.content?.[0]?.text ?? ''); for (const match of content.matchAll(skillMentionRegex)) { + if (!mentionSkillIds.includes(match[1])) { + mentionSkillIds.push(match[1]); + } if (!skillIds.includes(match[1])) { skillIds.push(match[1]); } @@ -2377,7 +2405,7 @@ } // Strip skill mentions from message content - if (skillIds.length > 0) { + if (mentionSkillIds.length > 0) { messages = messages.map((message) => { if (typeof message.content === 'string') { return { @@ -3109,6 +3137,7 @@ bind:prompt bind:autoScroll bind:selectedToolIds + bind:selectedSkillIds bind:selectedFilterIds bind:imageGenerationEnabled bind:codeInterpreterEnabled @@ -3190,6 +3219,7 @@ bind:prompt bind:autoScroll bind:selectedToolIds + bind:selectedSkillIds bind:selectedFilterIds bind:imageGenerationEnabled bind:codeInterpreterEnabled diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 11cd749987..a9b5522a92 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -27,6 +27,7 @@ config, showCallOverlay, tools, + skills, toolServers, terminalServers, user as _user, @@ -58,6 +59,7 @@ import { getChatById } from '$lib/apis/chats'; import { getSessionUser } from '$lib/apis/auths'; import { getTools } from '$lib/apis/tools'; + import { getSkills } from '$lib/apis/skills'; import { WEBUI_BASE_URL, WEBUI_API_BASE_URL, PASTED_TEXT_CHARACTER_LIMIT } from '$lib/constants'; import { getOAuthClientAuthorizationUrl } from '$lib/apis/configs'; @@ -69,6 +71,7 @@ import VoiceRecording from './MessageInput/VoiceRecording.svelte'; import ToolServersModal from './ToolServersModal.svelte'; + import SkillsModal from './SkillsModal.svelte'; import RichTextInput from '../common/RichTextInput.svelte'; import Tooltip from '../common/Tooltip.svelte'; @@ -80,6 +83,7 @@ import GlobeAlt from '../icons/GlobeAlt.svelte'; import Photo from '../icons/Photo.svelte'; import Wrench from '../icons/Wrench.svelte'; + import Keyframes from '../icons/Keyframes.svelte'; import Sparkles from '../icons/Sparkles.svelte'; import InputVariablesModal from './MessageInput/InputVariablesModal.svelte'; @@ -131,6 +135,7 @@ export let files = []; export let selectedToolIds = []; + export let selectedSkillIds = []; export let selectedFilterIds = []; export let imageGenerationEnabled = false; @@ -176,6 +181,7 @@ }; }), selectedToolIds, + selectedSkillIds, selectedFilterIds, imageGenerationEnabled, webSearchEnabled, @@ -418,6 +424,7 @@ let suggestions = null; let showTools = false; + let showSkills = false; let loaded = false; let recording = false; @@ -508,6 +515,9 @@ let showToolsButton = false; $: showToolsButton = ($tools ?? []).length > 0 || ($toolServers ?? []).length > 0; + let showSkillsButton = false; + $: showSkillsButton = ($skills ?? []).some((skill) => skill.is_active); + let showWebSearchButton = false; $: showWebSearchButton = (atSelectedModel?.id ? [atSelectedModel.id] : selectedModels).length === @@ -1098,6 +1108,7 @@ } tools.set(await getTools(localStorage.token)); + skills.set(await getSkills(localStorage.token)); }; initialize(); @@ -1120,6 +1131,7 @@ + - {#if showWebSearchButton || showImageGenerationButton || showCodeInterpreterButton || showToolsButton || (toggleFilters && toggleFilters.length > 0)} + {#if showWebSearchButton || showImageGenerationButton || showCodeInterpreterButton || showToolsButton || showSkillsButton || (toggleFilters && toggleFilters.length > 0)}
@@ -1680,6 +1692,7 @@ {showImageGenerationButton} {showCodeInterpreterButton} bind:selectedToolIds + bind:selectedSkillIds bind:selectedFilterIds bind:webSearchEnabled bind:imageGenerationEnabled @@ -1751,6 +1764,29 @@ {/if} + {#if (selectedSkillIds ?? []).length > 0} + + + + {/if} + {#each selectedFilterIds as filterId (filterId)} {@const filter = toggleFilters.find((f) => f.id === filterId)} {#if filter} diff --git a/src/lib/components/chat/MessageInput/IntegrationsMenu.svelte b/src/lib/components/chat/MessageInput/IntegrationsMenu.svelte index 2fc2c4bc5f..5fc20b9046 100644 --- a/src/lib/components/chat/MessageInput/IntegrationsMenu.svelte +++ b/src/lib/components/chat/MessageInput/IntegrationsMenu.svelte @@ -6,6 +6,7 @@ config, user, tools as _tools, + skills as _skills, mobile, settings, toolServers, @@ -15,6 +16,7 @@ import { getOAuthClientAuthorizationUrl } from '$lib/apis/configs'; import { deleteOAuthSession } from '$lib/apis/auths'; import { getTools } from '$lib/apis/tools'; + import { getSkills } from '$lib/apis/skills'; import { toast } from 'svelte-sonner'; @@ -24,6 +26,7 @@ import Switch from '$lib/components/common/Switch.svelte'; import Spinner from '$lib/components/common/Spinner.svelte'; import Wrench from '$lib/components/icons/Wrench.svelte'; + import Keyframes from '$lib/components/icons/Keyframes.svelte'; import Sparkles from '$lib/components/icons/Sparkles.svelte'; import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte'; import Photo from '$lib/components/icons/Photo.svelte'; @@ -35,6 +38,7 @@ const i18n = getContext('i18n'); export let selectedToolIds: string[] = []; + export let selectedSkillIds: string[] = []; export let selectedModels: string[] = []; export let fileUploadCapableModels: string[] = []; @@ -58,6 +62,7 @@ let tab = ''; let tools = null; + let skills = null; $: if (show) { init(); @@ -99,6 +104,26 @@ } selectedToolIds = selectedToolIds.filter((id) => Object.keys(tools).includes(id)); + + if ($_skills === null) { + await _skills.set(await getSkills(localStorage.token)); + } + + 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; + }, {}); + } + + selectedSkillIds = selectedSkillIds.filter((id) => Object.keys(skills ?? {}).includes(id)); }; @@ -141,6 +166,28 @@
{/if} + + {#if skills && Object.keys(skills).length > 0} + + {/if} {:else}
@@ -439,6 +486,59 @@ {/each}
+ {:else if tab === 'skills' && skills} +
+ + + {#each Object.keys(skills) as skillId} + + {/each} +
{/if} diff --git a/src/lib/components/chat/Placeholder.svelte b/src/lib/components/chat/Placeholder.svelte index 8c998b357d..4577ff9b6b 100644 --- a/src/lib/components/chat/Placeholder.svelte +++ b/src/lib/components/chat/Placeholder.svelte @@ -47,6 +47,7 @@ export let messageInput = null; export let selectedToolIds = []; + export let selectedSkillIds = []; export let selectedFilterIds = []; export let pendingOAuthTools = []; @@ -217,6 +218,7 @@ bind:prompt bind:autoScroll bind:selectedToolIds + bind:selectedSkillIds bind:selectedFilterIds bind:imageGenerationEnabled bind:codeInterpreterEnabled diff --git a/src/lib/components/chat/SkillsModal.svelte b/src/lib/components/chat/SkillsModal.svelte new file mode 100644 index 0000000000..c251ff5905 --- /dev/null +++ b/src/lib/components/chat/SkillsModal.svelte @@ -0,0 +1,60 @@ + + + +
+
+
{$i18n.t('Available Skills')}
+ +
+ + {#if selectedSkills.length > 0} +
+
{$i18n.t('Skills')}
+
+ +
+
+ {#each selectedSkills as skill} + +
+
+ {skill?.name} +
+ + {#if skill?.description} +
+ {skill?.description} +
+ {/if} +
+
+ {/each} +
+
+ {/if} +
+
diff --git a/src/lib/i18n/locales/en-US/translation.json b/src/lib/i18n/locales/en-US/translation.json index 10768b186b..d5142ec5a2 100644 --- a/src/lib/i18n/locales/en-US/translation.json +++ b/src/lib/i18n/locales/en-US/translation.json @@ -10,6 +10,7 @@ "[Yesterday at] h:mm A": "", "{{ models }}": "", "{{COUNT}} Available Tools": "", + "{{COUNT}} Available Skills": "", "{{COUNT}} characters": "", "{{COUNT}} extracted lines": "", "{{COUNT}} files": "", @@ -240,6 +241,7 @@ "Available list": "", "Available models": "", "Available Tools": "", + "Available Skills": "", "available users": "", "available!": "", "Away": "",