Răsfoiți Sursa

enh: reference note in chat

Timothy Jaeryang Baek 3 luni în urmă
părinte
comite
d5f9bbc7a7

+ 10 - 1
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")]],

+ 9 - 7
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")
     ]
 

+ 36 - 1
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;
 

+ 2 - 3
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(

+ 33 - 9
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
 										</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}
 										<div
 											class="bg-green-500/20 text-green-700 dark:text-green-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"

+ 4 - 1
src/lib/components/notes/NoteEditor.svelte

@@ -442,7 +442,10 @@
 
 	const downloadHandler = async (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' });
 			saveAs(blob, `${note.title}.md`);
 		} else if (type === 'pdf') {

+ 2 - 2
src/lib/components/notes/Notes.svelte

@@ -302,7 +302,7 @@
 						</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)}
 								<div
@@ -340,7 +340,7 @@
 												</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}
 														{note.data?.content?.md}

+ 9 - 0
src/lib/components/notes/Notes/NoteMenu.svelte

@@ -57,6 +57,15 @@
 					transition={flyAndScale}
 					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
 						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={() => {