mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 12:25:20 +00:00
refac: feedback list optimisation
This commit is contained in:
parent
485896753d
commit
6083960655
5 changed files with 202 additions and 99 deletions
|
|
@ -4,7 +4,7 @@ import uuid
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from open_webui.internal.db import Base, get_db
|
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 open_webui.env import SRC_LOG_LEVELS
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
@ -92,6 +92,28 @@ class FeedbackForm(BaseModel):
|
||||||
model_config = ConfigDict(extra="allow")
|
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:
|
class FeedbackTable:
|
||||||
def insert_new_feedback(
|
def insert_new_feedback(
|
||||||
self, user_id: str, form_data: FeedbackForm
|
self, user_id: str, form_data: FeedbackForm
|
||||||
|
|
@ -143,6 +165,70 @@ class FeedbackTable:
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
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]:
|
def get_all_feedbacks(self) -> list[FeedbackModel]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
return [
|
return [
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ from open_webui.models.feedbacks import (
|
||||||
FeedbackModel,
|
FeedbackModel,
|
||||||
FeedbackResponse,
|
FeedbackResponse,
|
||||||
FeedbackForm,
|
FeedbackForm,
|
||||||
|
FeedbackUserResponse,
|
||||||
|
FeedbackListResponse,
|
||||||
Feedbacks,
|
Feedbacks,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -56,35 +58,10 @@ async def update_config(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class UserResponse(BaseModel):
|
@router.get("/feedbacks/all", response_model=list[FeedbackResponse])
|
||||||
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])
|
|
||||||
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 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
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/feedbacks/all")
|
@router.delete("/feedbacks/all")
|
||||||
|
|
@ -111,6 +88,31 @@ async def delete_feedbacks(user=Depends(get_verified_user)):
|
||||||
return success
|
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)
|
@router.post("/feedback", response_model=FeedbackModel)
|
||||||
async def create_feedback(
|
async def create_feedback(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,45 @@ export const getAllFeedbacks = async (token: string = '') => {
|
||||||
return res;
|
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 = '') => {
|
export const exportAllFeedbacks = async (token: string = '') => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,9 @@
|
||||||
let feedbacks = [];
|
let feedbacks = [];
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
// TODO: feedbacks elo rating calculation should be done in the backend; remove below line later
|
||||||
feedbacks = await getAllFeedbacks(localStorage.token);
|
feedbacks = await getAllFeedbacks(localStorage.token);
|
||||||
|
|
||||||
loaded = true;
|
loaded = true;
|
||||||
|
|
||||||
const containerElement = document.getElementById('users-tabs-container');
|
const containerElement = document.getElementById('users-tabs-container');
|
||||||
|
|
@ -117,7 +119,7 @@
|
||||||
{#if selectedTab === 'leaderboard'}
|
{#if selectedTab === 'leaderboard'}
|
||||||
<Leaderboard {feedbacks} />
|
<Leaderboard {feedbacks} />
|
||||||
{:else if selectedTab === 'feedbacks'}
|
{:else if selectedTab === 'feedbacks'}
|
||||||
<Feedbacks {feedbacks} />
|
<Feedbacks />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
const i18n = getContext('i18n');
|
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 Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import Download from '$lib/components/icons/Download.svelte';
|
import Download from '$lib/components/icons/Download.svelte';
|
||||||
|
|
@ -23,78 +23,24 @@
|
||||||
|
|
||||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||||
import ChevronDown from '$lib/components/icons/ChevronDown.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';
|
import { config } from '$lib/stores';
|
||||||
|
|
||||||
export let feedbacks = [];
|
|
||||||
|
|
||||||
let page = 1;
|
let page = 1;
|
||||||
$: paginatedFeedbacks = sortedFeedbacks.slice((page - 1) * 10, page * 10);
|
let items = null;
|
||||||
|
let total = null;
|
||||||
|
|
||||||
let orderBy: string = 'updated_at';
|
let orderBy: string = 'updated_at';
|
||||||
let direction: 'asc' | 'desc' = 'desc';
|
let direction: 'asc' | 'desc' = 'desc';
|
||||||
|
|
||||||
type Feedback = {
|
const setSortKey = (key) => {
|
||||||
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) {
|
|
||||||
if (orderBy === key) {
|
if (orderBy === key) {
|
||||||
direction = direction === 'asc' ? 'desc' : 'asc';
|
direction = direction === 'asc' ? 'desc' : 'asc';
|
||||||
} else {
|
} else {
|
||||||
orderBy = key;
|
orderBy = key;
|
||||||
if (key === 'user' || key === 'model_id') {
|
direction = 'asc';
|
||||||
direction = 'asc';
|
|
||||||
} else {
|
|
||||||
direction = 'desc';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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 showFeedbackModal = false;
|
||||||
let selectedFeedback = null;
|
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 deleteFeedbackHandler = async (feedbackId: string) => {
|
||||||
const response = await deleteFeedbackById(localStorage.token, feedbackId).catch((err) => {
|
const response = await deleteFeedbackById(localStorage.token, feedbackId).catch((err) => {
|
||||||
toast.error(err);
|
toast.error(err);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
if (response) {
|
if (response) {
|
||||||
feedbacks = feedbacks.filter((f) => f.id !== feedbackId);
|
toast.success($i18n.t('Feedback deleted successfully'));
|
||||||
|
page = 1;
|
||||||
|
getFeedbacks();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -175,10 +149,10 @@
|
||||||
|
|
||||||
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
|
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
|
||||||
|
|
||||||
<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">{total}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if feedbacks.length > 0}
|
{#if total > 0}
|
||||||
<div>
|
<div>
|
||||||
<Tooltip content={$i18n.t('Export')}>
|
<Tooltip content={$i18n.t('Export')}>
|
||||||
<button
|
<button
|
||||||
|
|
@ -195,7 +169,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full">
|
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full">
|
||||||
{#if (feedbacks ?? []).length === 0}
|
{#if (items ?? []).length === 0}
|
||||||
<div class="text-center text-xs text-gray-500 dark:text-gray-400 py-1">
|
<div class="text-center text-xs text-gray-500 dark:text-gray-400 py-1">
|
||||||
{$i18n.t('No feedbacks found')}
|
{$i18n.t('No feedbacks found')}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -299,7 +273,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="">
|
<tbody class="">
|
||||||
{#each paginatedFeedbacks as feedback (feedback.id)}
|
{#each items as feedback (feedback.id)}
|
||||||
<tr
|
<tr
|
||||||
class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-850/50 transition"
|
class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-850/50 transition"
|
||||||
on:click={() => openFeedbackModal(feedback)}
|
on:click={() => openFeedbackModal(feedback)}
|
||||||
|
|
@ -309,7 +283,7 @@
|
||||||
<Tooltip content={feedback?.user?.name}>
|
<Tooltip content={feedback?.user?.name}>
|
||||||
<div class="shrink-0">
|
<div class="shrink-0">
|
||||||
<img
|
<img
|
||||||
src={feedback?.user?.profile_image_url ?? `${WEBUI_BASE_URL}/user.png`}
|
src={`${WEBUI_API_BASE_URL}/users/${feedback.user.id}/profile/image`}
|
||||||
alt={feedback?.user?.name}
|
alt={feedback?.user?.name}
|
||||||
class="size-5 rounded-full object-cover shrink-0"
|
class="size-5 rounded-full object-cover shrink-0"
|
||||||
/>
|
/>
|
||||||
|
|
@ -388,7 +362,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if feedbacks.length > 0 && $config?.features?.enable_community_sharing}
|
{#if total > 0 && $config?.features?.enable_community_sharing}
|
||||||
<div class=" flex flex-col justify-end w-full text-right gap-1">
|
<div class=" flex flex-col justify-end w-full text-right gap-1">
|
||||||
<div class="line-clamp-1 text-gray-500 text-xs">
|
<div class="line-clamp-1 text-gray-500 text-xs">
|
||||||
{$i18n.t('Help us create the best community leaderboard by sharing your feedback history!')}
|
{$i18n.t('Help us create the best community leaderboard by sharing your feedback history!')}
|
||||||
|
|
@ -419,6 +393,6 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if feedbacks.length > 10}
|
{#if total > 30}
|
||||||
<Pagination bind:page count={feedbacks.length} perPage={10} />
|
<Pagination bind:page count={total} perPage={30} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue