refac
This commit is contained in:
@@ -1882,7 +1882,12 @@ async def chat_completion_files_handler(
|
||||
__event_emitter__ = extra_params['__event_emitter__']
|
||||
sources = []
|
||||
|
||||
if files := body.get('metadata', {}).get('files', None):
|
||||
files = [
|
||||
item
|
||||
for item in (body.get('metadata', {}).get('files', None) or [])
|
||||
if item.get('type') != 'filesystem'
|
||||
]
|
||||
if files:
|
||||
# Check if all files are in full context mode
|
||||
all_full_context = all(item.get('context') == 'full' for item in files)
|
||||
|
||||
|
||||
@@ -11,6 +11,14 @@ export type TerminalFileList = {
|
||||
writable?: boolean;
|
||||
};
|
||||
|
||||
export type TerminalFileSearchResult = FileEntry & {
|
||||
path: string;
|
||||
};
|
||||
|
||||
export type TerminalFileSearchResponse = {
|
||||
results: TerminalFileSearchResult[];
|
||||
};
|
||||
|
||||
export type ListeningPort = {
|
||||
port: number;
|
||||
pid: number | null;
|
||||
@@ -38,6 +46,21 @@ const bearerHeaders = (apiKey: string): Record<string, string> => ({
|
||||
Authorization: `Bearer ${apiKey.trim()}`
|
||||
});
|
||||
|
||||
const joinTerminalPath = (base: string, child: string) => {
|
||||
if (!child) return base;
|
||||
if (child.startsWith('/') || /^[A-Za-z]:[\\/]/.test(child)) return child;
|
||||
return `${base.replace(/[\\/]+$/, '')}/${child.replace(/^[\\/]+/, '')}`;
|
||||
};
|
||||
|
||||
const basename = (path: string) =>
|
||||
path.replace(/\\/g, '/').split('/').filter(Boolean).at(-1) ?? path;
|
||||
|
||||
const hasHiddenPathPart = (path: string) =>
|
||||
path
|
||||
.replace(/\\/g, '/')
|
||||
.split('/')
|
||||
.some((part) => part.startsWith('.'));
|
||||
|
||||
export type TerminalServer = {
|
||||
id: string;
|
||||
url: string;
|
||||
@@ -112,6 +135,65 @@ export const listFiles = async (
|
||||
return res?.entries ? { entries: res.entries, writable: res.writable } : null;
|
||||
};
|
||||
|
||||
export const searchFiles = async (
|
||||
baseUrl: string,
|
||||
apiKey: string,
|
||||
query: string,
|
||||
path: string = '.',
|
||||
limit: number = 20,
|
||||
type: 'file' | 'directory' | 'any' = 'any',
|
||||
sessionId?: string,
|
||||
showHidden: boolean = false
|
||||
): Promise<TerminalFileSearchResponse | null> => {
|
||||
const headers: Record<string, string> = bearerHeaders(apiKey);
|
||||
if (sessionId) headers['X-Session-Id'] = sessionId;
|
||||
|
||||
const searchParams = new URLSearchParams({
|
||||
query,
|
||||
path,
|
||||
limit: String(limit),
|
||||
type,
|
||||
show_hidden: String(showHidden)
|
||||
});
|
||||
const base = baseUrl.replace(/\/$/, '');
|
||||
const searchRes = await fetch(`${base}/files/search?${searchParams.toString()}`, {
|
||||
headers
|
||||
}).catch(() => null);
|
||||
|
||||
if (searchRes?.ok) {
|
||||
const json = await searchRes.json().catch(() => null);
|
||||
if (Array.isArray(json?.results)) return { results: json.results };
|
||||
}
|
||||
|
||||
const globParams = new URLSearchParams({
|
||||
pattern: query.trim() ? `*${query.trim()}*` : '*',
|
||||
path,
|
||||
type,
|
||||
max_results: String(limit)
|
||||
});
|
||||
const globRes = await fetch(`${base}/files/glob?${globParams.toString()}`, {
|
||||
headers
|
||||
}).catch((err) => {
|
||||
console.error('open-terminal searchFiles error:', err);
|
||||
return null;
|
||||
});
|
||||
if (!globRes?.ok) return null;
|
||||
|
||||
const json = await globRes.json().catch(() => null);
|
||||
const root = json?.path ?? path;
|
||||
return {
|
||||
results: (json?.matches ?? [])
|
||||
.filter((item: FileEntry & { path: string }) => showHidden || !hasHiddenPathPart(item.path))
|
||||
.map((item: FileEntry & { path: string }) => ({
|
||||
path: joinTerminalPath(root, item.path),
|
||||
name: basename(item.path),
|
||||
type: item.type,
|
||||
size: item.size,
|
||||
modified: item.modified
|
||||
}))
|
||||
};
|
||||
};
|
||||
|
||||
export const readFile = async (
|
||||
baseUrl: string,
|
||||
apiKey: string,
|
||||
|
||||
@@ -1359,6 +1359,26 @@
|
||||
status: 'processed'
|
||||
}
|
||||
];
|
||||
} else if (type === 'filesystem') {
|
||||
const path = data.path ?? data.url ?? data.id;
|
||||
if (
|
||||
!path ||
|
||||
files.find((f) => f.type === 'filesystem' && (f.path ?? f.url ?? f.id) === path)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
files = [
|
||||
...files,
|
||||
{
|
||||
type: 'filesystem',
|
||||
id: path,
|
||||
path,
|
||||
url: path,
|
||||
name: data.name,
|
||||
size: data.size,
|
||||
status: 'processed'
|
||||
}
|
||||
];
|
||||
} else {
|
||||
if (files.find((f) => f.url === data || f.name === data)) {
|
||||
return;
|
||||
|
||||
@@ -88,8 +88,8 @@
|
||||
</script>
|
||||
|
||||
<div class={(filteredItems ?? []).length > 0 ? '' : 'hidden'} id="suggestions-container">
|
||||
<DropdownMenu className="w-72 font-sans text-xs">
|
||||
<div class="overflow-y-auto scrollbar-thin max-h-60">
|
||||
<DropdownMenu className="w-72 max-w-[calc(100vw-1rem)] overflow-x-hidden font-sans text-xs">
|
||||
<div class="max-h-60 overflow-y-auto overflow-x-hidden scrollbar-thin">
|
||||
{#if char === '/'}
|
||||
<SlashCommands
|
||||
bind:this={suggestionElement}
|
||||
@@ -192,6 +192,13 @@
|
||||
type: 'file',
|
||||
data: data
|
||||
});
|
||||
} else if (type === 'filesystem') {
|
||||
insertTextHandler('');
|
||||
|
||||
onUpload({
|
||||
type: 'filesystem',
|
||||
data: data
|
||||
});
|
||||
} else if (type === 'web') {
|
||||
insertTextHandler('');
|
||||
|
||||
|
||||
@@ -2,11 +2,20 @@
|
||||
import Fuse from 'fuse.js';
|
||||
import { getContext, onDestroy, onMount, tick } from 'svelte';
|
||||
|
||||
import { folders, models } from '$lib/stores';
|
||||
import {
|
||||
chatId as activeChatId,
|
||||
folders,
|
||||
models,
|
||||
selectedTerminalId,
|
||||
settings,
|
||||
terminalServers
|
||||
} from '$lib/stores';
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
import { getFolders } from '$lib/apis/folders';
|
||||
import { searchKnowledgeBases, searchKnowledgeFiles } from '$lib/apis/knowledge';
|
||||
import { searchFiles } from '$lib/apis/terminal';
|
||||
import { decodeString, isValidHttpUrl, isYoutubeUrl } from '$lib/utils';
|
||||
import { isTemporaryChatId } from '$lib/utils/chatId';
|
||||
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Database from '$lib/components/icons/Database.svelte';
|
||||
@@ -26,6 +35,7 @@
|
||||
let searchDebounceTimer: ReturnType<typeof setTimeout>;
|
||||
|
||||
let folderItems: any[] = [];
|
||||
let filesystemItems: any[] = [];
|
||||
let knowledgeItems: any[] = [];
|
||||
let fileItems: any[] = [];
|
||||
let modelItems: any[] = [];
|
||||
@@ -48,6 +58,7 @@
|
||||
|
||||
$: filteredModels = query ? fuse.search(query).map((e) => e.item) : modelItems;
|
||||
$: knowledgeResults = [
|
||||
...filesystemItems,
|
||||
...(query.startsWith('http')
|
||||
? isYoutubeUrl(query)
|
||||
? [{ type: 'youtube', name: query, description: query }]
|
||||
@@ -81,11 +92,68 @@
|
||||
});
|
||||
|
||||
const getItems = () => {
|
||||
const terminal = getSelectedTerminal();
|
||||
getFilesystemItems(terminal);
|
||||
getFolderItems();
|
||||
getKnowledgeItems();
|
||||
getKnowledgeFileItems();
|
||||
};
|
||||
|
||||
const getFilesystemItems = async (terminal = getSelectedTerminal()) => {
|
||||
if (!terminal) {
|
||||
filesystemItems = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await searchFiles(
|
||||
terminal.url,
|
||||
terminal.key,
|
||||
query,
|
||||
'.',
|
||||
20,
|
||||
'any',
|
||||
$activeChatId || undefined,
|
||||
localStorage.getItem('fileNav:showHidden') === 'true'
|
||||
).catch(() => null);
|
||||
|
||||
filesystemItems = (res?.results ?? []).map((item: any) => ({
|
||||
...item,
|
||||
type: 'filesystem',
|
||||
filesystem_type: item.type,
|
||||
id: item.path,
|
||||
url: item.path,
|
||||
name: item.name,
|
||||
description: item.path,
|
||||
status: 'processed'
|
||||
}));
|
||||
};
|
||||
|
||||
const chatContext = (terminal: any) => terminal?.contexts?.chat ?? {};
|
||||
|
||||
const getSelectedTerminal = (): { url: string; key: string } | null => {
|
||||
if (!$selectedTerminalId) return null;
|
||||
|
||||
const systemTerminal = ($terminalServers ?? []).find(
|
||||
(t) => t.id && t.id === $selectedTerminalId
|
||||
);
|
||||
const systemChatContext = chatContext(systemTerminal);
|
||||
if (systemTerminal) {
|
||||
if (
|
||||
systemChatContext === false ||
|
||||
(isTemporaryChatId($activeChatId) && systemChatContext?.context_id === 'chat_id')
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { url: systemTerminal.url, key: localStorage.token };
|
||||
}
|
||||
|
||||
const directTerminal = ($settings?.terminalServers ?? []).find(
|
||||
(t: any) => t.url === $selectedTerminalId && t.enabled
|
||||
);
|
||||
return directTerminal?.url ? { url: directTerminal.url, key: directTerminal.key ?? '' } : null;
|
||||
};
|
||||
|
||||
const getFolderItems = () => {
|
||||
folderItems = (($folders ?? []) as any[])
|
||||
.map((folder) => ({
|
||||
@@ -122,6 +190,11 @@
|
||||
};
|
||||
|
||||
const selectKnowledgeItem = (item: any) => {
|
||||
if (item.type === 'filesystem') {
|
||||
onSelect({ type: 'filesystem', data: item });
|
||||
return;
|
||||
}
|
||||
|
||||
if (['youtube', 'web'].includes(item.type)) {
|
||||
if (isValidHttpUrl(query)) {
|
||||
onSelect({ type: 'web', data: query });
|
||||
@@ -175,12 +248,14 @@
|
||||
{$i18n.t('Collections')}
|
||||
{:else if item?.type === 'file'}
|
||||
{$i18n.t('Files')}
|
||||
{:else if item?.type === 'filesystem'}
|
||||
{$i18n.t('Filesystem')}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="flex h-[1.6875rem] w-full items-center justify-between rounded-xl px-2 text-left text-[0.8125rem] hover:bg-gray-50/40 dark:hover:bg-gray-800/40 {itemIdx ===
|
||||
class="flex h-[1.6875rem] w-full max-w-full items-center justify-between overflow-hidden rounded-xl px-2 text-left text-[0.8125rem] hover:bg-gray-50/40 dark:hover:bg-gray-800/40 {itemIdx ===
|
||||
selectedIdx
|
||||
? 'bg-gray-50/40 dark:bg-gray-800/40 dark:text-gray-100 selected-command-option-button'
|
||||
: ''}"
|
||||
@@ -193,24 +268,29 @@
|
||||
}}
|
||||
data-selected={itemIdx === selectedIdx}
|
||||
>
|
||||
<div class="flex min-w-0 items-center gap-1.5 text-black dark:text-gray-100">
|
||||
<div
|
||||
class="flex w-full min-w-0 items-center gap-1.5 overflow-hidden text-black dark:text-gray-100"
|
||||
>
|
||||
<Tooltip
|
||||
className="shrink-0 flex"
|
||||
content={item?.legacy
|
||||
? $i18n.t('Legacy')
|
||||
: item?.type === 'file'
|
||||
? `${item?.collection?.name} > ${$i18n.t('File')}`
|
||||
: item?.type === 'collection'
|
||||
? $i18n.t('Collection')
|
||||
: item?.type === 'youtube'
|
||||
? $i18n.t('YouTube')
|
||||
: item?.type === 'web'
|
||||
? $i18n.t('Web')
|
||||
: ''}
|
||||
: item?.type === 'filesystem'
|
||||
? item?.path
|
||||
: item?.type === 'file'
|
||||
? `${item?.collection?.name} > ${$i18n.t('File')}`
|
||||
: item?.type === 'collection'
|
||||
? $i18n.t('Collection')
|
||||
: item?.type === 'youtube'
|
||||
? $i18n.t('YouTube')
|
||||
: item?.type === 'web'
|
||||
? $i18n.t('Web')
|
||||
: ''}
|
||||
placement="top"
|
||||
>
|
||||
{#if item?.type === 'collection'}
|
||||
<Database className="size-3.5" />
|
||||
{:else if item?.type === 'folder'}
|
||||
{:else if item?.type === 'folder' || item?.filesystem_type === 'directory'}
|
||||
<Folder className="size-3.5" />
|
||||
{:else if item?.type === 'youtube'}
|
||||
<Youtube className="size-3.5" />
|
||||
@@ -221,8 +301,12 @@
|
||||
{/if}
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={`${decodeString(item?.name)}`} placement="top-start">
|
||||
<div class="min-w-0 flex-1 truncate">
|
||||
<Tooltip
|
||||
className="min-w-0 flex-1"
|
||||
content={`${decodeString(item?.name)}`}
|
||||
placement="top-start"
|
||||
>
|
||||
<div class="line-clamp-1 min-w-0 overflow-hidden break-all">
|
||||
{decodeString(item?.name)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
Reference in New Issue
Block a user