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

View File

@@ -92,6 +92,40 @@ export const getFileById = async (token: string, id: string) => {
return res;
};
export const updateFileDataContentById = async (token: string, id: string, content: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/files/${id}/data/content/update`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
},
body: JSON.stringify({
content: content
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const getFileContentById = async (id: string) => {
let error = null;

View File

@@ -0,0 +1,276 @@
import { WEBUI_API_BASE_URL } from '$lib/constants';
export const createNewKnowledge = async (token: string, name: string, description: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/create`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
},
body: JSON.stringify({
name: name,
description: description
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const getKnowledgeItems = async (token: string = '') => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const getKnowledgeById = async (token: string, id: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/${id}`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
type KnowledgeUpdateForm = {
name?: string;
description?: string;
data?: object;
};
export const updateKnowledgeById = async (token: string, id: string, form: KnowledgeUpdateForm) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/${id}/update`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
},
body: JSON.stringify({
name: form?.name ? form.name : undefined,
description: form?.description ? form.description : undefined,
data: form?.data ? form.data : undefined
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const addFileToKnowledgeById = async (token: string, id: string, fileId: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/${id}/file/add`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
},
body: JSON.stringify({
file_id: fileId
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const updateFileFromKnowledgeById = async (token: string, id: string, fileId: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/${id}/file/update`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
},
body: JSON.stringify({
file_id: fileId
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const removeFileFromKnowledgeById = async (token: string, id: string, fileId: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/${id}/file/remove`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
},
body: JSON.stringify({
file_id: fileId
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const deleteKnowledgeById = async (token: string, id: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/${id}/delete`, {
method: 'DELETE',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};

View File

@@ -306,7 +306,11 @@ export interface SearchDocument {
filenames: string[];
}
export const processFile = async (token: string, file_id: string) => {
export const processFile = async (
token: string,
file_id: string,
collection_name: string | null = null
) => {
let error = null;
const res = await fetch(`${RAG_API_BASE_URL}/process/file`, {
@@ -317,7 +321,8 @@ export const processFile = async (token: string, file_id: string) => {
authorization: `Bearer ${token}`
},
body: JSON.stringify({
file_id: file_id
file_id: file_id,
collection_name: collection_name ? collection_name : undefined
})
})
.then(async (res) => {

View File

@@ -1,10 +1,18 @@
<script>
import { getContext } from 'svelte';
export let title = '';
const i18n = getContext('i18n');
</script>
<div class=" text-center text-6xl mb-3">📄</div>
<div class="text-center dark:text-white text-2xl font-semibold z-50">{$i18n.t('Add Files')}</div>
<div class="text-center dark:text-white text-2xl font-semibold z-50">
{#if title}
{title}
{:else}
{$i18n.t('Add Files')}
{/if}
</div>
<slot
><div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">

View File

@@ -1,10 +1,10 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { onMount, getContext, createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import { getDocs } from '$lib/apis/documents';
import { deleteAllFiles, deleteFileById } from '$lib/apis/files';
import {
getQuerySettings,
processDocsDir,
@@ -18,11 +18,13 @@
getRAGConfig,
updateRAGConfig
} from '$lib/apis/retrieval';
import { knowledge, models } from '$lib/stores';
import { getKnowledgeItems } from '$lib/apis/knowledge';
import { deleteAllFiles, deleteFileById } from '$lib/apis/files';
import ResetUploadDirConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import ResetVectorDBConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import { documents, models } from '$lib/stores';
import { toast } from 'svelte-sonner';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
@@ -67,7 +69,7 @@
scanDirLoading = false;
if (res) {
await documents.set(await getDocs(localStorage.token));
await knowledge.set(await getKnowledgeItems(localStorage.token));
toast.success($i18n.t('Scan complete!'));
}
};

View File

@@ -2,7 +2,7 @@
import { getRAGConfig, updateRAGConfig } from '$lib/apis/retrieval';
import Switch from '$lib/components/common/Switch.svelte';
import { documents, models } from '$lib/stores';
import { models } from '$lib/stores';
import { onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';

View File

@@ -35,7 +35,7 @@
{#each chatFiles as file, fileIdx}
<FileItem
className="w-full"
{file}
item={file}
edit={true}
url={`${file?.url}`}
name={file.name}

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}

View File

@@ -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'
}

View File

@@ -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}

View File

@@ -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}

View File

@@ -0,0 +1,18 @@
<script lang="ts">
export let type = 'info';
export let content = '';
const classNames: Record<string, string> = {
info: 'bg-blue-500/20 text-blue-700 dark:text-blue-200 ',
success: 'bg-green-500/20 text-green-700 dark:text-green-200',
warning: 'bg-yellow-500/20 text-yellow-700 dark:text-yellow-200',
error: 'bg-red-500/20 text-red-700 dark:text-red-200'
};
</script>
<div
class=" text-xs font-bold {classNames[type] ??
classNames['info']} w-fit px-2 rounded uppercase line-clamp-1 mr-0.5"
>
{content}
</div>

View File

@@ -3,18 +3,19 @@
import { formatFileSize } from '$lib/utils';
import FileItemModal from './FileItemModal.svelte';
import GarbageBin from '../icons/GarbageBin.svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
export let className = 'w-72';
export let colorClassName = 'bg-white dark:bg-gray-800';
export let className = 'w-60';
export let colorClassName = 'bg-white dark:bg-gray-850 border border-gray-50 dark:border-white/5';
export let url: string | null = null;
export let dismissible = false;
export let status = 'processed';
export let file = null;
export let item = null;
export let edit = false;
export let name: string;
@@ -24,115 +25,113 @@
let showModal = false;
</script>
{#if file}
<FileItemModal bind:show={showModal} bind:file {edit} />
{#if item}
<FileItemModal bind:show={showModal} bind:item {edit} />
{/if}
<div class="relative group">
<button
class="h-14 {className} flex items-center space-x-3 {colorClassName} rounded-xl border border-gray-100 dark:border-gray-800 text-left"
type="button"
on:click={async () => {
if (file?.file?.content) {
showModal = !showModal;
} else {
if (url) {
if (type === 'file') {
window.open(`${url}/content`, '_blank').focus();
} else {
window.open(`${url}`, '_blank').focus();
}
<button
class="relative group p-1.5 {className} flex items-center {colorClassName} rounded-2xl text-left"
type="button"
on:click={async () => {
if (item?.file?.data?.content) {
showModal = !showModal;
} else {
if (url) {
if (type === 'file') {
window.open(`${url}/content`, '_blank').focus();
} else {
window.open(`${url}`, '_blank').focus();
}
}
}
dispatch('click');
}}
>
<div class="p-4 py-[1.1rem] bg-red-400 text-white rounded-l-xl">
{#if status === 'processed'}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class=" size-5"
>
<path
fill-rule="evenodd"
d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
clip-rule="evenodd"
/>
<path
d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
/>
</svg>
dispatch('click');
}}
>
<div class="p-3 bg-black/20 dark:bg-white/10 text-white rounded-xl">
{#if status === 'processed'}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class=" size-5"
>
<path
fill-rule="evenodd"
d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
clip-rule="evenodd"
/>
<path
d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
/>
</svg>
{:else}
<svg
class=" size-5 translate-y-[0.5px]"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_qM83 {
animation: spinner_8HQG 1.05s infinite;
}
.spinner_oXPr {
animation-delay: 0.1s;
}
.spinner_ZTLf {
animation-delay: 0.2s;
}
@keyframes spinner_8HQG {
0%,
57.14% {
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
transform: translate(0);
}
28.57% {
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
transform: translateY(-6px);
}
100% {
transform: translate(0);
}
}
</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
class="spinner_qM83 spinner_oXPr"
cx="12"
cy="12"
r="2.5"
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="2.5" /></svg
>
{/if}
</div>
<div class="flex flex-col justify-center -space-y-0.5 ml-1 px-2.5 w-full">
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1 mb-1">
{name}
</div>
<div class=" flex justify-between text-gray-500 text-xs">
{#if type === 'file'}
{$i18n.t('File')}
{:else if type === 'doc'}
{$i18n.t('Document')}
{:else if type === 'collection'}
{$i18n.t('Collection')}
{:else}
<svg
class=" size-5 translate-y-[0.5px]"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_qM83 {
animation: spinner_8HQG 1.05s infinite;
}
.spinner_oXPr {
animation-delay: 0.1s;
}
.spinner_ZTLf {
animation-delay: 0.2s;
}
@keyframes spinner_8HQG {
0%,
57.14% {
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
transform: translate(0);
}
28.57% {
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
transform: translateY(-6px);
}
100% {
transform: translate(0);
}
}
</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
class="spinner_qM83 spinner_oXPr"
cx="12"
cy="12"
r="2.5"
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="2.5" /></svg
>
<span class=" capitalize">{type}</span>
{/if}
{#if size}
<span class="capitalize">{formatFileSize(size)}</span>
{/if}
</div>
<div class="flex flex-col justify-center -space-y-0.5 pl-1.5 pr-4 w-full">
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1 mb-1">
{name}
</div>
<div class=" flex justify-between text-gray-500 text-xs">
{#if type === 'file'}
{$i18n.t('File')}
{:else if type === 'doc'}
{$i18n.t('Document')}
{:else if type === 'collection'}
{$i18n.t('Collection')}
{:else}
<span class=" capitalize">{type}</span>
{/if}
{#if size}
<span class="capitalize">{formatFileSize(size)}</span>
{/if}
</div>
</div>
</button>
</div>
{#if dismissible}
<div class=" absolute -top-1 -right-1">
<button
class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition"
type="button"
on:click={() => {
on:click|stopPropagation={() => {
dispatch('dismiss');
}}
>
@@ -147,6 +146,15 @@
/>
</svg>
</button>
<!-- <button
class=" p-1 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-full group-hover:visible invisible transition"
type="button"
on:click={() => {
}}
>
<GarbageBin />
</button> -->
</div>
{/if}
</div>
</button>

View File

@@ -10,7 +10,7 @@
import Switch from './Switch.svelte';
import Tooltip from './Tooltip.svelte';
export let file;
export let item;
export let show = false;
export let edit = false;
@@ -18,9 +18,9 @@
let enableFullContent = false;
onMount(() => {
console.log(file);
console.log(item);
if (file?.context === 'full') {
if (item?.context === 'full') {
enableFullContent = true;
}
});
@@ -33,11 +33,11 @@
<div>
<div class=" font-medium text-lg dark:text-gray-100">
<a
href={file.url ? (file.type === 'file' ? `${file.url}/content` : `${file.url}`) : '#'}
href={item.url ? (item.type === 'file' ? `${item.url}/content` : `${item.url}`) : '#'}
target="_blank"
class="hover:underline line-clamp-1"
>
{file?.name ?? 'File'}
{item?.name ?? 'File'}
</a>
</div>
</div>
@@ -56,14 +56,14 @@
<div>
<div class="flex flex-col items-center md:flex-row gap-1 justify-between w-full">
<div class=" flex flex-wrap text-sm gap-1 text-gray-500">
{#if file.size}
<div class="capitalize shrink-0">{formatFileSize(file.size)}</div>
{#if item.size}
<div class="capitalize shrink-0">{formatFileSize(item.size)}</div>
{/if}
{#if file?.file?.content}
{#if item?.file?.data?.content}
<div class="capitalize shrink-0">
{getLineCount(file?.file?.content ?? '')} extracted lines
{getLineCount(item?.file?.data?.content ?? '')} extracted lines
</div>
<div class="flex items-center gap-1 shrink-0">
@@ -90,7 +90,7 @@
<Switch
bind:state={enableFullContent}
on:change={(e) => {
file.context = e.detail ? 'full' : undefined;
item.context = e.detail ? 'full' : undefined;
}}
/>
</div>
@@ -102,7 +102,7 @@
</div>
<div class="max-h-96 overflow-scroll scrollbar-hidden text-xs whitespace-pre-wrap">
{file?.file?.content ?? 'No content'}
{item?.file?.data?.content ?? 'No content'}
</div>
</div>
</Modal>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
export let className = 'size-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3 4.5h14.25M3 9h9.75M3 13.5h5.25m5.25-.75L17.25 9m0 0L21 12.75M17.25 9v12"
/>
</svg>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
export let className = 'size-4';
export let strokeWidth = '2';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25"
/>
</svg>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
export let className = 'size-4';
export let strokeWidth = '1.5';
</script>
<svg
stroke="currentColor"
class={className}
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-width={strokeWidth}
d="M11 16h2m6.707-9.293-2.414-2.414A1 1 0 0 0 16.586 4H5a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1V7.414a1 1 0 0 0-.293-.707ZM16 20v-6a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1v6h8ZM9 4h6v3a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1V4Z"
/>
</svg>

View File

@@ -0,0 +1,195 @@
<script lang="ts">
import Fuse from 'fuse.js';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
import { toast } from 'svelte-sonner';
import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
import { WEBUI_NAME, knowledge } from '$lib/stores';
import { getKnowledgeItems, deleteKnowledgeById } from '$lib/apis/knowledge';
import { blobToFile, transformFileName } from '$lib/utils';
import { goto } from '$app/navigation';
import Tooltip from '../common/Tooltip.svelte';
import GarbageBin from '../icons/GarbageBin.svelte';
import Pencil from '../icons/Pencil.svelte';
import DeleteConfirmDialog from '../common/ConfirmDialog.svelte';
import ItemMenu from './Knowledge/ItemMenu.svelte';
let query = '';
let selectedItem = null;
let showDeleteConfirm = false;
let fuse = null;
let filteredItems = [];
$: if (fuse) {
filteredItems = query
? fuse.search(query).map((e) => {
return e.item;
})
: $knowledge;
}
const deleteHandler = async (item) => {
const res = await deleteKnowledgeById(localStorage.token, item.id).catch((e) => {
toast.error(e);
});
if (res) {
knowledge.set(await getKnowledgeItems(localStorage.token));
toast.success($i18n.t('Knowledge deleted successfully.'));
}
};
onMount(async () => {
knowledge.set(await getKnowledgeItems(localStorage.token));
knowledge.subscribe((value) => {
fuse = new Fuse(value, {
keys: ['name', 'description']
});
});
});
</script>
<svelte:head>
<title>
{$i18n.t('Knowledge')} | {$WEBUI_NAME}
</title>
</svelte:head>
<DeleteConfirmDialog
bind:show={showDeleteConfirm}
on:confirm={() => {
deleteHandler(selectedItem);
}}
/>
<div class="mb-3">
<div class="flex justify-between items-center">
<div class="flex md:self-center text-lg font-medium px-0.5">
{$i18n.t('Knowledge')}
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{$knowledge.length}</span>
</div>
</div>
</div>
<div class=" flex w-full space-x-2">
<div class="flex flex-1">
<div class=" self-center ml-1 mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
clip-rule="evenodd"
/>
</svg>
</div>
<input
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
bind:value={query}
placeholder={$i18n.t('Search Knowledge')}
/>
</div>
<div>
<button
class=" px-2 py-2 rounded-xl border border-gray-50 dark:border-gray-800 dark:border-0 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition font-medium text-sm flex items-center space-x-1"
aria-label={$i18n.t('Create Knowledge')}
on:click={() => {
goto('/workspace/knowledge/create');
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
/>
</svg>
</button>
</div>
</div>
<hr class=" border-gray-50 dark:border-gray-850 my-2.5" />
<div class="my-3 mb-5 grid lg:grid-cols-2 xl:grid-cols-3 gap-2">
{#each filteredItems as item}
<button
class=" flex space-x-4 cursor-pointer text-left w-full px-4 py-3 border border-gray-50 dark:border-gray-850 hover:bg-gray-50 dark:hover:bg-gray-850 transition rounded-xl"
on:click={() => {
if (item?.meta?.document) {
toast.error(
$i18n.t(
'Only collections can be edited, create a new knowledge base to edit/add documents.'
)
);
} else {
goto(`/workspace/knowledge/${item.id}`);
}
}}
>
<div class=" w-full">
<div class="flex items-center justify-between -mt-1">
<div class=" font-semibold line-clamp-1 h-fit">{item.name}</div>
<div class=" flex self-center">
<ItemMenu
on:delete={() => {
selectedItem = item;
showDeleteConfirm = true;
}}
/>
</div>
</div>
<div class=" self-center flex-1">
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
{item.description}
</div>
<div class="mt-5 flex justify-between">
<div>
{#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"
>
{$i18n.t('Document')}
</div>
{:else}
<div
class="bg-green-500/20 text-green-700 dark:text-green-200 rounded uppercase text-xs font-bold px-1"
>
{$i18n.t('Collection')}
</div>
{/if}
</div>
<div class=" text-xs text-gray-500">
Updated {dayjs(item.updated_at * 1000).fromNow()}
</div>
</div>
</div>
</div>
</button>
{/each}
</div>
<div class=" text-gray-500 text-xs mt-1 mb-2">
{$i18n.t("Use '#' in the prompt input to load and include your knowledge.")}
</div>

View File

@@ -0,0 +1,479 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { onMount, getContext, onDestroy } from 'svelte';
const i18n = getContext('i18n');
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { mobile, showSidebar } from '$lib/stores';
import { updateFileDataContentById, uploadFile } from '$lib/apis/files';
import {
addFileToKnowledgeById,
getKnowledgeById,
removeFileFromKnowledgeById,
updateFileFromKnowledgeById,
updateKnowledgeById
} from '$lib/apis/knowledge';
import Spinner from '$lib/components/common/Spinner.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Badge from '$lib/components/common/Badge.svelte';
import Files from './Collection/Files.svelte';
import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
import AddContentModal from './Collection/AddTextContentModal.svelte';
import { transcribeAudio } from '$lib/apis/audio';
import { blobToFile } from '$lib/utils';
import { processFile } from '$lib/apis/retrieval';
import AddContentMenu from './Collection/AddContentMenu.svelte';
import AddTextContentModal from './Collection/AddTextContentModal.svelte';
import Check from '$lib/components/icons/Check.svelte';
import FloppyDisk from '$lib/components/icons/FloppyDisk.svelte';
let largeScreen = true;
type Knowledge = {
id: string;
name: string;
description: string;
data: {
file_ids: string[];
};
files: any[];
};
let id = null;
let knowledge: Knowledge | null = null;
let query = '';
let showAddTextContentModal = false;
let inputFiles = null;
let selectedFile = null;
let selectedFileId = null;
$: if (selectedFileId) {
const file = knowledge.files.find((file) => file.id === selectedFileId);
if (file) {
file.data = file.data ?? { content: '' };
selectedFile = file;
}
} else {
selectedFile = null;
}
let debounceTimeout = null;
let mediaQuery;
let dragged = false;
const createFileFromText = (name, content) => {
const blob = new Blob([content], { type: 'text/plain' });
const file = blobToFile(blob, `${name}.md`);
console.log(file);
return file;
};
const uploadFileHandler = async (file) => {
console.log(file);
// Check if the file is an audio file and transcribe/convert it to text file
if (['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/x-m4a'].includes(file['type'])) {
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
toast.error(error);
return null;
});
if (res) {
console.log(res);
const blob = new Blob([res.text], { type: 'text/plain' });
file = blobToFile(blob, `${file.name}.txt`);
}
}
try {
const uploadedFile = await uploadFile(localStorage.token, file).catch((e) => {
toast.error(e);
});
if (uploadedFile) {
console.log(uploadedFile);
addFileHandler(uploadedFile.id);
} else {
toast.error($i18n.t('Failed to upload file.'));
}
} catch (e) {
toast.error(e);
}
};
const addFileHandler = async (fileId) => {
const updatedKnowledge = await addFileToKnowledgeById(localStorage.token, id, fileId).catch(
(e) => {
toast.error(e);
}
);
if (updatedKnowledge) {
knowledge = updatedKnowledge;
toast.success($i18n.t('File added successfully.'));
}
};
const deleteFileHandler = async (fileId) => {
const updatedKnowledge = await removeFileFromKnowledgeById(
localStorage.token,
id,
fileId
).catch((e) => {
toast.error(e);
});
if (updatedKnowledge) {
knowledge = updatedKnowledge;
toast.success($i18n.t('File removed successfully.'));
}
};
const updateFileContentHandler = async () => {
const fileId = selectedFile.id;
const content = selectedFile.data.content;
const res = updateFileDataContentById(localStorage.token, fileId, content).catch((e) => {
toast.error(e);
});
const updatedKnowledge = await updateFileFromKnowledgeById(
localStorage.token,
id,
fileId
).catch((e) => {
toast.error(e);
});
if (res && updatedKnowledge) {
knowledge = updatedKnowledge;
toast.success($i18n.t('File content updated successfully.'));
}
};
const changeDebounceHandler = () => {
console.log('debounce');
if (debounceTimeout) {
clearTimeout(debounceTimeout);
}
debounceTimeout = setTimeout(async () => {
const res = await updateKnowledgeById(localStorage.token, id, {
name: knowledge.name,
description: knowledge.description
}).catch((e) => {
toast.error(e);
});
if (res) {
toast.success($i18n.t('Knowledge updated successfully'));
}
}, 1000);
};
const handleMediaQuery = async (e) => {
if (e.matches) {
largeScreen = true;
} else {
largeScreen = false;
}
};
const onDragOver = (e) => {
e.preventDefault();
dragged = true;
};
const onDragLeave = () => {
dragged = false;
};
const onDrop = async (e) => {
e.preventDefault();
if (e.dataTransfer?.files) {
const inputFiles = e.dataTransfer?.files;
if (inputFiles && inputFiles.length > 0) {
for (const file of inputFiles) {
await uploadFileHandler(file);
}
} else {
toast.error($i18n.t(`File not found.`));
}
}
dragged = false;
};
onMount(async () => {
// listen to resize 1024px
mediaQuery = window.matchMedia('(min-width: 1024px)');
mediaQuery.addEventListener('change', handleMediaQuery);
handleMediaQuery(mediaQuery);
id = $page.params.id;
const res = await getKnowledgeById(localStorage.token, id).catch((e) => {
toast.error(e);
return null;
});
if (res) {
knowledge = res;
} else {
goto('/workspace/knowledge');
}
const dropZone = document.querySelector('body');
dropZone?.addEventListener('dragover', onDragOver);
dropZone?.addEventListener('drop', onDrop);
dropZone?.addEventListener('dragleave', onDragLeave);
});
onDestroy(() => {
mediaQuery?.removeEventListener('change', handleMediaQuery);
const dropZone = document.querySelector('body');
dropZone?.removeEventListener('dragover', onDragOver);
dropZone?.removeEventListener('drop', onDrop);
dropZone?.removeEventListener('dragleave', onDragLeave);
});
</script>
{#if dragged}
<div
class="fixed {$showSidebar
? 'left-0 md:left-[260px] md:w-[calc(100%-260px)]'
: 'left-0'} w-full h-full flex z-50 touch-none pointer-events-none"
id="dropzone"
role="region"
aria-label="Drag and Drop Container"
>
<div class="absolute w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
<div class="m-auto pt-64 flex flex-col justify-center">
<div class="max-w-md">
<AddFilesPlaceholder>
<div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
Drop any files here to add to my documents
</div>
</AddFilesPlaceholder>
</div>
</div>
</div>
</div>
{/if}
<AddTextContentModal
bind:show={showAddTextContentModal}
on:submit={(e) => {
const file = createFileFromText(e.detail.name, e.detail.content);
uploadFileHandler(file);
}}
/>
<input
id="files-input"
bind:files={inputFiles}
type="file"
multiple
hidden
on:change={() => {
if (inputFiles && inputFiles.length > 0) {
for (const file of inputFiles) {
uploadFileHandler(file);
}
inputFiles = null;
const fileInputElement = document.getElementById('files-input');
if (fileInputElement) {
fileInputElement.value = '';
}
} else {
toast.error($i18n.t(`File not found.`));
}
}}
/>
<div class="flex flex-col w-full max-h-[100dvh] h-full">
<button
class="flex space-x-1"
on:click={() => {
goto('/workspace/knowledge');
}}
>
<div class=" self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center font-medium text-sm">{$i18n.t('Back')}</div>
</button>
<div class="flex flex-col my-2 flex-1 overflow-auto h-0">
{#if id && knowledge}
<div class=" flex w-full mt-1 mb-3.5">
<div class="flex-1">
<div class="flex items-center justify-between w-full px-0.5 mb-1">
<div class="w-full">
<input
type="text"
class="w-full font-medium text-2xl font-primary bg-transparent outline-none"
bind:value={knowledge.name}
on:input={() => {
changeDebounceHandler();
}}
/>
</div>
<div class=" flex-shrink-0">
<div>
<Badge type="success" content="Collection" />
</div>
</div>
</div>
<div class="flex w-full px-1">
<input
type="text"
class="w-full text-gray-500 text-sm bg-transparent outline-none"
bind:value={knowledge.description}
on:input={() => {
changeDebounceHandler();
}}
/>
</div>
</div>
</div>
<div class="flex flex-row h-0 flex-1 overflow-auto">
<div
class=" {largeScreen
? 'flex-shrink-0'
: 'flex-1'} flex py-2.5 w-80 rounded-2xl border border-gray-50 dark:border-gray-850"
>
<div class=" flex flex-col w-full space-x-2 rounded-lg h-full">
<div class="w-full h-full flex flex-col">
<div class=" px-3">
<div class="flex">
<div class=" self-center ml-1 mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
clip-rule="evenodd"
/>
</svg>
</div>
<input
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
bind:value={query}
placeholder={$i18n.t('Search Collection')}
/>
<div>
<AddContentMenu
on:files={() => {
document.getElementById('files-input').click();
}}
on:text={() => {
showAddTextContentModal = true;
}}
/>
</div>
</div>
<hr class=" mt-2 mb-1 border-gray-50 dark:border-gray-850" />
</div>
{#if (knowledge?.files ?? []).length > 0}
<div class=" flex overflow-y-auto h-full w-full scrollbar-hidden text-xs">
<Files
files={knowledge.files}
{selectedFileId}
on:click={(e) => {
selectedFileId = e.detail;
}}
on:delete={(e) => {
console.log(e.detail);
selectedFileId = null;
deleteFileHandler(e.detail);
}}
/>
</div>
{:else}
<div class="m-auto text-gray-500 text-xs">No content found</div>
{/if}
</div>
</div>
</div>
{#if largeScreen}
<div class="flex-1 flex justify-start max-h-full overflow-hidden pl-3">
{#if selectedFile}
<div class=" flex flex-col w-full h-full">
<div class=" flex-shrink-0 mb-2 flex items-center">
<div class=" flex-1 text-xl line-clamp-1">
{selectedFile?.meta?.name}
</div>
<div>
<button
class="self-center w-fit text-sm py-1 px-2.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-lg"
on:click={() => {
updateFileContentHandler();
}}
>
{$i18n.t('Save')}
</button>
</div>
</div>
<div class=" flex-grow">
<textarea
class=" w-full h-full resize-none rounded-xl py-4 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={selectedFile.data.content}
placeholder={$i18n.t('Add content here')}
/>
</div>
</div>
{:else}
<div class="m-auto">
<AddFilesPlaceholder title={$i18n.t('Select/Add Files')}>
<div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
Select a file to view or drag and drop a file to upload
</div>
</AddFilesPlaceholder>
</div>
{/if}
</div>
{/if}
</div>
{:else}
<Spinner />
{/if}
</div>
</div>

View File

@@ -0,0 +1,86 @@
<script lang="ts">
import { DropdownMenu } from 'bits-ui';
import { flyAndScale } from '$lib/utils/transitions';
import { getContext, createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import Dropdown from '$lib/components/common/Dropdown.svelte';
import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
import Pencil from '$lib/components/icons/Pencil.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Tags from '$lib/components/chat/Tags.svelte';
import Share from '$lib/components/icons/Share.svelte';
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte';
import ArrowUpCircle from '$lib/components/icons/ArrowUpCircle.svelte';
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
import BarsArrowUp from '$lib/components/icons/BarsArrowUp.svelte';
const i18n = getContext('i18n');
export let onClose: Function = () => {};
let show = false;
</script>
<Dropdown
bind:show
on:change={(e) => {
if (e.detail === false) {
onClose();
}
}}
align="end"
>
<Tooltip content={$i18n.t('Add Content')}>
<button
class=" px-2 py-2 rounded-xl border border-gray-50 dark:border-gray-600 dark:border-0 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition font-medium text-sm flex items-center space-x-1"
on:click={(e) => {
e.stopPropagation();
show = true;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
/>
</svg>
</button>
</Tooltip>
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-44 rounded-xl p-1 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
sideOffset={4}
side="bottom"
align="end"
transition={flyAndScale}
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
dispatch('files');
}}
>
<ArrowUpCircle strokeWidth="2" />
<div class="flex items-center">{$i18n.t('Upload files')}</div>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
dispatch('text');
}}
>
<BarsArrowUp strokeWidth="2" />
<div class="flex items-center">{$i18n.t('Add text content')}</div>
</DropdownMenu.Item>
</DropdownMenu.Content>
</div>
</Dropdown>

View File

@@ -0,0 +1,118 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import dayjs from 'dayjs';
import { onMount, getContext, createEventDispatcher } from 'svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
import Modal from '$lib/components/common/Modal.svelte';
export let show = false;
let name = '';
let content = '';
</script>
<Modal size="md" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4">
<div class=" text-lg font-medium self-center">{$i18n.t('Add Content')}</div>
<button
class="self-center"
on:click={() => {
show = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
<div class="flex flex-col md:flex-row w-full px-5 py-4 md:space-x-4 dark:text-gray-200">
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
<form
class="flex flex-col w-full"
on:submit|preventDefault={() => {
dispatch('submit', {
name,
content
});
show = false;
name = '';
content = '';
}}
>
<div class="mb-3 w-full">
<div class="w-full flex flex-col gap-2.5">
<div class="w-full">
<div class=" text-sm mb-2">Title</div>
<div class="w-full mt-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-850 outline-none"
type="text"
bind:value={name}
placeholder={`Name your content`}
required
/>
</div>
</div>
<div>
<div class="text-sm mb-2">Content</div>
<div class=" w-full mt-1">
<textarea
class="w-full resize-none rounded-lg py-2 px-4 text-sm bg-whites dark:text-gray-300 dark:bg-gray-850 outline-none"
rows="10"
bind:value={content}
placeholder={`Write your content here`}
required
/>
</div>
</div>
</div>
</div>
<div class="flex justify-end text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit"
>
{$i18n.t('Add Content')}
</button>
</div>
</form>
</div>
</div>
</div>
</Modal>
<style>
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
/* display: none; <- Crashes Chrome on hover */
-webkit-appearance: none;
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
}
.tabs::-webkit-scrollbar {
display: none; /* for Chrome, Safari and Opera */
}
.tabs {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
input[type='number'] {
-moz-appearance: textfield; /* Firefox */
}
</style>

View File

@@ -0,0 +1,33 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import FileItem from '$lib/components/common/FileItem.svelte';
export let selectedFileId = null;
export let files = [];
</script>
<div class=" max-h-full flex flex-col w-full">
{#each files as file (file.id)}
<div class="mt-2 px-2">
<FileItem
className="w-full"
colorClassName="{selectedFileId === file.id
? ' bg-gray-50 dark:bg-gray-850'
: 'bg-transparent'} hover:bg-gray-50 dark:hover:bg-gray-850 transition"
{file}
name={file.meta.name}
type="file"
size={file.meta?.size ?? ''}
dismissible
on:click={() => {
dispatch('click', file.id);
}}
on:dismiss={() => {
dispatch('delete', file.id);
}}
/>
</div>
{/each}
</div>

View File

@@ -0,0 +1,138 @@
<script>
import { goto } from '$app/navigation';
import { getContext } from 'svelte';
const i18n = getContext('i18n');
import { createNewKnowledge, getKnowledgeItems } from '$lib/apis/knowledge';
import { toast } from 'svelte-sonner';
import { knowledge } from '$lib/stores';
let loading = false;
let name = '';
let description = '';
const submitHandler = async () => {
loading = true;
const res = await createNewKnowledge(localStorage.token, name, description).catch((e) => {
toast.error(e);
});
if (res) {
toast.success($i18n.t('Knowledge created successfully.'));
knowledge.set(await getKnowledgeItems(localStorage.token));
goto(`/workspace/knowledge/${res.id}`);
}
loading = false;
};
</script>
<div class="w-full max-h-full">
<button
class="flex space-x-1"
on:click={() => {
goto('/workspace/knowledge');
}}
>
<div class=" self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center font-medium text-sm">{$i18n.t('Back')}</div>
</button>
<form
class="flex flex-col max-w-lg mx-auto mt-10 mb-10"
on:submit|preventDefault={() => {
submitHandler();
}}
>
<div class=" w-full flex flex-col justify-center">
<div class=" text-2xl font-medium font-primary mb-2.5">Create a knowledge base</div>
<div class="w-full flex flex-col gap-2.5">
<div class="w-full">
<div class=" text-sm mb-2">What are you working on?</div>
<div class="w-full mt-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
type="text"
bind:value={name}
placeholder={`Name your knowledge base`}
required
/>
</div>
</div>
<div>
<div class="text-sm mb-2">What are you trying to achieve?</div>
<div class=" w-full mt-1">
<textarea
class="w-full resize-none rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
rows="4"
bind:value={description}
placeholder={`Describe your knowledge base and objectives`}
required
/>
</div>
</div>
</div>
</div>
<div class="flex justify-end mt-2">
<div>
<button
class=" text-sm px-4 py-2 transition rounded-lg {loading
? ' cursor-not-allowed bg-gray-100 dark:bg-gray-800'
: ' bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800'} flex"
type="submit"
disabled={loading}
>
<div class=" self-center font-medium">{$i18n.t('Create Knowledge')}</div>
{#if loading}
<div class="ml-1.5 self-center">
<svg
class=" w-4 h-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div>
{/if}
</button>
</div>
</div>
</form>
</div>

View File

@@ -0,0 +1,69 @@
<script lang="ts">
import { DropdownMenu } from 'bits-ui';
import { flyAndScale } from '$lib/utils/transitions';
import { getContext, createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import Dropdown from '$lib/components/common/Dropdown.svelte';
import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
import Pencil from '$lib/components/icons/Pencil.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Tags from '$lib/components/chat/Tags.svelte';
import Share from '$lib/components/icons/Share.svelte';
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte';
import ArrowUpCircle from '$lib/components/icons/ArrowUpCircle.svelte';
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
const i18n = getContext('i18n');
export let onClose: Function = () => {};
let show = false;
</script>
<Dropdown
bind:show
on:change={(e) => {
if (e.detail === false) {
onClose();
}
}}
align="end"
>
<Tooltip content={$i18n.t('More')}>
<slot
><button
class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
on:click={(e) => {
e.stopPropagation();
show = true;
}}
>
<EllipsisHorizontal className="size-5" />
</button>
</slot>
</Tooltip>
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[160px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
sideOffset={-2}
side="bottom"
align="end"
transition={flyAndScale}
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
dispatch('delete');
}}
>
<GarbageBin strokeWidth="2" />
<div class="flex items-center">{$i18n.t('Delete')}</div>
</DropdownMenu.Item>
</DropdownMenu.Content>
</div>
</Dropdown>

View File

@@ -1,9 +1,9 @@
<script lang="ts">
import { getContext } from 'svelte';
import Selector from './Knowledge/Selector.svelte';
import FileItem from '$lib/components/common/FileItem.svelte';
export let knowledge = [];
const i18n = getContext('i18n');
</script>
@@ -13,91 +13,44 @@
</div>
<div class=" text-xs dark:text-gray-500">
{$i18n.t('To add documents here, upload them to the "Documents" workspace first.')}
{$i18n.t('To attach knowledge base here, add them to the "Knowledge" workspace first.')}
</div>
<div class="flex flex-col">
{#if knowledge.length > 0}
{#if knowledge?.length > 0}
<div class=" flex items-center gap-2 mt-2">
{#each knowledge as file, fileIdx}
<div class=" relative group">
<div
class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none"
>
<div class="p-2.5 bg-red-400 text-white rounded-lg">
{#if (file?.type ?? 'doc') === 'doc'}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-6"
>
<path
fill-rule="evenodd"
d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
clip-rule="evenodd"
/>
<path
d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
/>
</svg>
{:else if file.type === 'collection'}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-6"
>
<path
d="M7.5 3.375c0-1.036.84-1.875 1.875-1.875h.375a3.75 3.75 0 0 1 3.75 3.75v1.875C13.5 8.161 14.34 9 15.375 9h1.875A3.75 3.75 0 0 1 21 12.75v3.375C21 17.16 20.16 18 19.125 18h-9.75A1.875 1.875 0 0 1 7.5 16.125V3.375Z"
/>
<path
d="M15 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 17.25 7.5h-1.875A.375.375 0 0 1 15 7.125V5.25ZM4.875 6H6v10.125A3.375 3.375 0 0 0 9.375 19.5H16.5v1.125c0 1.035-.84 1.875-1.875 1.875h-9.75A1.875 1.875 0 0 1 3 20.625V7.875C3 6.839 3.84 6 4.875 6Z"
/>
</svg>
{/if}
</div>
<div class="flex flex-col justify-center -space-y-0.5">
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
{file?.title ?? `#${file.name}`}
</div>
<div class=" text-gray-500 text-sm">{$i18n.t(file?.type ?? 'Document')}</div>
</div>
</div>
<div class=" absolute -top-1 -right-1">
<button
class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition"
type="button"
on:click={() => {
knowledge.splice(fileIdx, 1);
knowledge = knowledge;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
</div>
<FileItem
{file}
dismissible
on:dismiss={(e) => {
knowledge = knowledge.filter((_, idx) => idx !== fileIdx);
}}
/>
{/each}
</div>
{/if}
<div class="flex flex-wrap text-sm font-medium gap-1.5 mt-2">
<Selector bind:knowledge>
<Selector
bind:knowledge
on:select={(e) => {
const item = e.detail;
if (!knowledge.find((k) => k.name === item.name)) {
knowledge = [
...knowledge,
{
...item,
type: item?.type ?? 'doc'
}
];
}
}}
>
<button
class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-300 dark:outline-gray-800 rounded-3xl"
type="button">{$i18n.t('Select Documents')}</button
type="button">{$i18n.t('Select Knowledge')}</button
>
</Selector>
</div>

View File

@@ -1,46 +1,50 @@
<script lang="ts">
import { DropdownMenu } from 'bits-ui';
import { documents } from '$lib/stores';
import { onMount, getContext, createEventDispatcher } from 'svelte';
import { flyAndScale } from '$lib/utils/transitions';
import { knowledge } from '$lib/stores';
import Dropdown from '$lib/components/common/Dropdown.svelte';
import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
export let onClose: Function = () => {};
export let knowledge = [];
let items = [];
onMount(() => {
let collections = [
...($documents.length > 0
let legacy_documents = $knowledge.filter((item) => item?.meta?.document);
let legacy_collections =
legacy_documents.length > 0
? [
{
name: 'All Documents',
legacy: true,
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)
}))
];
description: 'Deprecated (legacy collection), please create a new knowledge base.',
items = [...collections, ...$documents];
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];
});
</script>
@@ -55,7 +59,7 @@
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[300px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
class="w-full max-w-80 rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
sideOffset={8}
side="bottom"
align="start"
@@ -64,64 +68,38 @@
<div class="max-h-[10rem] overflow-y-scroll">
{#if items.length === 0}
<div class="text-center text-sm text-gray-500 dark:text-gray-400">
{$i18n.t('No documents found')}
{$i18n.t('No knowledge found')}
</div>
{:else}
{#each items as item}
<DropdownMenu.Item
class="flex gap-2.5 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
if (!knowledge.find((k) => k.name === item.name)) {
knowledge = [
...knowledge,
{
...item,
type: item?.type ?? 'doc'
}
];
}
dispatch('select', item);
}}
>
<div class="flex self-start">
{#if (item?.type ?? 'doc') === 'doc'}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-4"
>
<path
fill-rule="evenodd"
d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
clip-rule="evenodd"
/>
<path
d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
/>
</svg>
{:else if item.type === 'collection'}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-4"
>
<path
d="M7.5 3.375c0-1.036.84-1.875 1.875-1.875h.375a3.75 3.75 0 0 1 3.75 3.75v1.875C13.5 8.161 14.34 9 15.375 9h1.875A3.75 3.75 0 0 1 21 12.75v3.375C21 17.16 20.16 18 19.125 18h-9.75A1.875 1.875 0 0 1 7.5 16.125V3.375Z"
/>
<path
d="M15 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 17.25 7.5h-1.875A.375.375 0 0 1 15 7.125V5.25ZM4.875 6H6v10.125A3.375 3.375 0 0 0 9.375 19.5H16.5v1.125c0 1.035-.84 1.875-1.875 1.875h-9.75A1.875 1.875 0 0 1 3 20.625V7.875C3 6.839 3.84 6 4.875 6Z"
/>
</svg>
{/if}
</div>
<div class="flex items-center">
<div class="flex flex-col">
<div
class=" w-fit text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
>
{item?.type ?? 'Document'}
<div class=" w-fit mb-0.5">
{#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>
<div class="line-clamp-1 font-medium pr-0.5">

View File

@@ -29,7 +29,7 @@ export const tags = writable([]);
export const models: Writable<Model[]> = writable([]);
export const prompts: Writable<Prompt[]> = writable([]);
export const documents: Writable<Document[]> = writable([]);
export const knowledge: Writable<Document[]> = writable([]);
export const tools = writable([]);
export const functions = writable([]);

View File

@@ -3,50 +3,46 @@
import { onMount, tick, getContext } from 'svelte';
import { openDB, deleteDB } from 'idb';
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import mermaid from 'mermaid';
const { saveAs } = fileSaver;
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { fade } from 'svelte/transition';
import { getKnowledgeItems } from '$lib/apis/knowledge';
import { getFunctions } from '$lib/apis/functions';
import { getModels as _getModels, getVersionUpdates } from '$lib/apis';
import { getAllChatTags } from '$lib/apis/chats';
import { getPrompts } from '$lib/apis/prompts';
import { getDocs } from '$lib/apis/documents';
import { getTools } from '$lib/apis/tools';
import { getBanners } from '$lib/apis/configs';
import { getUserSettings } from '$lib/apis/users';
import {
user,
showSettings,
settings,
models,
prompts,
documents,
tags,
banners,
showChangelog,
config,
showCallOverlay,
tools,
functions,
temporaryChatEnabled
} from '$lib/stores';
import SettingsModal from '$lib/components/chat/SettingsModal.svelte';
import Sidebar from '$lib/components/layout/Sidebar.svelte';
import ChangelogModal from '$lib/components/ChangelogModal.svelte';
import AccountPending from '$lib/components/layout/Overlay/AccountPending.svelte';
import { getFunctions } from '$lib/apis/functions';
import { page } from '$app/stores';
import { WEBUI_VERSION } from '$lib/constants';
import { compareVersion } from '$lib/utils';
import {
config,
user,
settings,
models,
prompts,
knowledge,
tools,
functions,
tags,
banners,
showSettings,
showChangelog,
temporaryChatEnabled
} from '$lib/stores';
import Sidebar from '$lib/components/layout/Sidebar.svelte';
import SettingsModal from '$lib/components/chat/SettingsModal.svelte';
import ChangelogModal from '$lib/components/ChangelogModal.svelte';
import AccountPending from '$lib/components/layout/Overlay/AccountPending.svelte';
import UpdateInfoToast from '$lib/components/layout/UpdateInfoToast.svelte';
import { fade } from 'svelte/transition';
const i18n = getContext('i18n');
@@ -109,7 +105,7 @@
prompts.set(await getPrompts(localStorage.token));
})(),
(async () => {
documents.set(await getDocs(localStorage.token));
knowledge.set(await getKnowledgeItems(localStorage.token));
})(),
(async () => {
tools.set(await getTools(localStorage.token));

View File

@@ -61,6 +61,17 @@
href="/workspace/models">{$i18n.t('Models')}</a
>
<a
class="min-w-fit rounded-lg p-1.5 px-3 {$page.url.pathname.includes(
'/workspace/knowledge'
)
? 'bg-gray-50 dark:bg-gray-850'
: ''} transition"
href="/workspace/knowledge"
>
{$i18n.t('Knowledge')}
</a>
<a
class="min-w-fit rounded-lg p-1.5 px-3 {$page.url.pathname.includes('/workspace/prompts')
? 'bg-gray-50 dark:bg-gray-850'
@@ -68,17 +79,6 @@
href="/workspace/prompts">{$i18n.t('Prompts')}</a
>
<a
class="min-w-fit rounded-lg p-1.5 px-3 {$page.url.pathname.includes(
'/workspace/documents'
)
? 'bg-gray-50 dark:bg-gray-850'
: ''} transition"
href="/workspace/documents"
>
{$i18n.t('Documents')}
</a>
<a
class="min-w-fit rounded-lg p-1.5 px-3 {$page.url.pathname.includes('/workspace/tools')
? 'bg-gray-50 dark:bg-gray-850'
@@ -101,7 +101,7 @@
</div>
</div>
<hr class=" my-2 dark:border-gray-850" />
<hr class=" my-2 border-gray-100 dark:border-gray-850" />
<div class=" py-1 px-5 flex-1 max-h-full overflow-y-auto">
<slot />

View File

@@ -1,5 +0,0 @@
<script>
import Documents from '$lib/components/workspace/Documents.svelte';
</script>
<Documents />

View File

@@ -0,0 +1,5 @@
<script>
import Knowledge from '$lib/components/workspace/Knowledge.svelte';
</script>
<Knowledge />

View File

@@ -0,0 +1,5 @@
<script>
import Collection from '$lib/components/workspace/Knowledge/Collection.svelte';
</script>
<Collection />

View File

@@ -0,0 +1,5 @@
<script>
import CreateCollection from '$lib/components/workspace/Knowledge/CreateCollection.svelte';
</script>
<CreateCollection />