This commit is contained in:
Timothy J. Baek 2024-10-02 20:42:10 -07:00
parent 1a26e67611
commit a2eadb30f5
7 changed files with 187 additions and 29 deletions

View File

@ -106,6 +106,13 @@ class FilesTable:
with get_db() as db: with get_db() as db:
return [FileModel.model_validate(file) for file in db.query(File).all()] return [FileModel.model_validate(file) for file in db.query(File).all()]
def get_files_by_ids(self, ids: list[str]) -> list[FileModel]:
with get_db() as db:
return [
FileModel.model_validate(file)
for file in db.query(File).filter(File.id.in_(ids)).all()
]
def get_files_by_user_id(self, user_id: str) -> list[FileModel]: def get_files_by_user_id(self, user_id: str) -> list[FileModel]:
with get_db() as db: with get_db() as db:
return [ return [

View File

@ -71,6 +71,12 @@ class KnowledgeForm(BaseModel):
data: Optional[dict] = None data: Optional[dict] = None
class KnowledgeUpdateForm(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
data: Optional[dict] = None
class KnowledgeTable: class KnowledgeTable:
def insert_new_knowledge( def insert_new_knowledge(
self, user_id: str, form_data: KnowledgeForm self, user_id: str, form_data: KnowledgeForm
@ -116,18 +122,37 @@ class KnowledgeTable:
return None return None
def update_knowledge_by_id( def update_knowledge_by_id(
self, id: str, form_data: KnowledgeForm self, id: str, form_data: KnowledgeUpdateForm, overwrite: bool = False
) -> Optional[KnowledgeModel]: ) -> Optional[KnowledgeModel]:
try: try:
with get_db() as db: with get_db() as db:
db.query(Knowledge).filter_by(id=id).update( db.query(Knowledge).filter_by(id=id).update(
{ {
"name": form_data.name, **({"name": form_data.name} if form_data.name else {}),
"updated_id": int(time.time()), **(
{"description": form_data.description}
if form_data.description
else {}
),
**(
{
"data": (
form_data.data
if overwrite
else {
**(self.get_knowledge_by_id(id=id)).data,
**form_data.data,
}
)
}
if form_data.data
else {}
),
"updated_at": int(time.time()),
} }
) )
db.commit() db.commit()
return self.get_knowledge_by_id(id=form_data.id) return self.get_knowledge_by_id(id=id)
except Exception as e: except Exception as e:
log.exception(e) log.exception(e)
return None return None

View File

@ -6,9 +6,12 @@ from pathlib import Path
from typing import Optional from typing import Optional
from open_webui.apps.webui.models.files import FileForm, FileModel, Files from open_webui.apps.webui.models.files import FileForm, FileModel, Files
from open_webui.apps.webui.models.knowledge import Knowledges
from open_webui.config import UPLOAD_DIR from open_webui.config import UPLOAD_DIR
from open_webui.constants import ERROR_MESSAGES from open_webui.constants import ERROR_MESSAGES
from open_webui.env import SRC_LOG_LEVELS from open_webui.env import SRC_LOG_LEVELS
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
from fastapi.responses import FileResponse, StreamingResponse from fastapi.responses import FileResponse, StreamingResponse
from open_webui.utils.utils import get_admin_user, get_verified_user from open_webui.utils.utils import get_admin_user, get_verified_user

View File

@ -6,10 +6,12 @@ from fastapi import APIRouter, Depends, HTTPException, status
from open_webui.apps.webui.models.knowledge import ( from open_webui.apps.webui.models.knowledge import (
Knowledges, Knowledges,
KnowledgeModel, KnowledgeUpdateForm,
KnowledgeForm, KnowledgeForm,
KnowledgeResponse, KnowledgeResponse,
) )
from open_webui.apps.webui.models.files import Files, FileModel
from open_webui.constants import ERROR_MESSAGES from open_webui.constants import ERROR_MESSAGES
from open_webui.utils.utils import get_admin_user, get_verified_user from open_webui.utils.utils import get_admin_user, get_verified_user
@ -66,12 +68,22 @@ async def create_new_knowledge(form_data: KnowledgeForm, user=Depends(get_admin_
############################ ############################
@router.get("/{id}", response_model=Optional[KnowledgeResponse]) class KnowledgeFilesResponse(KnowledgeResponse):
files: list[FileModel]
@router.get("/{id}", response_model=Optional[KnowledgeFilesResponse])
async def get_knowledge_by_id(id: str, user=Depends(get_verified_user)): async def get_knowledge_by_id(id: str, user=Depends(get_verified_user)):
knowledge = Knowledges.get_knowledge_by_id(id=id) knowledge = Knowledges.get_knowledge_by_id(id=id)
if knowledge: if knowledge:
return knowledge file_ids = knowledge.data.get("file_ids", []) if knowledge.data else []
files = Files.get_files_by_ids(file_ids)
return KnowledgeFilesResponse(
**knowledge.model_dump(),
files=files,
)
else: else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
@ -87,7 +99,7 @@ async def get_knowledge_by_id(id: str, user=Depends(get_verified_user)):
@router.post("/{id}/update", response_model=Optional[KnowledgeResponse]) @router.post("/{id}/update", response_model=Optional[KnowledgeResponse])
async def update_knowledge_by_id( async def update_knowledge_by_id(
id: str, id: str,
form_data: KnowledgeForm, form_data: KnowledgeUpdateForm,
user=Depends(get_admin_user), user=Depends(get_admin_user),
): ):
knowledge = Knowledges.update_knowledge_by_id(id=id, form_data=form_data) knowledge = Knowledges.update_knowledge_by_id(id=id, form_data=form_data)

View File

@ -95,13 +95,13 @@ export const getKnowledgeById = async (token: string, id: string) => {
return res; return res;
}; };
type KnowledgeForm = { type KnowledgeUpdateForm = {
name: string; name?: string;
description: string; description?: string;
data: object; data?: object;
}; };
export const updateKnowledgeById = async (token: string, id: string, form: KnowledgeForm) => { export const updateKnowledgeById = async (token: string, id: string, form: KnowledgeUpdateForm) => {
let error = null; let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/${id}/update`, { const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/${id}/update`, {
@ -112,9 +112,9 @@ export const updateKnowledgeById = async (token: string, id: string, form: Knowl
authorization: `Bearer ${token}` authorization: `Bearer ${token}`
}, },
body: JSON.stringify({ body: JSON.stringify({
name: form.name, name: form?.name ? form.name : undefined,
description: form.description, description: form?.description ? form.description : undefined,
data: form.data data: form?.data ? form.data : undefined
}) })
}) })
.then(async (res) => { .then(async (res) => {

View File

@ -0,0 +1,7 @@
<script lang="ts">
export let files = [];
</script>
<div>
{JSON.stringify(files)}
</div>

View File

@ -1,31 +1,80 @@
<script lang="ts"> <script lang="ts">
import { toast } from 'svelte-sonner';
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
import { PaneGroup, Pane, PaneResizer } from 'paneforge';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { mobile, showSidebar } from '$lib/stores'; import { mobile, showSidebar } from '$lib/stores';
import { getKnowledgeById } from '$lib/apis/knowledge'; import { uploadFile } from '$lib/apis/files';
import { getKnowledgeById, updateKnowledgeById } from '$lib/apis/knowledge';
import Spinner from '$lib/components/common/Spinner.svelte'; import Spinner from '$lib/components/common/Spinner.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
import EllipsisVertical from '$lib/components/icons/EllipsisVertical.svelte';
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
import BookOpen from '$lib/components/icons/BookOpen.svelte';
import Badge from '$lib/components/common/Badge.svelte'; import Badge from '$lib/components/common/Badge.svelte';
import Files from './Files.svelte'; import Files from './Files.svelte';
import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte'; import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
let largeScreen = true;
type Knowledge = {
id: string;
name: string;
description: string;
data: {
file_ids: string[];
};
files: any[];
};
let id = null; let id = null;
let knowledge = null; let knowledge: Knowledge | null = null;
let query = ''; let query = '';
let selectedFileId = null; let selectedFileId = null;
let debounceTimeout = null;
let dragged = false; let dragged = false;
let showAddContentModal = false;
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);
};
onMount(async () => { onMount(async () => {
// listen to resize 1024px
const mediaQuery = window.matchMedia('(min-width: 1024px)');
const handleMediaQuery = async (e) => {
if (e.matches) {
largeScreen = true;
} else {
largeScreen = false;
}
};
mediaQuery.addEventListener('change', handleMediaQuery);
handleMediaQuery(mediaQuery);
id = $page.params.id; id = $page.params.id;
const res = await getKnowledgeById(localStorage.token, id).catch((e) => { const res = await getKnowledgeById(localStorage.token, id).catch((e) => {
@ -37,6 +86,55 @@
} else { } else {
goto('/workspace/knowledge'); goto('/workspace/knowledge');
} }
const dropZone = document.querySelector('body');
const onDragOver = (e) => {
e.preventDefault();
dragged = true;
};
const onDragLeave = () => {
dragged = false;
};
const onDrop = async (e) => {
e.preventDefault();
if (e.dataTransfer?.files) {
let reader = new FileReader();
const inputFiles = e.dataTransfer?.files;
if (inputFiles && inputFiles.length > 0) {
for (const file of inputFiles) {
console.log(file, file.name.split('.').at(-1));
const uploadedFile = await uploadFile(localStorage.token, file).catch((e) => {
toast.error(e);
});
if (uploadedFile) {
knowledge.data.file_ids = [...(knowledge.data.file_ids ?? []), uploadedFile.id];
}
}
} else {
toast.error($i18n.t(`File not found.`));
}
}
dragged = false;
};
dropZone?.addEventListener('dragover', onDragOver);
dropZone?.addEventListener('drop', onDrop);
dropZone?.addEventListener('dragleave', onDragLeave);
return () => {
mediaQuery.removeEventListener('change', handleMediaQuery);
dropZone?.removeEventListener('dragover', onDragOver);
dropZone?.removeEventListener('drop', onDrop);
dropZone?.removeEventListener('dragleave', onDragLeave);
};
}); });
</script> </script>
@ -92,11 +190,14 @@
<div class=" flex w-full mt-1 mb-3.5"> <div class=" flex w-full mt-1 mb-3.5">
<div class="flex-1"> <div class="flex-1">
<div class="flex items-center justify-between w-full px-0.5 mb-1"> <div class="flex items-center justify-between w-full px-0.5 mb-1">
<div> <div class="w-full">
<input <input
type="text" type="text"
class="w-full font-medium text-2xl font-primary bg-transparent outline-none" class="w-full font-medium text-2xl font-primary bg-transparent outline-none"
bind:value={knowledge.name} bind:value={knowledge.name}
on:input={() => {
changeDebounceHandler();
}}
/> />
</div> </div>
@ -112,6 +213,9 @@
type="text" type="text"
class="w-full font-medium text-gray-500 text-sm bg-transparent outline-none" class="w-full font-medium text-gray-500 text-sm bg-transparent outline-none"
bind:value={knowledge.description} bind:value={knowledge.description}
on:input={() => {
changeDebounceHandler();
}}
/> />
</div> </div>
</div> </div>
@ -119,7 +223,7 @@
<div class="flex flex-row h-0 flex-1 overflow-auto"> <div class="flex flex-row h-0 flex-1 overflow-auto">
<div <div
class=" {!$mobile class=" {largeScreen
? 'flex-shrink-0' ? 'flex-shrink-0'
: 'flex-1'} p-2.5 w-80 rounded-2xl border border-gray-50 dark:border-gray-850" : 'flex-1'} p-2.5 w-80 rounded-2xl border border-gray-50 dark:border-gray-850"
> >
@ -148,9 +252,9 @@
<div> <div>
<Tooltip content={$i18n.t('Add Content')}> <Tooltip content={$i18n.t('Add Content')}>
<button <button
class=" px-2 py-2 rounded-xl border border-gray-200 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" class=" px-2 py-2 rounded-xl border border-gray-100 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={() => { on:click={() => {
goto('/workspace/knowledge/create'); showAddContentModal = true;
}} }}
> >
<svg <svg
@ -171,7 +275,7 @@
<div class="w-full h-full flex"> <div class="w-full h-full flex">
{#if (knowledge?.data?.file_ids ?? []).length > 0} {#if (knowledge?.data?.file_ids ?? []).length > 0}
<Files fileIds={knowledge.data.file_ids} /> <Files files={knowledge.files} />
{:else} {:else}
<div class="m-auto text-gray-500 text-xs">No content found</div> <div class="m-auto text-gray-500 text-xs">No content found</div>
{/if} {/if}
@ -179,8 +283,8 @@
</div> </div>
</div> </div>
{#if !$mobile} {#if largeScreen}
<div class="flex-1 p-1 flex justify-start h-full"> <div class="flex-1 p-2 flex justify-start h-full">
{#if selectedFileId} {#if selectedFileId}
<textarea /> <textarea />
{:else} {:else}