Merge pull request #5861 from open-webui/projects

feat: knowledge/projects
This commit is contained in:
Timothy Jaeryang Baek
2024-10-04 10:00:47 +02:00
committed by GitHub
48 changed files with 2875 additions and 557 deletions
@@ -35,7 +35,7 @@
{#each chatFiles as file, fileIdx}
<FileItem
className="w-full"
{file}
item={file}
edit={true}
url={`${file?.url}`}
name={file.name}
+5 -24
View File
@@ -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'
}
@@ -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}