diff --git a/backend/open_webui/models/feedbacks.py b/backend/open_webui/models/feedbacks.py index aa6c7bdca..f930739f6 100644 --- a/backend/open_webui/models/feedbacks.py +++ b/backend/open_webui/models/feedbacks.py @@ -215,6 +215,13 @@ class FeedbackTable: query = db.query(Feedback, User).join(User, Feedback.user_id == User.id) if filter: + # Apply model_id filter (exact match) + model_id = filter.get('model_id') + if model_id: + query = query.filter( + Feedback.data['model_id'].as_string() == model_id + ) + order_by = filter.get('order_by') direction = filter.get('direction') @@ -288,6 +295,17 @@ class FeedbackTable: .all() ] + def get_distinct_model_ids(self, db: Optional[Session] = None) -> list[str]: + """Get distinct model_ids from feedback data for filter dropdowns.""" + with get_db_context(db) as db: + rows = ( + db.query(Feedback.data['model_id'].as_string()) + .filter(Feedback.data['model_id'].as_string().isnot(None)) + .distinct() + .all() + ) + return sorted([row[0] for row in rows if row[0]]) + def get_feedbacks_for_leaderboard(self, db: Optional[Session] = None) -> list[LeaderboardFeedbackData]: """Fetch only id and data for leaderboard computation (excludes snapshot/meta).""" with get_db_context(db) as db: diff --git a/backend/open_webui/routers/evaluations.py b/backend/open_webui/routers/evaluations.py index de97e172f..a743f8f9f 100644 --- a/backend/open_webui/routers/evaluations.py +++ b/backend/open_webui/routers/evaluations.py @@ -291,6 +291,11 @@ async def update_config( } +@router.get('/feedbacks/models', response_model=list[str]) +async def get_feedback_model_ids(user=Depends(get_admin_user), db: Session = Depends(get_session)): + return Feedbacks.get_distinct_model_ids(db=db) + + @router.get('/feedbacks/all', response_model=list[FeedbackResponse]) async def get_all_feedbacks(user=Depends(get_admin_user), db: Session = Depends(get_session)): feedbacks = Feedbacks.get_all_feedbacks(db=db) @@ -309,8 +314,17 @@ async def delete_all_feedbacks(user=Depends(get_admin_user), db: Session = Depen @router.get('/feedbacks/all/export', response_model=list[FeedbackModel]) -async def export_all_feedbacks(user=Depends(get_admin_user), db: Session = Depends(get_session)): +async def export_all_feedbacks( + model_id: Optional[str] = None, + user=Depends(get_admin_user), + db: Session = Depends(get_session), +): feedbacks = Feedbacks.get_all_feedbacks(db=db) + if model_id: + feedbacks = [ + f for f in feedbacks + if f.data and f.data.get('model_id') == model_id + ] return feedbacks @@ -334,6 +348,7 @@ async def get_feedbacks( order_by: Optional[str] = None, direction: Optional[str] = None, page: Optional[int] = 1, + model_id: Optional[str] = None, user=Depends(get_admin_user), db: Session = Depends(get_session), ): @@ -347,6 +362,8 @@ async def get_feedbacks( filter['order_by'] = order_by if direction: filter['direction'] = direction + if model_id: + filter['model_id'] = model_id result = Feedbacks.get_feedback_items(filter=filter, skip=skip, limit=limit, db=db) return result diff --git a/src/lib/apis/evaluations/index.ts b/src/lib/apis/evaluations/index.ts index a3af6e80b..925329511 100644 --- a/src/lib/apis/evaluations/index.ts +++ b/src/lib/apis/evaluations/index.ts @@ -161,13 +161,42 @@ export const getModelHistory = async (token: string = '', modelId: string, days: return res; }; -export const getFeedbackItems = async (token: string = '', orderBy, direction, page) => { +export const getFeedbackModelIds = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/evaluations/feedbacks/models`, { + 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(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getFeedbackItems = async (token: string = '', orderBy, direction, page, modelId: string = '') => { 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()); + if (modelId) searchParams.append('model_id', modelId); const res = await fetch( `${WEBUI_API_BASE_URL}/evaluations/feedbacks/list?${searchParams.toString()}`, @@ -200,10 +229,13 @@ export const getFeedbackItems = async (token: string = '', orderBy, direction, p return res; }; -export const exportAllFeedbacks = async (token: string = '') => { +export const exportAllFeedbacks = async (token: string = '', modelId: string = '') => { let error = null; - const res = await fetch(`${WEBUI_API_BASE_URL}/evaluations/feedbacks/all/export`, { + const searchParams = new URLSearchParams(); + if (modelId) searchParams.append('model_id', modelId); + + const res = await fetch(`${WEBUI_API_BASE_URL}/evaluations/feedbacks/all/export?${searchParams.toString()}`, { method: 'GET', headers: { Accept: 'application/json', diff --git a/src/lib/components/admin/Evaluations/Feedbacks.svelte b/src/lib/components/admin/Evaluations/Feedbacks.svelte index 1dedb94f0..0e47aaba0 100644 --- a/src/lib/components/admin/Evaluations/Feedbacks.svelte +++ b/src/lib/components/admin/Evaluations/Feedbacks.svelte @@ -10,7 +10,12 @@ import { onMount, getContext } from 'svelte'; const i18n = getContext('i18n'); - import { deleteFeedbackById, exportAllFeedbacks, getFeedbackItems } from '$lib/apis/evaluations'; + import { + deleteFeedbackById, + exportAllFeedbacks, + getFeedbackItems, + getFeedbackModelIds + } from '$lib/apis/evaluations'; import Tooltip from '$lib/components/common/Tooltip.svelte'; import Download from '$lib/components/icons/Download.svelte'; @@ -26,6 +31,8 @@ import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; import { config } from '$lib/stores'; import Spinner from '$lib/components/common/Spinner.svelte'; + import Select from '$lib/components/common/Select.svelte'; + import Check from '$lib/components/icons/Check.svelte'; let page = 1; let items = null; @@ -34,6 +41,9 @@ let orderBy: string = 'updated_at'; let direction: 'asc' | 'desc' = 'desc'; + let selectedModelId: string = ''; + let modelIds: string[] = []; + const setSortKey = (key) => { if (orderBy === key) { direction = direction === 'asc' ? 'desc' : 'asc'; @@ -64,12 +74,16 @@ const getFeedbacks = async () => { try { - const res = await getFeedbackItems(localStorage.token, orderBy, direction, page).catch( - (error) => { - toast.error(`${error}`); - return null; - } - ); + const res = await getFeedbackItems( + localStorage.token, + orderBy, + direction, + page, + selectedModelId + ).catch((error) => { + toast.error(`${error}`); + return null; + }); if (res) { items = res.items; @@ -80,10 +94,21 @@ } }; - $: if (orderBy && direction && page) { + $: if (orderBy && direction && page !== undefined) { getFeedbacks(); } + const loadModelIds = async () => { + try { + const res = await getFeedbackModelIds(localStorage.token); + if (res) { + modelIds = res; + } + } catch (err) { + console.error(err); + } + }; + const deleteFeedbackHandler = async (feedbackId: string) => { const response = await deleteFeedbackById(localStorage.token, feedbackId).catch((err) => { toast.error(err); @@ -124,10 +149,12 @@ }; const exportHandler = async () => { - const _feedbacks = await exportAllFeedbacks(localStorage.token).catch((err) => { - toast.error(err); - return null; - }); + const _feedbacks = await exportAllFeedbacks(localStorage.token, selectedModelId).catch( + (err) => { + toast.error(err); + return null; + } + ); if (_feedbacks) { let blob = new Blob([JSON.stringify(_feedbacks)], { @@ -136,6 +163,10 @@ saveAs(blob, `feedback-history-export-${Date.now()}.json`); } }; + + onMount(() => { + loadModelIds(); + }); @@ -145,236 +176,296 @@ {:else} -
-
-
- {$i18n.t('Feedback History')} +
+
+
+
+ {$i18n.t('Feedback History')} +
+ +
+ {total} +
-
- {total} -
-
- - {#if total > 0} -
- +
+ {#if total > 0} - + {/if}
- {/if} +
-
- {#if (items ?? []).length === 0} -
- {$i18n.t('No feedback found')} -
- {:else} - + {#if modelIds.length > 0} +
{ + if (e.deltaY !== 0) { + e.preventDefault(); + e.currentTarget.scrollLeft += e.deltaY; + } + }} > -
- - +
+
+ + + + + {/if} - + {/each} + +
setSortKey('user')} - > -
- {$i18n.t('User')} - {#if orderBy === 'user'} - - {#if direction === 'asc'} - - {:else} - - {/if} - - {:else} - - {/if} -
-
setSortKey('model_id')} - > -
- {$i18n.t('Models')} - {#if orderBy === 'model_id'} - - {#if direction === 'asc'} - - {:else} - - {/if} - - {:else} - - {/if} + + {item.label} +
+
-
setSortKey('rating')} - > -
- {$i18n.t('Result')} - {#if orderBy === 'rating'} - - {#if direction === 'asc'} +
+ {#if (items ?? []).length === 0} +
+
+
😕
+
{$i18n.t('No feedback found')}
+
+ {$i18n.t('Try adjusting your search or filter to find what you are looking for.')} +
+
+
+ {:else} + + + + - - - - - - - - {#each items as feedback (feedback.id)} - openFeedbackModal(feedback)} - > - + - - {#if feedback?.data?.rating} - + + + + + + + + {#each items as feedback (feedback.id)} + openFeedbackModal(feedback)} + > + - {/if} - + + + {#if feedback?.data?.rating} + + {/if} + + + + - - {/each} - -
setSortKey('user')} + > +
+ {$i18n.t('User')} + {#if orderBy === 'user'} + + {#if direction === 'asc'} + + {:else} + + {/if} + + {:else} + - {:else} - - {/if} -
-
setSortKey('updated_at')} - > -
- {$i18n.t('Updated At')} - {#if orderBy === 'updated_at'} - - {#if direction === 'asc'} - - {:else} - - {/if} - - {:else} - - {/if} -
-
-
- -
- {feedback?.user?.name} -
-
+ + {/if}
-
-
-
- {#if feedback.data?.sibling_model_ids} - -
- {feedback.data?.model_id} -
-
- - -
- {#if feedback.data.sibling_model_ids.length > 2} - - {feedback.data.sibling_model_ids.slice(0, 2).join(', ')}, {$i18n.t( - 'and {{COUNT}} more', - { COUNT: feedback.data.sibling_model_ids.length - 2 } - )} - {:else} - {feedback.data.sibling_model_ids.join(', ')} - {/if} -
-
- {:else} - -
- {feedback.data?.model_id} -
-
- {/if} -
+
setSortKey('model_id')} + > +
+ {$i18n.t('Models')} + {#if orderBy === 'model_id'} + + {#if direction === 'asc'} + + {:else} + + {/if} + + {:else} + + {/if}
- +
-
- {#if feedback?.data?.rating.toString() === '1'} - - {:else if feedback?.data?.rating.toString() === '0'} - - {:else if feedback?.data?.rating.toString() === '-1'} - - {/if} +
setSortKey('rating')} + > +
+ {$i18n.t('Result')} + {#if orderBy === 'rating'} + + {#if direction === 'asc'} + + {:else} + + {/if} + + {:else} + + {/if} +
+
setSortKey('updated_at')} + > +
+ {$i18n.t('Updated At')} + {#if orderBy === 'updated_at'} + + {#if direction === 'asc'} + + {:else} + + {/if} + + {:else} + + {/if} +
+
+
+ +
+ {feedback?.user?.name} +
+
- {dayjs(feedback.updated_at * 1000).fromNow()} - +
+
+ {#if feedback.data?.sibling_model_ids} + +
+ {feedback.data?.model_id} +
+
-
e.stopPropagation()}> - { - deleteFeedbackHandler(feedback.id); - }} - > - +
+ {#if feedback?.data?.rating.toString() === '1'} + + {:else if feedback?.data?.rating.toString() === '0'} + + {:else if feedback?.data?.rating.toString() === '-1'} + + {/if} +
+
+ {dayjs(feedback.updated_at * 1000).fromNow()} + e.stopPropagation()}> + { + deleteFeedbackHandler(feedback.id); + }} > - - - -
+ + + +
+ {/if} +
+ + {#if total > 30} + {/if}
- - {#if total > 30} - - {/if} {/if}