enh: reference note in chat

This commit is contained in:
Timothy Jaeryang Baek 2025-07-09 01:17:25 +04:00
parent f2ee99d760
commit d5f9bbc7a7
8 changed files with 105 additions and 24 deletions

View file

@ -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.users import UserModel
from open_webui.models.files import Files from open_webui.models.files import Files
from open_webui.models.notes import Notes
from open_webui.retrieval.vector.main import GetResult 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")]], "documents": [[doc.get("content") for doc in file.get("docs")]],
"metadatas": [[doc.get("metadata") 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 # Manual Full Mode Toggle
query_result = { query_result = {
"documents": [[file.get("file").get("data", {}).get("content")]], "documents": [[file.get("file").get("data", {}).get("content")]],

View file

@ -51,7 +51,14 @@ async def get_notes(request: Request, user=Depends(get_verified_user)):
return notes 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)): async def get_note_list(request: Request, user=Depends(get_verified_user)):
if user.role != "admin" and not has_permission( 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 = [ notes = [
NoteUserResponse( NoteTitleIdResponse(**note.model_dump())
**{
**note.model_dump(),
"user": UserResponse(**Users.get_user_by_id(note.user_id).model_dump()),
}
)
for note in Notes.get_notes_by_user_id(user.id, "read") for note in Notes.get_notes_by_user_id(user.id, "read")
] ]

View file

@ -39,7 +39,7 @@ export const createNewNote = async (token: string, note: NoteItem) => {
return res; return res;
}; };
export const getNotes = async (token: string = '') => { export const getNotes = async (token: string = '', raw: boolean = false) => {
let error = null; let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/notes/`, { const res = await fetch(`${WEBUI_API_BASE_URL}/notes/`, {
@ -67,6 +67,10 @@ export const getNotes = async (token: string = '') => {
throw error; throw error;
} }
if (raw) {
return res; // Return raw response if requested
}
if (!Array.isArray(res)) { if (!Array.isArray(res)) {
return {}; // or throw new Error("Notes response is not an array") return {}; // or throw new Error("Notes response is not an array")
} }
@ -87,6 +91,37 @@ export const getNotes = async (token: string = '') => {
return grouped; 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) => { export const getNoteById = async (token: string, id: string) => {
let error = null; let error = null;

View file

@ -1597,9 +1597,8 @@
let files = JSON.parse(JSON.stringify(chatFiles)); let files = JSON.parse(JSON.stringify(chatFiles));
files.push( files.push(
...(userMessage?.files ?? []).filter((item) => ...(userMessage?.files ?? []).filter((item) =>
['doc', 'file', 'collection'].includes(item.type) ['doc', 'file', 'note', 'collection'].includes(item.type)
), )
...(responseMessage?.files ?? []).filter((item) => ['web_search_results'].includes(item.type))
); );
// Remove duplicates // Remove duplicates
files = files.filter( files = files.filter(

View file

@ -9,6 +9,7 @@
import { tick, getContext, onMount, onDestroy } from 'svelte'; import { tick, getContext, onMount, onDestroy } from 'svelte';
import { removeLastWordFromString, isValidHttpUrl } from '$lib/utils'; import { removeLastWordFromString, isValidHttpUrl } from '$lib/utils';
import { knowledge } from '$lib/stores'; import { knowledge } from '$lib/stores';
import { getNoteList, getNotes } from '$lib/apis/notes';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -75,10 +76,23 @@
} }
}; };
onMount(() => { onMount(async () => {
window.addEventListener('resize', adjustHeight); window.addEventListener('resize', adjustHeight);
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 let legacy_documents = $knowledge
.filter((item) => item?.meta?.document) .filter((item) => item?.meta?.document)
.map((item) => ({ .map((item) => ({
@ -144,14 +158,18 @@
] ]
: []; : [];
items = [...collections, ...collection_files, ...legacy_collections, ...legacy_documents].map( items = [
(item) => { ...notes,
return { ...collections,
...item, ...collection_files,
...(item?.legacy || item?.meta?.legacy || item?.meta?.document ? { legacy: true } : {}) ...legacy_collections,
}; ...legacy_documents
} ].map((item) => {
); return {
...item,
...(item?.legacy || item?.meta?.legacy || item?.meta?.document ? { legacy: true } : {})
};
});
fuse = new Fuse(items, { fuse = new Fuse(items, {
keys: ['name', 'description'] keys: ['name', 'description']
@ -210,6 +228,12 @@
> >
File File
</div> </div>
{:else if item?.type === 'note'}
<div
class="bg-blue-500/20 text-blue-700 dark:text-blue-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
>
Note
</div>
{:else} {:else}
<div <div
class="bg-green-500/20 text-green-700 dark:text-green-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0" class="bg-green-500/20 text-green-700 dark:text-green-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"

View file

@ -442,7 +442,10 @@
const downloadHandler = async (type) => { const downloadHandler = async (type) => {
console.log('downloadHandler', type); 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' }); const blob = new Blob([note.data.content.md], { type: 'text/markdown' });
saveAs(blob, `${note.title}.md`); saveAs(blob, `${note.title}.md`);
} else if (type === 'pdf') { } else if (type === 'pdf') {

View file

@ -302,7 +302,7 @@
</div> </div>
<div <div
class="mb-5 gap-2.5 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4" class="mb-5 gap-2.5 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5"
> >
{#each notes[timeRange] as note, idx (note.id)} {#each notes[timeRange] as note, idx (note.id)}
<div <div
@ -340,7 +340,7 @@
</div> </div>
<div <div
class=" text-xs text-gray-500 dark:text-gray-500 mb-3 line-clamp-5 min-h-18" class=" text-xs text-gray-500 dark:text-gray-500 mb-3 line-clamp-3 min-h-10"
> >
{#if note.data?.content?.md} {#if note.data?.content?.md}
{note.data?.content?.md} {note.data?.content?.md}

View file

@ -57,6 +57,15 @@
transition={flyAndScale} transition={flyAndScale}
sideOffset={8} sideOffset={8}
> >
<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={() => {
onDownload('txt');
}}
>
<div class="flex items-center line-clamp-1">{$i18n.t('Plain text (.txt)')}</div>
</DropdownMenu.Item>
<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" 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={() => { on:click={() => {