Skip to content

Commit e8b4c7f

Browse files
committed
refac
1 parent 9588c97 commit e8b4c7f

2 files changed

Lines changed: 294 additions & 7 deletions

File tree

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

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script lang="ts">
22
import SlashCommands from './Commands/SlashCommands.svelte';
3+
import AtCommands from './Commands/AtCommands.svelte';
34
import Knowledge from './Commands/Knowledge.svelte';
4-
import Models from './Commands/Models.svelte';
55
import Skills from './Commands/Skills.svelte';
66
import Emojis from './Commands/Emojis.svelte';
77
import DropdownMenu from '$lib/components/common/DropdownMenu.svelte';
@@ -10,12 +10,12 @@
1010
export let query = '';
1111
export let command: (payload: { id: string; label: string }) => void;
1212
13-
export let onSelect = (e) => {};
14-
export let onUpload = (e) => {};
15-
export let insertTextHandler = (text) => {};
13+
export let onSelect: (e: any) => void = () => {};
14+
export let onUpload: (e: any) => void = () => {};
15+
export let insertTextHandler: (text: string) => void = () => {};
1616
17-
let suggestionElement = null;
18-
let filteredItems = [];
17+
let suggestionElement: any = null;
18+
let filteredItems: any[] = [];
1919
2020
const onKeyDown = (event: KeyboardEvent) => {
2121
if (!['ArrowUp', 'ArrowDown', 'Enter', 'Tab', 'Escape'].includes(event.key)) return false;
@@ -105,7 +105,7 @@
105105
}}
106106
/>
107107
{:else if char === '@'}
108-
<Models
108+
<AtCommands
109109
bind:this={suggestionElement}
110110
{query}
111111
bind:filteredItems
@@ -119,6 +119,20 @@
119119
type: 'model',
120120
data: data
121121
});
122+
} else if (type === 'knowledge') {
123+
insertTextHandler('');
124+
125+
onUpload({
126+
type: 'file',
127+
data: data
128+
});
129+
} else if (type === 'web') {
130+
insertTextHandler('');
131+
132+
onUpload({
133+
type: 'web',
134+
data: data
135+
});
122136
}
123137
}}
124138
/>
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
<script lang="ts">
2+
import Fuse from 'fuse.js';
3+
import { getContext, onDestroy, onMount, tick } from 'svelte';
4+
5+
import { folders, models } from '$lib/stores';
6+
import { WEBUI_API_BASE_URL } from '$lib/constants';
7+
import { getFolders } from '$lib/apis/folders';
8+
import { searchKnowledgeBases, searchKnowledgeFiles } from '$lib/apis/knowledge';
9+
import { decodeString, isValidHttpUrl, isYoutubeUrl } from '$lib/utils';
10+
11+
import Tooltip from '$lib/components/common/Tooltip.svelte';
12+
import Database from '$lib/components/icons/Database.svelte';
13+
import DocumentPage from '$lib/components/icons/DocumentPage.svelte';
14+
import Folder from '$lib/components/icons/Folder.svelte';
15+
import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
16+
import Youtube from '$lib/components/icons/Youtube.svelte';
17+
import { toast } from 'svelte-sonner';
18+
19+
const i18n = getContext<any>('i18n');
20+
21+
export let query = '';
22+
export let onSelect: (e: any) => void = () => {};
23+
24+
let selectedIdx = 0;
25+
export let filteredItems: any[] = [];
26+
let searchDebounceTimer: ReturnType<typeof setTimeout>;
27+
28+
let folderItems: any[] = [];
29+
let knowledgeItems: any[] = [];
30+
let fileItems: any[] = [];
31+
let modelItems: any[] = [];
32+
let filteredModels: any[] = [];
33+
let knowledgeResults: any[] = [];
34+
35+
$: modelItems = (($models ?? []) as any[])
36+
.filter((model) => !model?.info?.meta?.hidden)
37+
.map((model) => ({
38+
...model,
39+
modelName: model?.name,
40+
tags: model?.info?.meta?.tags?.map((tag: any) => tag.name).join(' '),
41+
desc: model?.info?.meta?.description
42+
}));
43+
44+
$: fuse = new Fuse(modelItems, {
45+
keys: ['value', 'tags', 'modelName'],
46+
threshold: 0.5
47+
});
48+
49+
$: filteredModels = query ? fuse.search(query).map((e) => e.item) : modelItems;
50+
$: knowledgeResults = [
51+
...(query.startsWith('http')
52+
? isYoutubeUrl(query)
53+
? [{ type: 'youtube', name: query, description: query }]
54+
: [{ type: 'web', name: query, description: query }]
55+
: []),
56+
...folderItems,
57+
...knowledgeItems,
58+
...fileItems
59+
];
60+
61+
$: filteredItems = [
62+
...knowledgeResults.map((data) => ({ type: data.type, data })),
63+
...filteredModels.map((data) => ({ type: 'model', data }))
64+
];
65+
66+
$: if (query) {
67+
selectedIdx = 0;
68+
}
69+
70+
$: selectedIdx = Math.min(selectedIdx, Math.max(filteredItems.length - 1, 0));
71+
72+
$: if (query !== undefined) {
73+
clearTimeout(searchDebounceTimer);
74+
searchDebounceTimer = setTimeout(() => {
75+
getItems();
76+
}, 200);
77+
}
78+
79+
onDestroy(() => {
80+
clearTimeout(searchDebounceTimer);
81+
});
82+
83+
const getItems = () => {
84+
getFolderItems();
85+
getKnowledgeItems();
86+
getKnowledgeFileItems();
87+
};
88+
89+
const getFolderItems = () => {
90+
folderItems = (($folders ?? []) as any[])
91+
.map((folder) => ({
92+
...folder,
93+
type: 'folder',
94+
description: $i18n.t('Folder'),
95+
title: folder.name
96+
}))
97+
.filter((folder: any) => folder.name.toLowerCase().includes(query.toLowerCase()));
98+
};
99+
100+
const getKnowledgeItems = async () => {
101+
const res = await searchKnowledgeBases(localStorage.token, query).catch(() => null);
102+
103+
if (res) {
104+
knowledgeItems = res.items.map((item: any) => ({
105+
...item,
106+
type: 'collection'
107+
}));
108+
}
109+
};
110+
111+
const getKnowledgeFileItems = async () => {
112+
const res = await searchKnowledgeFiles(localStorage.token, query).catch(() => null);
113+
114+
if (res) {
115+
fileItems = res.items.map((item: any) => ({
116+
...item,
117+
type: 'file',
118+
name: item.filename,
119+
description: item.collection ? item.collection.name : ''
120+
}));
121+
}
122+
};
123+
124+
const selectKnowledgeItem = (item: any) => {
125+
if (['youtube', 'web'].includes(item.type)) {
126+
if (isValidHttpUrl(query)) {
127+
onSelect({ type: 'web', data: query });
128+
} else {
129+
toast.error(
130+
$i18n.t('Oops! Looks like the URL is invalid. Please double-check and try again.')
131+
);
132+
}
133+
return;
134+
}
135+
136+
onSelect({ type: 'knowledge', data: item });
137+
};
138+
139+
export const selectUp = () => {
140+
selectedIdx = Math.max(0, selectedIdx - 1);
141+
};
142+
143+
export const selectDown = () => {
144+
selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1);
145+
};
146+
147+
export const select = async () => {
148+
const item = filteredItems[selectedIdx];
149+
if (!item) return;
150+
151+
if (item.type === 'model') {
152+
onSelect({ type: 'model', data: item.data });
153+
} else {
154+
selectKnowledgeItem(item.data);
155+
}
156+
};
157+
158+
onMount(async () => {
159+
if ($folders === null) {
160+
await folders.set(await getFolders(localStorage.token));
161+
}
162+
163+
await tick();
164+
});
165+
</script>
166+
167+
{#if knowledgeResults.length > 0 || query.startsWith('http')}
168+
{#each knowledgeResults as item, idx}
169+
{@const itemIdx = idx}
170+
{#if idx === 0 || item?.type !== knowledgeResults[idx - 1]?.type}
171+
<div class="px-2 py-1 text-[11px] text-gray-500 dark:text-gray-400">
172+
{#if item?.type === 'folder'}
173+
{$i18n.t('Folders')}
174+
{:else if item?.type === 'collection'}
175+
{$i18n.t('Collections')}
176+
{:else if item?.type === 'file'}
177+
{$i18n.t('Files')}
178+
{/if}
179+
</div>
180+
{/if}
181+
182+
<button
183+
class="flex h-[1.6875rem] w-full items-center justify-between rounded-xl px-2 text-left text-[13px] hover:bg-gray-50/40 dark:hover:bg-gray-800/40 {itemIdx ===
184+
selectedIdx
185+
? 'bg-gray-50/40 dark:bg-gray-800/40 dark:text-gray-100 selected-command-option-button'
186+
: ''}"
187+
type="button"
188+
on:click={() => {
189+
selectKnowledgeItem(item);
190+
}}
191+
on:mousemove={() => {
192+
selectedIdx = itemIdx;
193+
}}
194+
data-selected={itemIdx === selectedIdx}
195+
>
196+
<div class="flex min-w-0 items-center gap-1.5 text-black dark:text-gray-100">
197+
<Tooltip
198+
content={item?.legacy
199+
? $i18n.t('Legacy')
200+
: item?.type === 'file'
201+
? `${item?.collection?.name} > ${$i18n.t('File')}`
202+
: item?.type === 'collection'
203+
? $i18n.t('Collection')
204+
: item?.type === 'youtube'
205+
? $i18n.t('YouTube')
206+
: item?.type === 'web'
207+
? $i18n.t('Web')
208+
: ''}
209+
placement="top"
210+
>
211+
{#if item?.type === 'collection'}
212+
<Database className="size-3.5" />
213+
{:else if item?.type === 'folder'}
214+
<Folder className="size-3.5" />
215+
{:else if item?.type === 'youtube'}
216+
<Youtube className="size-3.5" />
217+
{:else if item?.type === 'web'}
218+
<GlobeAlt className="size-3.5" />
219+
{:else}
220+
<DocumentPage className="size-3.5" />
221+
{/if}
222+
</Tooltip>
223+
224+
<Tooltip content={`${decodeString(item?.name)}`} placement="top-start">
225+
<div class="min-w-0 flex-1 truncate">
226+
{decodeString(item?.name)}
227+
</div>
228+
</Tooltip>
229+
</div>
230+
</button>
231+
{/each}
232+
{/if}
233+
234+
{#if filteredModels.length > 0}
235+
<div class="px-2 py-1 text-[11px] text-gray-500 dark:text-gray-400">
236+
{$i18n.t('Models')}
237+
</div>
238+
239+
{#each filteredModels as model, modelIdx}
240+
{@const itemIdx = knowledgeResults.length + modelIdx}
241+
<Tooltip content={model.id} placement="top-start">
242+
<button
243+
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 ===
244+
selectedIdx
245+
? 'bg-gray-50/40 dark:bg-gray-800/40 selected-command-option-button'
246+
: ''}"
247+
type="button"
248+
on:click={() => {
249+
onSelect({ type: 'model', data: model });
250+
}}
251+
on:mousemove={() => {
252+
selectedIdx = itemIdx;
253+
}}
254+
on:focus={() => {}}
255+
data-selected={itemIdx === selectedIdx}
256+
>
257+
<div class="flex min-w-0 items-center text-black dark:text-gray-100">
258+
<img
259+
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model.id}&lang=${$i18n.language}`}
260+
alt={model?.name ?? model.id}
261+
class="mr-2 size-4.5 rounded-full object-cover"
262+
on:error={(e) => {
263+
(e.currentTarget as HTMLImageElement).src = '/favicon.png';
264+
}}
265+
/>
266+
<div class="min-w-0 truncate">
267+
{model.name}
268+
</div>
269+
</div>
270+
</button>
271+
</Tooltip>
272+
{/each}
273+
{/if}

0 commit comments

Comments
 (0)