From 60839606556922a4f565f405752e20ff400e24e3 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 20 Nov 2025 17:10:12 -0500 Subject: [PATCH] refac: feedback list optimisation --- backend/open_webui/models/feedbacks.py | 88 +++++++++++++- backend/open_webui/routers/evaluations.py | 56 ++++----- src/lib/apis/evaluations/index.ts | 39 ++++++ src/lib/components/admin/Evaluations.svelte | 4 +- .../admin/Evaluations/Feedbacks.svelte | 114 +++++++----------- 5 files changed, 202 insertions(+), 99 deletions(-) diff --git a/backend/open_webui/models/feedbacks.py b/backend/open_webui/models/feedbacks.py index 215e36aa24..33f7f6179a 100644 --- a/backend/open_webui/models/feedbacks.py +++ b/backend/open_webui/models/feedbacks.py @@ -4,7 +4,7 @@ import uuid from typing import Optional from open_webui.internal.db import Base, get_db -from open_webui.models.chats import Chats +from open_webui.models.users import User from open_webui.env import SRC_LOG_LEVELS from pydantic import BaseModel, ConfigDict @@ -92,6 +92,28 @@ class FeedbackForm(BaseModel): model_config = ConfigDict(extra="allow") +class UserResponse(BaseModel): + id: str + name: str + email: str + role: str = "pending" + + last_active_at: int # timestamp in epoch + updated_at: int # timestamp in epoch + created_at: int # timestamp in epoch + + model_config = ConfigDict(from_attributes=True) + + +class FeedbackUserResponse(FeedbackResponse): + user: Optional[UserResponse] = None + + +class FeedbackListResponse(BaseModel): + items: list[FeedbackUserResponse] + total: int + + class FeedbackTable: def insert_new_feedback( self, user_id: str, form_data: FeedbackForm @@ -143,6 +165,70 @@ class FeedbackTable: except Exception: return None + def get_feedback_items( + self, filter: dict = {}, skip: int = 0, limit: int = 30 + ) -> FeedbackListResponse: + with get_db() as db: + query = db.query(Feedback, User).join(User, Feedback.user_id == User.id) + + if filter: + order_by = filter.get("order_by") + direction = filter.get("direction") + + if order_by == "username": + if direction == "asc": + query = query.order_by(User.name.asc()) + else: + query = query.order_by(User.name.desc()) + elif order_by == "model_id": + # it's stored in feedback.data['model_id'] + if direction == "asc": + query = query.order_by( + Feedback.data["model_id"].as_string().asc() + ) + else: + query = query.order_by( + Feedback.data["model_id"].as_string().desc() + ) + elif order_by == "rating": + # it's stored in feedback.data['rating'] + if direction == "asc": + query = query.order_by( + Feedback.data["rating"].as_string().asc() + ) + else: + query = query.order_by( + Feedback.data["rating"].as_string().desc() + ) + elif order_by == "updated_at": + if direction == "asc": + query = query.order_by(Feedback.updated_at.asc()) + else: + query = query.order_by(Feedback.updated_at.desc()) + + else: + query = query.order_by(Feedback.created_at.desc()) + + # Count BEFORE pagination + total = query.count() + + if skip: + query = query.offset(skip) + if limit: + query = query.limit(limit) + + items = query.all() + + feedbacks = [] + for feedback, user in items: + feedback_model = FeedbackModel.model_validate(feedback) + user_model = UserResponse.model_validate(user) + feedbacks.append( + FeedbackUserResponse(**feedback_model.model_dump(), user=user_model) + ) + + return FeedbackListResponse(items=feedbacks, total=total) + def get_all_feedbacks(self) -> list[FeedbackModel]: with get_db() as db: return [ diff --git a/backend/open_webui/routers/evaluations.py b/backend/open_webui/routers/evaluations.py index c76a1f6915..3e5e14801c 100644 --- a/backend/open_webui/routers/evaluations.py +++ b/backend/open_webui/routers/evaluations.py @@ -7,6 +7,8 @@ from open_webui.models.feedbacks import ( FeedbackModel, FeedbackResponse, FeedbackForm, + FeedbackUserResponse, + FeedbackListResponse, Feedbacks, ) @@ -56,35 +58,10 @@ async def update_config( } -class UserResponse(BaseModel): - id: str - name: str - email: str - role: str = "pending" - - last_active_at: int # timestamp in epoch - updated_at: int # timestamp in epoch - created_at: int # timestamp in epoch - - -class FeedbackUserResponse(FeedbackResponse): - user: Optional[UserResponse] = None - - -@router.get("/feedbacks/all", response_model=list[FeedbackUserResponse]) +@router.get("/feedbacks/all", response_model=list[FeedbackResponse]) async def get_all_feedbacks(user=Depends(get_admin_user)): feedbacks = Feedbacks.get_all_feedbacks() - - feedback_list = [] - for feedback in feedbacks: - user = Users.get_user_by_id(feedback.user_id) - feedback_list.append( - FeedbackUserResponse( - **feedback.model_dump(), - user=UserResponse(**user.model_dump()) if user else None, - ) - ) - return feedback_list + return feedbacks @router.delete("/feedbacks/all") @@ -111,6 +88,31 @@ async def delete_feedbacks(user=Depends(get_verified_user)): return success +PAGE_ITEM_COUNT = 30 + + +@router.get("/feedbacks/list", response_model=FeedbackListResponse) +async def get_feedbacks( + order_by: Optional[str] = None, + direction: Optional[str] = None, + page: Optional[int] = 1, + user=Depends(get_admin_user), +): + limit = PAGE_ITEM_COUNT + + page = max(1, page) + skip = (page - 1) * limit + + filter = {} + if order_by: + filter["order_by"] = order_by + if direction: + filter["direction"] = direction + + result = Feedbacks.get_feedback_items(filter=filter, skip=skip, limit=limit) + return result + + @router.post("/feedback", response_model=FeedbackModel) async def create_feedback( request: Request, diff --git a/src/lib/apis/evaluations/index.ts b/src/lib/apis/evaluations/index.ts index 96a689fcb1..1f48c7bfbf 100644 --- a/src/lib/apis/evaluations/index.ts +++ b/src/lib/apis/evaluations/index.ts @@ -93,6 +93,45 @@ export const getAllFeedbacks = async (token: string = '') => { return res; }; +export const getFeedbackItems = async (token: string = '', orderBy, direction, page) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (orderBy) searchParams.append('order_by', orderBy); + if (direction) searchParams.append('direction', direction); + if (page) searchParams.append('page', page.toString()); + + const res = await fetch( + `${WEBUI_API_BASE_URL}/evaluations/feedbacks/list?${searchParams.toString()}`, + { + 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 exportAllFeedbacks = async (token: string = '') => { let error = null; diff --git a/src/lib/components/admin/Evaluations.svelte b/src/lib/components/admin/Evaluations.svelte index d29dee746c..e2849bd98b 100644 --- a/src/lib/components/admin/Evaluations.svelte +++ b/src/lib/components/admin/Evaluations.svelte @@ -33,7 +33,9 @@ let feedbacks = []; onMount(async () => { + // TODO: feedbacks elo rating calculation should be done in the backend; remove below line later feedbacks = await getAllFeedbacks(localStorage.token); + loaded = true; const containerElement = document.getElementById('users-tabs-container'); @@ -117,7 +119,7 @@ {#if selectedTab === 'leaderboard'} {:else if selectedTab === 'feedbacks'} - + {/if} diff --git a/src/lib/components/admin/Evaluations/Feedbacks.svelte b/src/lib/components/admin/Evaluations/Feedbacks.svelte index 62304088ed..4b95b802fe 100644 --- a/src/lib/components/admin/Evaluations/Feedbacks.svelte +++ b/src/lib/components/admin/Evaluations/Feedbacks.svelte @@ -10,7 +10,7 @@ import { onMount, getContext } from 'svelte'; const i18n = getContext('i18n'); - import { deleteFeedbackById, exportAllFeedbacks, getAllFeedbacks } from '$lib/apis/evaluations'; + import { deleteFeedbackById, exportAllFeedbacks, getFeedbackItems } from '$lib/apis/evaluations'; import Tooltip from '$lib/components/common/Tooltip.svelte'; import Download from '$lib/components/icons/Download.svelte'; @@ -23,78 +23,24 @@ import ChevronUp from '$lib/components/icons/ChevronUp.svelte'; import ChevronDown from '$lib/components/icons/ChevronDown.svelte'; - import { WEBUI_BASE_URL } from '$lib/constants'; + import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; import { config } from '$lib/stores'; - export let feedbacks = []; - let page = 1; - $: paginatedFeedbacks = sortedFeedbacks.slice((page - 1) * 10, page * 10); + let items = null; + let total = null; let orderBy: string = 'updated_at'; let direction: 'asc' | 'desc' = 'desc'; - type Feedback = { - id: string; - data: { - rating: number; - model_id: string; - sibling_model_ids: string[] | null; - reason: string; - comment: string; - tags: string[]; - }; - user: { - name: string; - profile_image_url: string; - }; - updated_at: number; - }; - - type ModelStats = { - rating: number; - won: number; - lost: number; - }; - - function setSortKey(key: string) { + const setSortKey = (key) => { if (orderBy === key) { direction = direction === 'asc' ? 'desc' : 'asc'; } else { orderBy = key; - if (key === 'user' || key === 'model_id') { - direction = 'asc'; - } else { - direction = 'desc'; - } + direction = 'asc'; } - page = 1; - } - - $: sortedFeedbacks = [...feedbacks].sort((a, b) => { - let aVal, bVal; - - switch (orderBy) { - case 'user': - aVal = a.user?.name || ''; - bVal = b.user?.name || ''; - return direction === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal); - case 'model_id': - aVal = a.data.model_id || ''; - bVal = b.data.model_id || ''; - return direction === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal); - case 'rating': - aVal = a.data.rating; - bVal = b.data.rating; - return direction === 'asc' ? aVal - bVal : bVal - aVal; - case 'updated_at': - aVal = a.updated_at; - bVal = b.updated_at; - return direction === 'asc' ? aVal - bVal : bVal - aVal; - default: - return 0; - } - }); + }; let showFeedbackModal = false; let selectedFeedback = null; @@ -115,13 +61,41 @@ // ////////////////////// + const getFeedbacks = async () => { + try { + const res = await getFeedbackItems(localStorage.token, orderBy, direction, page).catch( + (error) => { + toast.error(`${error}`); + return null; + } + ); + + if (res) { + items = res.items; + total = res.total; + } + } catch (err) { + console.error(err); + } + }; + + $: if (page) { + getFeedbacks(); + } + + $: if (orderBy && direction) { + getFeedbacks(); + } + const deleteFeedbackHandler = async (feedbackId: string) => { const response = await deleteFeedbackById(localStorage.token, feedbackId).catch((err) => { toast.error(err); return null; }); if (response) { - feedbacks = feedbacks.filter((f) => f.id !== feedbackId); + toast.success($i18n.t('Feedback deleted successfully')); + page = 1; + getFeedbacks(); } }; @@ -175,10 +149,10 @@
- {feedbacks.length} + {total}
- {#if feedbacks.length > 0} + {#if total > 0}