mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-13 04:45:19 +00:00
enh: reference note in chat
This commit is contained in:
parent
f2ee99d760
commit
d5f9bbc7a7
8 changed files with 105 additions and 24 deletions
|
|
@ -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")]],
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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') {
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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={() => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue