Merge pull request #5861 from open-webui/projects
feat: knowledge/projects
This commit is contained in:
@@ -35,7 +35,7 @@
|
||||
{#each chatFiles as file, fileIdx}
|
||||
<FileItem
|
||||
className="w-full"
|
||||
{file}
|
||||
item={file}
|
||||
edit={true}
|
||||
url={`${file?.url}`}
|
||||
name={file.name}
|
||||
|
||||
@@ -125,16 +125,17 @@
|
||||
}
|
||||
|
||||
try {
|
||||
// During the file upload, file content is automatically extracted.
|
||||
const uploadedFile = await uploadFile(localStorage.token, file);
|
||||
|
||||
if (uploadedFile) {
|
||||
fileItem.status = 'uploaded';
|
||||
fileItem.status = 'processed';
|
||||
fileItem.file = uploadedFile;
|
||||
fileItem.id = uploadedFile.id;
|
||||
fileItem.collection_name = uploadedFile?.meta?.collection_name;
|
||||
fileItem.url = `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`;
|
||||
|
||||
// Try to extract content of the file for retrieval, even non-supported file types
|
||||
processFileItem(fileItem);
|
||||
files = files;
|
||||
} else {
|
||||
files = files.filter((item) => item.status !== null);
|
||||
}
|
||||
@@ -144,26 +145,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
const processFileItem = async (fileItem) => {
|
||||
try {
|
||||
const res = await processFile(localStorage.token, fileItem.id);
|
||||
if (res) {
|
||||
fileItem.status = 'processed';
|
||||
fileItem.collection_name = res.collection_name;
|
||||
fileItem.file = {
|
||||
...fileItem.file,
|
||||
content: res.content
|
||||
};
|
||||
|
||||
files = files;
|
||||
}
|
||||
} catch (e) {
|
||||
// We keep the file in the files list even if it fails to process
|
||||
fileItem.status = 'processed';
|
||||
files = files;
|
||||
}
|
||||
};
|
||||
|
||||
const inputFilesHandler = async (inputFiles) => {
|
||||
inputFiles.forEach((file) => {
|
||||
console.log(file, file.name.split('.').at(-1));
|
||||
@@ -456,7 +437,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<FileItem
|
||||
{file}
|
||||
item={file}
|
||||
name={file.name}
|
||||
type={file.type}
|
||||
size={file?.size}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import Prompts from './Commands/Prompts.svelte';
|
||||
import Documents from './Commands/Documents.svelte';
|
||||
import Knowledge from './Commands/Knowledge.svelte';
|
||||
import Models from './Commands/Models.svelte';
|
||||
|
||||
import { removeLastWordFromString } from '$lib/utils';
|
||||
@@ -97,7 +97,7 @@
|
||||
{#if command?.charAt(0) === '/'}
|
||||
<Prompts bind:this={commandElement} bind:prompt bind:files {command} />
|
||||
{:else if command?.charAt(0) === '#'}
|
||||
<Documents
|
||||
<Knowledge
|
||||
bind:this={commandElement}
|
||||
bind:prompt
|
||||
{command}
|
||||
@@ -114,7 +114,7 @@
|
||||
files = [
|
||||
...files,
|
||||
{
|
||||
type: e?.detail?.type ?? 'file',
|
||||
type: e?.detail?.meta?.document ? 'file' : 'collection',
|
||||
...e.detail,
|
||||
status: 'processed'
|
||||
}
|
||||
|
||||
+90
-75
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
import { documents } from '$lib/stores';
|
||||
import { removeLastWordFromString, isValidHttpUrl } from '$lib/utils';
|
||||
import { tick, getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import Fuse from 'fuse.js';
|
||||
|
||||
import { createEventDispatcher, tick, getContext, onMount } from 'svelte';
|
||||
import { removeLastWordFromString, isValidHttpUrl } from '$lib/utils';
|
||||
import { knowledge } from '$lib/stores';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -14,60 +14,22 @@
|
||||
const dispatch = createEventDispatcher();
|
||||
let selectedIdx = 0;
|
||||
|
||||
let items = [];
|
||||
let fuse = null;
|
||||
|
||||
let filteredItems = [];
|
||||
let filteredDocs = [];
|
||||
|
||||
let collections = [];
|
||||
|
||||
$: collections = [
|
||||
...($documents.length > 0
|
||||
? [
|
||||
{
|
||||
name: 'All Documents',
|
||||
type: 'collection',
|
||||
title: $i18n.t('All Documents'),
|
||||
collection_names: $documents.map((doc) => doc.collection_name)
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...$documents
|
||||
.reduce((a, e, i, arr) => {
|
||||
return [...new Set([...a, ...(e?.content?.tags ?? []).map((tag) => tag.name)])];
|
||||
}, [])
|
||||
.map((tag) => ({
|
||||
name: tag,
|
||||
type: 'collection',
|
||||
collection_names: $documents
|
||||
.filter((doc) => (doc?.content?.tags ?? []).map((tag) => tag.name).includes(tag))
|
||||
.map((doc) => doc.collection_name)
|
||||
}))
|
||||
];
|
||||
|
||||
$: filteredCollections = collections
|
||||
.filter((collection) => findByName(collection, command))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
$: filteredDocs = $documents
|
||||
.filter((doc) => findByName(doc, command))
|
||||
.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
$: filteredItems = [...filteredCollections, ...filteredDocs];
|
||||
$: if (fuse) {
|
||||
filteredItems = command.slice(1)
|
||||
? fuse.search(command).map((e) => {
|
||||
return e.item;
|
||||
})
|
||||
: items;
|
||||
}
|
||||
|
||||
$: if (command) {
|
||||
selectedIdx = 0;
|
||||
|
||||
console.log(filteredCollections);
|
||||
}
|
||||
|
||||
type ObjectWithName = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
const findByName = (obj: ObjectWithName, command: string) => {
|
||||
const name = obj.name.toLowerCase();
|
||||
return name.includes(command.toLowerCase().split(' ')?.at(0)?.substring(1) ?? '');
|
||||
};
|
||||
|
||||
export const selectUp = () => {
|
||||
selectedIdx = Math.max(0, selectedIdx - 1);
|
||||
};
|
||||
@@ -76,8 +38,8 @@
|
||||
selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1);
|
||||
};
|
||||
|
||||
const confirmSelect = async (doc) => {
|
||||
dispatch('select', doc);
|
||||
const confirmSelect = async (item) => {
|
||||
dispatch('select', item);
|
||||
|
||||
prompt = removeLastWordFromString(prompt, command);
|
||||
const chatInputElement = document.getElementById('chat-textarea');
|
||||
@@ -108,6 +70,48 @@
|
||||
chatInputElement?.focus();
|
||||
await tick();
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
let legacy_documents = $knowledge.filter((item) => item?.meta?.document);
|
||||
let legacy_collections =
|
||||
legacy_documents.length > 0
|
||||
? [
|
||||
{
|
||||
name: 'All Documents',
|
||||
legacy: true,
|
||||
type: 'collection',
|
||||
description: 'Deprecated (legacy collection), please create a new knowledge base.',
|
||||
title: $i18n.t('All Documents'),
|
||||
collection_names: legacy_documents.map((item) => item.id)
|
||||
},
|
||||
|
||||
...legacy_documents
|
||||
.reduce((a, item) => {
|
||||
return [...new Set([...a, ...(item?.meta?.tags ?? []).map((tag) => tag.name)])];
|
||||
}, [])
|
||||
.map((tag) => ({
|
||||
name: tag,
|
||||
legacy: true,
|
||||
type: 'collection',
|
||||
description: 'Deprecated (legacy collection), please create a new knowledge base.',
|
||||
collection_names: legacy_documents
|
||||
.filter((item) => (item?.meta?.tags ?? []).map((tag) => tag.name).includes(tag))
|
||||
.map((item) => item.id)
|
||||
}))
|
||||
]
|
||||
: [];
|
||||
|
||||
items = [...$knowledge, ...legacy_collections].map((item) => {
|
||||
return {
|
||||
...item,
|
||||
...{ legacy: item?.legacy ?? item?.meta?.document ?? undefined }
|
||||
};
|
||||
});
|
||||
|
||||
fuse = new Fuse(items, {
|
||||
keys: ['name', 'description']
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
||||
@@ -124,39 +128,50 @@
|
||||
class="max-h-60 flex flex-col w-full rounded-r-xl bg-white dark:bg-gray-900 dark:text-gray-100"
|
||||
>
|
||||
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5 scrollbar-hidden">
|
||||
{#each filteredItems as doc, docIdx}
|
||||
{#each filteredItems as item, idx}
|
||||
<button
|
||||
class=" px-3 py-1.5 rounded-xl w-full text-left {docIdx === selectedIdx
|
||||
class=" px-3 py-1.5 rounded-xl w-full text-left {idx === selectedIdx
|
||||
? ' bg-gray-50 dark:bg-gray-850 dark:text-gray-100 selected-command-option-button'
|
||||
: ''}"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
console.log(doc);
|
||||
|
||||
confirmSelect(doc);
|
||||
console.log(item);
|
||||
confirmSelect(item);
|
||||
}}
|
||||
on:mousemove={() => {
|
||||
selectedIdx = docIdx;
|
||||
selectedIdx = idx;
|
||||
}}
|
||||
on:focus={() => {}}
|
||||
>
|
||||
{#if doc.type === 'collection'}
|
||||
<div class=" font-medium text-black dark:text-gray-100 line-clamp-1">
|
||||
{doc?.title ?? `#${doc.name}`}
|
||||
</div>
|
||||
<div class=" font-medium text-black dark:text-gray-100 flex items-center gap-1">
|
||||
{#if item.legacy}
|
||||
<div
|
||||
class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded uppercase text-xs font-bold px-1"
|
||||
>
|
||||
Legacy
|
||||
</div>
|
||||
{:else if item?.meta?.document}
|
||||
<div
|
||||
class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded uppercase text-xs font-bold px-1"
|
||||
>
|
||||
Document
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="bg-green-500/20 text-green-700 dark:text-green-200 rounded uppercase text-xs font-bold px-1"
|
||||
>
|
||||
Collection
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class=" text-xs text-gray-600 dark:text-gray-100 line-clamp-1">
|
||||
{$i18n.t('Collection')}
|
||||
</div>
|
||||
{:else}
|
||||
<div class=" font-medium text-black dark:text-gray-100 line-clamp-1">
|
||||
#{doc.name} ({doc.filename})
|
||||
<div class="line-clamp-1">
|
||||
{item.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" text-xs text-gray-600 dark:text-gray-100 line-clamp-1">
|
||||
{doc.title}
|
||||
</div>
|
||||
{/if}
|
||||
<div class=" text-xs text-gray-600 dark:text-gray-100 line-clamp-1">
|
||||
{item?.description}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
<img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" />
|
||||
{:else}
|
||||
<FileItem
|
||||
{file}
|
||||
item={file}
|
||||
url={file.url}
|
||||
name={file.name}
|
||||
type={file.type}
|
||||
|
||||
Reference in New Issue
Block a user