Skip to content

Commit 9588c97

Browse files
committed
refac
1 parent eb11029 commit 9588c97

2 files changed

Lines changed: 179 additions & 2 deletions

File tree

src/lib/components/chat/MessageInput/CommandSuggestionList.svelte

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts">
2-
import Prompts from './Commands/Prompts.svelte';
2+
import SlashCommands from './Commands/SlashCommands.svelte';
33
import Knowledge from './Commands/Knowledge.svelte';
44
import Models from './Commands/Models.svelte';
55
import Skills from './Commands/Skills.svelte';
@@ -57,7 +57,7 @@
5757
<DropdownMenu className="w-72 font-sans text-xs">
5858
<div class="overflow-y-auto scrollbar-thin max-h-60">
5959
{#if char === '/'}
60-
<Prompts
60+
<SlashCommands
6161
bind:this={suggestionElement}
6262
{query}
6363
bind:filteredItems
@@ -66,6 +66,16 @@
6666

6767
if (type === 'prompt') {
6868
insertTextHandler(data.content);
69+
} else if (type === 'skill') {
70+
command({
71+
id: `${data.id}|${data.name}`,
72+
label: data.name
73+
});
74+
75+
onSelect({
76+
type: 'skill',
77+
data: data
78+
});
6979
}
7080
}}
7181
/>
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
<script lang="ts">
2+
import { getContext, onDestroy } from 'svelte';
3+
import { getPrompts } from '$lib/apis/prompts';
4+
import { getSkillItems } from '$lib/apis/skills';
5+
import Tooltip from '$lib/components/common/Tooltip.svelte';
6+
import Cube from '$lib/components/icons/Cube.svelte';
7+
8+
const i18n = getContext('i18n');
9+
10+
export let query = '';
11+
export let onSelect = (e) => {};
12+
13+
let selectedIdx = 0;
14+
export let filteredItems = [];
15+
16+
let prompts = [];
17+
let skills = [];
18+
let searchDebounceTimer: ReturnType<typeof setTimeout>;
19+
20+
$: filteredPrompts = prompts
21+
.filter((p) => p.command.toLowerCase().includes(query.toLowerCase()))
22+
.sort((a, b) => a.name.localeCompare(b.name));
23+
24+
$: filteredItems = [
25+
...filteredPrompts.map((data) => ({ type: 'prompt', data })),
26+
...skills.map((data) => ({ type: 'skill', data }))
27+
];
28+
29+
$: if (query) {
30+
selectedIdx = 0;
31+
}
32+
33+
$: selectedIdx = Math.min(selectedIdx, Math.max(filteredItems.length - 1, 0));
34+
35+
$: if (query !== undefined) {
36+
clearTimeout(searchDebounceTimer);
37+
searchDebounceTimer = setTimeout(() => {
38+
getItems();
39+
}, 200);
40+
}
41+
42+
onDestroy(() => {
43+
clearTimeout(searchDebounceTimer);
44+
});
45+
46+
const getItems = async () => {
47+
const [promptRes, skillRes] = await Promise.all([
48+
getPrompts(localStorage.token).catch(() => null),
49+
getSkillItems(localStorage.token, query).catch(() => null)
50+
]);
51+
52+
if (promptRes) {
53+
prompts = promptRes;
54+
}
55+
56+
skills = skillRes?.items ?? [];
57+
};
58+
59+
export const selectUp = () => {
60+
selectedIdx = Math.max(0, selectedIdx - 1);
61+
};
62+
63+
export const selectDown = () => {
64+
selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1);
65+
};
66+
67+
export const select = async () => {
68+
const item = filteredItems[selectedIdx];
69+
if (item) {
70+
onSelect(item);
71+
}
72+
};
73+
74+
const escapeTooltipText = (value = '') =>
75+
String(value)
76+
.replaceAll('&', '&amp;')
77+
.replaceAll('<', '&lt;')
78+
.replaceAll('>', '&gt;')
79+
.replaceAll('"', '&quot;')
80+
.replaceAll("'", '&#39;');
81+
82+
const getSkillTooltipContent = (skill) => {
83+
const name = escapeTooltipText(skill.name);
84+
const description = escapeTooltipText(skill.description);
85+
86+
return `<div class="max-w-80 whitespace-normal text-left leading-snug">
87+
<span class="break-words font-normal">${name}</span>${description ? `: <span class="break-words opacity-80">${description}</span>` : ''}
88+
</div>`;
89+
};
90+
</script>
91+
92+
{#if filteredPrompts.length > 0}
93+
<div class="px-2 py-1 text-[11px] text-gray-500 dark:text-gray-400">
94+
{$i18n.t('Prompts')}
95+
</div>
96+
97+
{#each filteredPrompts as promptItem, promptIdx}
98+
<Tooltip content={promptItem.name} placement="top-start">
99+
<button
100+
class="flex h-[1.6875rem] w-full items-center gap-1.5 rounded-xl px-2 text-left text-[13px] hover:bg-gray-50/40 dark:hover:bg-gray-800/40 {promptIdx ===
101+
selectedIdx
102+
? 'bg-gray-50/40 dark:bg-gray-800/40 selected-command-option-button'
103+
: ''}"
104+
type="button"
105+
on:click={() => {
106+
onSelect({ type: 'prompt', data: promptItem });
107+
}}
108+
on:mousemove={() => {
109+
selectedIdx = promptIdx;
110+
}}
111+
on:focus={() => {}}
112+
data-selected={promptIdx === selectedIdx}
113+
>
114+
<span class="shrink-0 font-normal text-black dark:text-gray-100">
115+
{promptItem.command}
116+
</span>
117+
118+
<span class="min-w-0 truncate text-xs text-gray-500 dark:text-gray-400">
119+
{promptItem.name}
120+
</span>
121+
</button>
122+
</Tooltip>
123+
{/each}
124+
{/if}
125+
126+
{#if skills.length > 0}
127+
<div class="px-2 py-1 text-[11px] text-gray-500 dark:text-gray-400">
128+
{$i18n.t('Skills')}
129+
</div>
130+
131+
{#each skills as skill, skillIdx}
132+
{@const itemIdx = filteredPrompts.length + skillIdx}
133+
<Tooltip
134+
content={getSkillTooltipContent(skill)}
135+
placement="top-start"
136+
tippyOptions={{ maxWidth: '20rem' }}
137+
>
138+
<button
139+
class="flex h-[1.6875rem] w-full items-center rounded-xl px-2 text-left text-[13px] hover:bg-gray-50/40 dark:hover:bg-gray-800/40 {itemIdx ===
140+
selectedIdx
141+
? 'bg-gray-50/40 dark:bg-gray-800/40 selected-command-option-button'
142+
: ''}"
143+
type="button"
144+
on:click={() => {
145+
onSelect({ type: 'skill', data: skill });
146+
}}
147+
on:mousemove={() => {
148+
selectedIdx = itemIdx;
149+
}}
150+
on:focus={() => {}}
151+
data-selected={itemIdx === selectedIdx}
152+
>
153+
<div class="flex w-full min-w-0 items-center text-black dark:text-gray-100">
154+
<div class="mr-2 flex size-4.5 shrink-0 items-center justify-center">
155+
<Cube className="size-3.5" />
156+
</div>
157+
<div class="truncate min-w-0 flex-1">
158+
{skill.name}
159+
</div>
160+
<div class="ml-2 max-w-24 shrink-0 truncate text-xs text-gray-500 dark:text-gray-400">
161+
{skill.id}
162+
</div>
163+
</div>
164+
</button>
165+
</Tooltip>
166+
{/each}
167+
{/if}

0 commit comments

Comments
 (0)