소스 검색

enh: feedback exports

Timothy J. Baek 6 달 전
부모
커밋
8b61b39c75

+ 27 - 15
backend/open_webui/apps/webui/routers/evaluations.py

@@ -5,6 +5,7 @@ from pydantic import BaseModel
 from open_webui.apps.webui.models.users import Users, UserModel
 from open_webui.apps.webui.models.users import Users, UserModel
 from open_webui.apps.webui.models.feedbacks import (
 from open_webui.apps.webui.models.feedbacks import (
     FeedbackModel,
     FeedbackModel,
+    FeedbackResponse,
     FeedbackForm,
     FeedbackForm,
     Feedbacks,
     Feedbacks,
 )
 )
@@ -55,27 +56,15 @@ async def update_config(
     }
     }
 
 
 
 
-@router.get("/feedbacks", response_model=list[FeedbackModel])
-async def get_feedbacks(user=Depends(get_verified_user)):
-    feedbacks = Feedbacks.get_feedbacks_by_user_id(user.id)
-    return feedbacks
-
-
-@router.delete("/feedbacks", response_model=bool)
-async def delete_feedbacks(user=Depends(get_verified_user)):
-    success = Feedbacks.delete_feedbacks_by_user_id(user.id)
-    return success
-
-
-class FeedbackUserModel(FeedbackModel):
+class FeedbackUserResponse(FeedbackResponse):
     user: Optional[UserModel] = None
     user: Optional[UserModel] = None
 
 
 
 
