From d5f9bbc7a7b3b610fdd41acb3c16ba6cfdc00c09 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 9 Jul 2025 01:17:25 +0400 Subject: [PATCH] enh: reference note in chat --- backend/open_webui/retrieval/utils.py | 11 ++++- backend/open_webui/routers/notes.py | 16 +++---- src/lib/apis/notes/index.ts | 37 +++++++++++++++- src/lib/components/chat/Chat.svelte | 5 +-- .../MessageInput/Commands/Knowledge.svelte | 42 +++++++++++++++---- src/lib/components/notes/NoteEditor.svelte | 5 ++- src/lib/components/notes/Notes.svelte | 4 +- .../components/notes/Notes/NoteMenu.svelte | 9 ++++ 8 files changed, 105 insertions(+), 24 deletions(-) diff --git a/backend/open_webui/retrieval/utils.py b/backend/open_webui/retrieval/utils.py index 0a0f0dabab..d0dc7ae57e 100644 --- a/backend/open_webui/retrieval/utils.py +++ b/backend/open_webui/retrieval/utils.py @@ -18,6 +18,7 @@ from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT from open_webui.models.users import UserModel from open_webui.models.files import Files +from open_webui.models.notes import Notes from open_webui.retrieval.vector.main import GetResult @@ -470,7 +471,15 @@ def get_sources_from_files( "documents": [[doc.get("content") for doc in file.get("docs")]], "metadatas": [[doc.get("metadata") for doc in file.get("docs")]], } - elif file.get("context") == "full": + elif file.get("type") == "note": + # Note Attached + note = Notes.get_note_by_id(file.get("id")) + + query_result = { + "documents": [[note.data.get("content", {}).get("md", "")]], + "metadatas": [[{"file_id": note.id, "name": note.title}]], + } + elif file.get("context") == "full" and file.get("type") == "file": # Manual Full Mode Toggle query_result = { "documents": [[file.get("file").get("data", {}).get("content")]], diff --git a/backend/open_webui/routers/notes.py b/backend/open_webui/routers/notes.py index 2cbbd331b5..068af0fd50 100644 --- a/backend/open_webui/routers/notes.py +++ b/backend/open_webui/routers/notes.py @@ -51,7 +51,14 @@ async def get_notes(request: Request, user=Depends(get_verified_user)): return notes -@router.get("/list", response_model=list[NoteUserResponse]) +class NoteTitleIdResponse(BaseModel): + id: str + title: str + updated_at: int + created_at: int + + +@router.get("/list", response_model=list[NoteTitleIdResponse]) async def get_note_list(request: Request, user=Depends(get_verified_user)): if user.role != "admin" and not has_permission( @@ -63,12 +70,7 @@ async def get_note_list(request: Request, user=Depends(get_verified_user)): ) notes = [ - NoteUserResponse( - **{ - **note.model_dump(), - "user": UserResponse(**Users.get_user_by_id(note.user_id).model_dump()), - } - ) + NoteTitleIdResponse(**note.model_dump()) for note in Notes.get_notes_by_user_id(user.id, "read") ] diff --git a/src/lib/apis/notes/index.ts b/src/lib/apis/notes/index.ts index df0be72627..965c4217ed 100644 --- a/src/lib/apis/notes/index.ts +++ b/src/lib/apis/notes/index.ts @@ -39,7 +39,7 @@ export const createNewNote = async (token: string, note: NoteItem) => { return res; }; -export const getNotes = async (token: string = '') => { +export const getNotes = async (token: string = '', raw: boolean = false) => { let error = null; const res = await fetch(`${WEBUI_API_BASE_URL}/notes/`, { @@ -67,6 +67,10 @@ export const getNotes = async (token: string = '') => { throw error; } + if (raw) { + return res; // Return raw response if requested + } + if (!Array.isArray(res)) { return {}; // or throw new Error("Notes response is not an array") } @@ -87,6 +91,37 @@ export const getNotes = async (token: string = '') => { return grouped; }; +export const getNoteList = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/notes/list`, { + 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.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + export const getNoteById = async (token: string, id: string) => { let error = null; diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 8029acb19b..8d733692e9 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -1597,9 +1597,8 @@ let files = JSON.parse(JSON.stringify(chatFiles)); files.push( ...(userMessage?.files ?? []).filter((item) => - ['doc', 'file', 'collection'].includes(item.type) - ), - ...(responseMessage?.files ?? []).filter((item) => ['web_search_results'].includes(item.type)) + ['doc', 'file', 'note', 'collection'].includes(item.type) + ) ); // Remove duplicates files = files.filter( diff --git a/src/lib/components/chat/MessageInput/Commands/Knowledge.svelte b/src/lib/components/chat/MessageInput/Commands/Knowledge.svelte index 760b49c057..81a3f43680 100644 --- a/src/lib/components/chat/MessageInput/Commands/Knowledge.svelte +++ b/src/lib/components/chat/MessageInput/Commands/Knowledge.svelte @@ -9,6 +9,7 @@ import { tick, getContext, onMount, onDestroy } from 'svelte'; import { removeLastWordFromString, isValidHttpUrl } from '$lib/utils'; import { knowledge } from '$lib/stores'; + import { getNoteList, getNotes } from '$lib/apis/notes'; const i18n = getContext('i18n'); @@ -75,10 +76,23 @@ } }; - onMount(() => { + onMount(async () => { window.addEventListener('resize', adjustHeight); adjustHeight(); + let notes = await getNoteList(localStorage.token).catch(() => { + return []; + }); + + notes = notes.map((note) => { + return { + ...note, + type: 'note', + name: note.title, + description: dayjs(note.updated_at / 1000000).fromNow() + }; + }); + let legacy_documents = $knowledge .filter((item) => item?.meta?.document) .map((item) => ({ @@ -144,14 +158,18 @@ ] : []; - items = [...collections, ...collection_files, ...legacy_collections, ...legacy_documents].map( - (item) => { - return { - ...item, - ...(item?.legacy || item?.meta?.legacy || item?.meta?.document ? { legacy: true } : {}) - }; - } - ); + items = [ + ...notes, + ...collections, + ...collection_files, + ...legacy_collections, + ...legacy_documents + ].map((item) => { + return { + ...item, + ...(item?.legacy || item?.meta?.legacy || item?.meta?.document ? { legacy: true } : {}) + }; + }); fuse = new Fuse(items, { keys: ['name', 'description'] @@ -210,6 +228,12 @@ > File + {:else if item?.type === 'note'} +
+ Note +
{:else}
{ console.log('downloadHandler', type); - if (type === 'md') { + if (type === 'txt') { + const blob = new Blob([note.data.content.md], { type: 'text/plain' }); + saveAs(blob, `${note.title}.txt`); + } else if (type === 'md') { const blob = new Blob([note.data.content.md], { type: 'text/markdown' }); saveAs(blob, `${note.title}.md`); } else if (type === 'pdf') { diff --git a/src/lib/components/notes/Notes.svelte b/src/lib/components/notes/Notes.svelte index b598e0d686..b6da80055c 100644 --- a/src/lib/components/notes/Notes.svelte +++ b/src/lib/components/notes/Notes.svelte @@ -302,7 +302,7 @@
{#each notes[timeRange] as note, idx (note.id)}
{#if note.data?.content?.md} {note.data?.content?.md} diff --git a/src/lib/components/notes/Notes/NoteMenu.svelte b/src/lib/components/notes/Notes/NoteMenu.svelte index b7d41438cb..9cb2d057c6 100644 --- a/src/lib/components/notes/Notes/NoteMenu.svelte +++ b/src/lib/components/notes/Notes/NoteMenu.svelte @@ -57,6 +57,15 @@ transition={flyAndScale} sideOffset={8} > + { + onDownload('txt'); + }} + > +
{$i18n.t('Plain text (.txt)')}
+
+ {