feat: $ skills mention
This commit is contained in:
@@ -2023,6 +2023,40 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Parse skill mentions (<$skillId|label>) from user messages
|
||||
const skillMentionRegex = /<\$([^|>]+)\|?[^>]*>/g;
|
||||
const skillIds = [];
|
||||
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 (!skillIds.includes(match[1])) {
|
||||
skillIds.push(match[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strip skill mentions from message content
|
||||
if (skillIds.length > 0) {
|
||||
messages = messages.map((message) => {
|
||||
if (typeof message.content === 'string') {
|
||||
return {
|
||||
...message,
|
||||
content: message.content.replace(/<\$[^>]+>/g, '').trim()
|
||||
};
|
||||
} else if (Array.isArray(message.content)) {
|
||||
return {
|
||||
...message,
|
||||
content: message.content.map((part) =>
|
||||
part.type === 'text'
|
||||
? { ...part, text: part.text.replace(/<\$[^>]+>/g, '').trim() }
|
||||
: part
|
||||
)
|
||||
};
|
||||
}
|
||||
return message;
|
||||
});
|
||||
}
|
||||
|
||||
const res = await generateOpenAIChatCompletion(
|
||||
localStorage.token,
|
||||
{
|
||||
@@ -2044,6 +2078,7 @@
|
||||
|
||||
filter_ids: selectedFilterIds.length > 0 ? selectedFilterIds : undefined,
|
||||
tool_ids: toolIds.length > 0 ? toolIds : undefined,
|
||||
skill_ids: skillIds.length > 0 ? skillIds : undefined,
|
||||
tool_servers: ($toolServers ?? []).filter(
|
||||
(server, idx) => toolServerIds.includes(idx) || toolServerIds.includes(server?.id)
|
||||
),
|
||||
|
||||
@@ -390,7 +390,7 @@
|
||||
|
||||
let command = '';
|
||||
export let showCommands = false;
|
||||
$: showCommands = ['/', '#', '@'].includes(command?.charAt(0)) || '\\#' === command?.slice(0, 2);
|
||||
$: showCommands = ['/', '#', '@', '$'].includes(command?.charAt(0)) || '\\#' === command?.slice(0, 2);
|
||||
let suggestions = null;
|
||||
|
||||
let showTools = false;
|
||||
@@ -946,6 +946,18 @@
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
char: '$',
|
||||
render: getSuggestionRenderer(CommandSuggestionList, {
|
||||
i18n,
|
||||
onSelect: (e) => {
|
||||
document.getElementById('chat-input')?.focus();
|
||||
},
|
||||
|
||||
insertTextHandler: insertTextAtCursor,
|
||||
onUpload: () => {}
|
||||
})
|
||||
}
|
||||
];
|
||||
loaded = true;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import Prompts from './Commands/Prompts.svelte';
|
||||
import Knowledge from './Commands/Knowledge.svelte';
|
||||
import Models from './Commands/Models.svelte';
|
||||
import Skills from './Commands/Skills.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
@@ -138,6 +139,27 @@
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{:else if char === '$'}
|
||||
<Skills
|
||||
bind:this={suggestionElement}
|
||||
{query}
|
||||
bind:filteredItems
|
||||
onSelect={(e) => {
|
||||
const { type, data } = e;
|
||||
|
||||
if (type === 'skill') {
|
||||
command({
|
||||
id: `${data.id}|${data.name}`,
|
||||
label: data.name
|
||||
});
|
||||
|
||||
onSelect({
|
||||
type: 'skill',
|
||||
data: data
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="py-4 flex flex-col w-full rounded-xl text-gray-700 dark:text-gray-300">
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
<script lang="ts">
|
||||
import Fuse from 'fuse.js';
|
||||
|
||||
import { getContext } from 'svelte';
|
||||
import { skills } from '$lib/stores';
|
||||
import { getSkillList } from '$lib/apis/skills';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Keyframes from '$lib/components/icons/Keyframes.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let query = '';
|
||||
export let onSelect = (e) => {};
|
||||
|
||||
let selectedIdx = 0;
|
||||
export let filteredItems = [];
|
||||
|
||||
let _skills = [];
|
||||
|
||||
const loadSkills = async () => {
|
||||
if ($skills) {
|
||||
_skills = $skills;
|
||||
} else {
|
||||
_skills = await getSkillList(localStorage.token);
|
||||
skills.set(_skills);
|
||||
}
|
||||
};
|
||||
|
||||
loadSkills();
|
||||
|
||||
$: fuse = new Fuse(
|
||||
(_skills ?? []).filter((s) => s.enabled !== false),
|
||||
{
|
||||
keys: ['name', 'id', 'meta.description'],
|
||||
threshold: 0.5
|
||||
}
|
||||
);
|
||||
|
||||
$: filteredItems = query
|
||||
? fuse.search(query).map((e) => e.item)
|
||||
: (_skills ?? []).filter((s) => s.enabled !== false);
|
||||
|
||||
$: if (query) {
|
||||
selectedIdx = 0;
|
||||
}
|
||||
|
||||
export const selectUp = () => {
|
||||
selectedIdx = Math.max(0, selectedIdx - 1);
|
||||
};
|
||||
|
||||
export const selectDown = () => {
|
||||
selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1);
|
||||
};
|
||||
|
||||
export const select = async () => {
|
||||
const skill = filteredItems[selectedIdx];
|
||||
if (skill) {
|
||||
onSelect({ type: 'skill', data: skill });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="px-2 text-xs text-gray-500 py-1">
|
||||
{$i18n.t('Skills')}
|
||||
</div>
|
||||
|
||||
{#if filteredItems.length > 0}
|
||||
{#each filteredItems as skill, skillIdx}
|
||||
<Tooltip content={skill.id} placement="top-start">
|
||||
<button
|
||||
class="px-2.5 py-1.5 rounded-xl w-full text-left {skillIdx === selectedIdx
|
||||
? 'bg-gray-50 dark:bg-gray-800 selected-command-option-button'
|
||||
: ''}"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
onSelect({ type: 'skill', data: skill });
|
||||
}}
|
||||
on:mousemove={() => {
|
||||
selectedIdx = skillIdx;
|
||||
}}
|
||||
on:focus={() => {}}
|
||||
data-selected={skillIdx === selectedIdx}
|
||||
>
|
||||
<div class="flex text-black dark:text-gray-100 line-clamp-1">
|
||||
<div class="flex items-center justify-center size-5 mr-2 shrink-0">
|
||||
<Keyframes className="size-4" />
|
||||
</div>
|
||||
<div class="truncate">
|
||||
{skill.name}
|
||||
</div>
|
||||
{#if skill.meta?.description}
|
||||
<div class="ml-2 text-xs text-gray-500 truncate">
|
||||
{skill.meta.description}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/each}
|
||||
{/if}
|
||||
@@ -46,7 +46,7 @@
|
||||
marked.use(footnoteExtension(options));
|
||||
marked.use(disableSingleTilde);
|
||||
marked.use({
|
||||
extensions: [mentionExtension({ triggerChar: '@' }), mentionExtension({ triggerChar: '#' })]
|
||||
extensions: [mentionExtension({ triggerChar: '@' }), mentionExtension({ triggerChar: '#' }), mentionExtension({ triggerChar: '$' })]
|
||||
});
|
||||
|
||||
$: (async () => {
|
||||
|
||||
@@ -25,8 +25,23 @@
|
||||
|
||||
const init = () => {
|
||||
const _id = token?.id;
|
||||
// split by : and take first part as idType and second part as id
|
||||
triggerChar = token?.triggerChar ?? '@';
|
||||
|
||||
if (triggerChar === '$') {
|
||||
// Skill mention: id format is "skillId|label"
|
||||
const pipeIdx = _id?.indexOf('|') ?? -1;
|
||||
if (pipeIdx > 0) {
|
||||
id = _id.substring(0, pipeIdx);
|
||||
label = _id.substring(pipeIdx + 1);
|
||||
} else {
|
||||
id = _id;
|
||||
label = _id;
|
||||
}
|
||||
idType = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// split by : and take first part as idType and second part as id
|
||||
const parts = _id?.split(':');
|
||||
if (parts) {
|
||||
idType = parts[0];
|
||||
@@ -37,7 +52,6 @@
|
||||
}
|
||||
|
||||
label = token?.label ?? id;
|
||||
triggerChar = token?.triggerChar ?? '@';
|
||||
|
||||
if (triggerChar === '#') {
|
||||
if (idType === 'C') {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
export let className = 'size-4';
|
||||
export let strokeWidth = '1.5';
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width={strokeWidth}
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class={className}
|
||||
>
|
||||
<path d="M16 5H19M22 5H19M19 5V2M19 5V8" />
|
||||
<path
|
||||
d="M16.8189 14.3287L11.4948 20.3183C10.6992 21.2134 9.30076 21.2134 8.50518 20.3183L3.18109 14.3287C2.50752 13.571 2.50752 12.429 3.18109 11.6713L8.50518 5.68167C9.30076 4.78664 10.6992 4.78664 11.4948 5.68167L16.8189 11.6713C17.4925 12.429 17.4925 13.571 16.8189 14.3287Z"
|
||||
/>
|
||||
</svg>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
export let className = 'size-4';
|
||||
export let strokeWidth = '1.5';
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width={strokeWidth}
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class={className}
|
||||
>
|
||||
<path
|
||||
d="M13.8476 13.317L9.50515 18.2798C8.70833 19.1905 7.29167 19.1905 6.49485 18.2798L2.15238 13.317C1.49259 12.563 1.49259 11.437 2.15238 10.683L6.49485 5.72018C7.29167 4.80952 8.70833 4.80952 9.50515 5.72017L13.8476 10.683C14.5074 11.437 14.5074 12.563 13.8476 13.317Z"
|
||||
/>
|
||||
<path d="M13 19L17.8844 13.3016C18.5263 12.5526 18.5263 11.4474 17.8844 10.6984L13 5" />
|
||||
<path d="M17 19L21.8844 13.3016C22.5263 12.5526 22.5263 11.4474 21.8844 10.6984L17 5" />
|
||||
</svg>
|
||||
Reference in New Issue
Block a user