-@router.get("/feedbacks/all", response_model=list[FeedbackUserModel])
+@router.get("/feedbacks/all", response_model=list[FeedbackUserResponse])
 async def get_all_feedbacks(user=Depends(get_admin_user)):
 async def get_all_feedbacks(user=Depends(get_admin_user)):
     feedbacks = Feedbacks.get_all_feedbacks()
     feedbacks = Feedbacks.get_all_feedbacks()
     return [
     return [
-        FeedbackUserModel(
+        FeedbackUserResponse(
             **feedback.model_dump(), user=Users.get_user_by_id(feedback.user_id)
             **feedback.model_dump(), user=Users.get_user_by_id(feedback.user_id)
         )
         )
         for feedback in feedbacks
         for feedback in feedbacks
@@ -88,6 +77,29 @@ async def delete_all_feedbacks(user=Depends(get_admin_user)):
     return success
     return success
 
 
 
 
+@router.get("/feedbacks/all/export", response_model=list[FeedbackModel])
+async def get_all_feedbacks(user=Depends(get_admin_user)):
+    feedbacks = Feedbacks.get_all_feedbacks()
+    return [
+        FeedbackModel(
+            **feedback.model_dump(), user=Users.get_user_by_id(feedback.user_id)
+        )
+        for feedback in feedbacks
+    ]
+
+
+@router.get("/feedbacks/user", response_model=list[FeedbackUserResponse])
+async def get_feedbacks(user=Depends(get_verified_user)):
+    feedbacks = Feedbacks.get_feedbacks_by_user_id(user.id)
+    return feedbacks
+
+
+@router.delete("/feedbacks", response_model=bool)
+async def delete_feedbacks(user=Depends(get_verified_user)):
+    success = Feedbacks.delete_feedbacks_by_user_id(user.id)
+    return success
+
+
 @router.post("/feedback", response_model=FeedbackModel)
 @router.post("/feedback", response_model=FeedbackModel)
 async def create_feedback(
 async def create_feedback(
     request: Request,
     request: Request,

+ 31 - 0
src/lib/apis/evaluations/index.ts

@@ -93,6 +93,37 @@ export const getAllFeedbacks = async (token: string = '') => {
 	return res;
 	return res;
 };
 };
 
 
+export const exportAllFeedbacks = async (token: string = '') => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/evaluations/feedbacks/all/export`, {
+		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.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
 export const createNewFeedback = async (token: string, feedback: object) => {
 export const createNewFeedback = async (token: string, feedback: object) => {
 	let error = null;
 	let error = null;
 
 

+ 37 - 13
src/lib/components/admin/Evaluations.svelte

@@ -1,4 +1,7 @@
 <script lang="ts">
 <script lang="ts">
+	import fileSaver from 'file-saver';
+	const { saveAs } = fileSaver;
+
 	import { onMount, getContext } from 'svelte';
 	import { onMount, getContext } from 'svelte';
 	import dayjs from 'dayjs';
 	import dayjs from 'dayjs';
 	import relativeTime from 'dayjs/plugin/relativeTime';
 	import relativeTime from 'dayjs/plugin/relativeTime';
@@ -12,7 +15,7 @@
 	let model = null;
 	let model = null;
 
 
 	import { models } from '$lib/stores';
 	import { models } from '$lib/stores';
-	import { deleteFeedbackById, getAllFeedbacks } from '$lib/apis/evaluations';
+	import { deleteFeedbackById, exportAllFeedbacks, getAllFeedbacks } from '$lib/apis/evaluations';
 
 
 	import FeedbackMenu from './Evaluations/FeedbackMenu.svelte';
 	import FeedbackMenu from './Evaluations/FeedbackMenu.svelte';
 	import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
 	import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
@@ -24,6 +27,9 @@
 	import CloudArrowUp from '../icons/CloudArrowUp.svelte';
 	import CloudArrowUp from '../icons/CloudArrowUp.svelte';
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
 	import Spinner from '../common/Spinner.svelte';
 	import Spinner from '../common/Spinner.svelte';
+	import DocumentArrowUpSolid from '../icons/DocumentArrowUpSolid.svelte';
+	import DocumentArrowDown from '../icons/DocumentArrowDown.svelte';
+	import ArrowDownTray from '../icons/ArrowDownTray.svelte';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
@@ -300,6 +306,20 @@
 		window.addEventListener('message', messageHandler, false);
 		window.addEventListener('message', messageHandler, false);
 	};
 	};
 
 
+	const exportHandler = async () => {
+		const _feedbacks = await exportAllFeedbacks(localStorage.token).catch((err) => {
+			toast.error(err);
+			return null;
+		});
+
+		if (_feedbacks) {
+			let blob = new Blob([JSON.stringify(_feedbacks)], {
+				type: 'application/json'
+			});
+			saveAs(blob, `feedback-history-export-${Date.now()}.json`);
+		}
+	};
+
 	const loadEmbeddingModel = async () => {
 	const loadEmbeddingModel = async () => {
 		// Check if the tokenizer and model are already loaded and stored in the window object
 		// Check if the tokenizer and model are already loaded and stored in the window object
 		if (!window.tokenizer) {
 		if (!window.tokenizer) {
@@ -483,6 +503,21 @@
 
 
 			<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{feedbacks.length}</span>
 			<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{feedbacks.length}</span>
 		</div>
 		</div>
+
+		<div>
+			<div>
+				<Tooltip content={$i18n.t('Export')}>
+					<button
+						class=" p-2 rounded-xl hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition font-medium text-sm flex items-center space-x-1"
+						on:click={() => {
+							exportHandler();
+						}}
+					>
+						<ArrowDownTray className="size-3" />
+					</button>
+				</Tooltip>
+			</div>
+		</div>
 	</div>
 	</div>
 
 
 	<div
 	<div
@@ -626,18 +661,7 @@
 						</div>
 						</div>
 
 
 						<div class=" self-center">
 						<div class=" self-center">
-							<svg
-								xmlns="http://www.w3.org/2000/svg"
-								viewBox="0 0 16 16"
-								fill="currentColor"
-								class="w-3.5 h-3.5"
-							>
-								<path
-									fill-rule="evenodd"
-									d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
-									clip-rule="evenodd"
-								/>
-							</svg>
+							<CloudArrowUp className="size-3" strokeWidth="3" />
 						</div>
 						</div>
 					</button>
 					</button>
 				</Tooltip>
 				</Tooltip>

+ 19 - 0
src/lib/components/icons/DocumentArrowDown.svelte

@@ -0,0 +1,19 @@
+<script lang="ts">
+	export let className = 'size-4';
+	export let strokeWidth = '1.5';
+</script>
+
+<svg
+	xmlns="http://www.w3.org/2000/svg"
+	fill="none"
+	viewBox="0 0 24 24"
+	stroke-width={strokeWidth}
+	stroke="currentColor"
+	class={className}
+>
+	<path
+		stroke-linecap="round"
+		stroke-linejoin="round"
+		d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m.75 12 3 3m0 0 3-3m-3 3v-6m-1.5-9H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"
+	/>
+</svg>