From 85bbed3ec539dc958d306117f97d6be977b7253b Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 26 Dec 2025 15:13:50 +0400 Subject: [PATCH] enh: sync stats --- backend/open_webui/routers/chats.py | 219 +++++++++--------- src/lib/apis/chats/index.ts | 41 ++++ .../chat/Settings/SyncStatsModal.svelte | 174 ++++++++++++++ src/routes/+layout.svelte | 34 +++ 4 files changed, 362 insertions(+), 106 deletions(-) create mode 100644 src/lib/components/chat/Settings/SyncStatsModal.svelte diff --git a/backend/open_webui/routers/chats.py b/backend/open_webui/routers/chats.py index 0cfe96c29b..3465205338 100644 --- a/backend/open_webui/routers/chats.py +++ b/backend/open_webui/routers/chats.py @@ -228,8 +228,7 @@ async def export_chat_stats( ) try: - # Default pagination - limit = 50 + limit = 100 skip = (page - 1) * limit # Fetch chats with date filtering @@ -255,121 +254,125 @@ async def export_chat_stats( chat_stats_export_list = [] for chat in result.items: - messages_map = chat.chat.get("history", {}).get("messages", {}) - message_id = chat.chat.get("history", {}).get("currentId") + try: + messages_map = chat.chat.get("history", {}).get("messages", {}) + message_id = chat.chat.get("history", {}).get("currentId") - history_models = {} - history_message_count = len(messages_map) - history_user_messages = [] - history_assistant_messages = [] + history_models = {} + history_message_count = len(messages_map) + history_user_messages = [] + history_assistant_messages = [] - # --- Detailed Message Stats --- - export_messages = {} - for key, message in messages_map.items(): - content = message.get("content", "") - if isinstance(content, str): - content_length = len(content) - else: - content_length = ( - 0 # Handle cases where content might be None or not string + export_messages = {} + for key, message in messages_map.items(): + try: + content = message.get("content", "") + if isinstance(content, str): + content_length = len(content) + else: + content_length = 0 # Handle cases where content might be None or not string + + # Extract rating safely + rating = message.get("annotation", {}).get("rating") + message_stat = MessageStats( + id=message.get("id"), + role=message.get("role"), + model=message.get("model"), + timestamp=message.get("timestamp"), + content_length=content_length, + token_count=None, # Populate if available, e.g. message.get("info", {}).get("token_count") + rating=rating, + ) + + export_messages[key] = message_stat + + # --- Aggregation Logic (copied/adapted from usage stats) --- + role = message.get("role", "") + if role == "user": + history_user_messages.append(message) + elif role == "assistant": + history_assistant_messages.append(message) + model = message.get("model") + if model: + if model not in history_models: + history_models[model] = 0 + history_models[model] += 1 + except Exception as e: + log.debug(f"Error processing message {key}: {e}") + continue + + # Calculate Averages + average_user_message_content_length = ( + sum( + len(m.get("content", "")) + for m in history_user_messages + if isinstance(m.get("content"), str) ) - - # Extract rating safely - rating = message.get("annotation", {}).get("rating") - - export_messages[key] = MessageStats( - id=message.get("id"), - role=message.get("role"), - model=message.get("model"), - timestamp=message.get("timestamp"), - content_length=content_length, - token_count=None, # Populate if available, e.g. message.get("info", {}).get("token_count") - rating=rating, + / len(history_user_messages) + if history_user_messages + else 0 ) - # --- Aggregation Logic (copied/adapted from usage stats) --- - role = message.get("role", "") - if role == "user": - history_user_messages.append(message) - elif role == "assistant": - history_assistant_messages.append(message) - model = message.get("model") - if model: - if model not in history_models: - history_models[model] = 0 - history_models[model] += 1 - - # Calculate Averages - average_user_message_content_length = ( - sum( - len(m.get("content", "")) - for m in history_user_messages - if isinstance(m.get("content"), str) + average_assistant_message_content_length = ( + sum( + len(m.get("content", "")) + for m in history_assistant_messages + if isinstance(m.get("content"), str) + ) + / len(history_assistant_messages) + if history_assistant_messages + else 0 ) - / len(history_user_messages) - if history_user_messages - else 0 - ) - average_assistant_message_content_length = ( - sum( - len(m.get("content", "")) - for m in history_assistant_messages - if isinstance(m.get("content"), str) + # Response Times + response_times = [] + for message in history_assistant_messages: + user_message_id = message.get("parentId", None) + if user_message_id and user_message_id in messages_map: + user_message = messages_map[user_message_id] + # Ensure timestamps exist + t1 = message.get("timestamp") + t0 = user_message.get("timestamp") + if t1 and t0: + response_times.append(t1 - t0) + + average_response_time = ( + sum(response_times) / len(response_times) if response_times else 0 ) - / len(history_assistant_messages) - if history_assistant_messages - else 0 - ) - # Response Times - response_times = [] - for message in history_assistant_messages: - user_message_id = message.get("parentId", None) - if user_message_id and user_message_id in messages_map: - user_message = messages_map[user_message_id] - # Ensure timestamps exist - t1 = message.get("timestamp") - t0 = user_message.get("timestamp") - if t1 and t0: - response_times.append(t1 - t0) + # Current Message List Logic (Main path) + message_list = get_message_list(messages_map, message_id) + message_count = len(message_list) + models = {} + for message in reversed(message_list): + if message.get("role") == "assistant": + model = message.get("model") + if model: + if model not in models: + models[model] = 0 + models[model] += 1 - average_response_time = ( - sum(response_times) / len(response_times) if response_times else 0 - ) + # Construct Aggregate Stats + stats = AggregateChatStats( + average_response_time=average_response_time, + average_user_message_content_length=average_user_message_content_length, + average_assistant_message_content_length=average_assistant_message_content_length, + models=models, + message_count=message_count, + history_models=history_models, + history_message_count=history_message_count, + history_user_message_count=len(history_user_messages), + history_assistant_message_count=len(history_assistant_messages), + ) - # Current Message List Logic (Main path) - message_list = get_message_list(messages_map, message_id) - message_count = len(message_list) - models = {} - for message in reversed(message_list): - if message.get("role") == "assistant": - model = message.get("model") - if model: - if model not in models: - models[model] = 0 - models[model] += 1 + # Construct Chat Body + chat_body = ChatBody( + history=ChatHistoryStats( + messages=export_messages, currentId=message_id + ) + ) - # Construct Aggregate Stats - stats = AggregateChatStats( - average_response_time=average_response_time, - average_user_message_content_length=average_user_message_content_length, - average_assistant_message_content_length=average_assistant_message_content_length, - models=models, - message_count=message_count, - history_models=history_models, - history_message_count=history_message_count, - history_user_message_count=len(history_user_messages), - history_assistant_message_count=len(history_assistant_messages), - ) - - # Construct Chat Body - chat_body = ChatBody( - history=ChatHistoryStats(messages=export_messages, currentId=message_id) - ) - - chat_stats_export_list.append( - ChatStatsExport( + chat_stat = ChatStatsExport( id=chat.id, user_id=chat.user_id, created_at=chat.created_at, @@ -378,7 +381,11 @@ async def export_chat_stats( stats=stats, chat=chat_body, ) - ) + + chat_stats_export_list.append(chat_stat) + except Exception as e: + log.debug(f"Error exporting stats for chat {chat.id}: {e}") + continue return ChatStatsExportList( items=chat_stats_export_list, total=result.total, page=page diff --git a/src/lib/apis/chats/index.ts b/src/lib/apis/chats/index.ts index 010c80a56f..435ffda800 100644 --- a/src/lib/apis/chats/index.ts +++ b/src/lib/apis/chats/index.ts @@ -1166,3 +1166,44 @@ export const archiveAllChats = async (token: string) => { return res; }; +export const exportChatStats = async (token: string, page: number = 1, params: object = {}) => { + let error = null; + + const searchParams = new URLSearchParams(); + searchParams.append('page', `${page}`); + + if (params) { + for (const [key, value] of Object.entries(params)) { + searchParams.append(key, `${value}`); + } + } + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/stats/export?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + + diff --git a/src/lib/components/chat/Settings/SyncStatsModal.svelte b/src/lib/components/chat/Settings/SyncStatsModal.svelte new file mode 100644 index 0000000000..c90da41031 --- /dev/null +++ b/src/lib/components/chat/Settings/SyncStatsModal.svelte @@ -0,0 +1,174 @@ + + + +
+ {#if completed} +
+ + +
+ +
+ +
+ {$i18n.t('Sync Complete!')} +
+ +
+ {$i18n.t('Your usage stats have been successfully synced with the Open WebUI Community.')} +
+ + +
+ {:else} +
+
{$i18n.t('Sync Usage Stats')}
+ +
+ +
+
+ {$i18n.t('Do you want to sync your usage stats with Open WebUI Community?')} +
+ +
+ {$i18n.t( + 'Participate in community leaderboards and evaluations! Syncing aggregated usage stats helps drive research and improvements to Open WebUI. Your privacy is paramount: no message content is ever shared.' + )} +
+ +
+
+ {$i18n.t('What is shared:')} +
+
    +
  • {$i18n.t('Model usage counts and preferences')}
  • +
  • {$i18n.t('Message counts and response timestamps')}
  • +
  • {$i18n.t('Content lengths (character counts only)')}
  • +
  • {$i18n.t('User ratings (thumbs up/down)')}
  • +
+ +
+ {$i18n.t('What is NOT shared:')} +
+
    +
  • {$i18n.t('Your message text or inputs')}
  • +
  • {$i18n.t('Model responses or outputs')}
  • +
  • {$i18n.t('Uploaded files or images')}
  • +
+
+ + {#if loading} +
+
+
{$i18n.t('Syncing stats...')}
+
{Math.round((processedItemsCount / total) * 100) || 0}%
+
+
+
+
+
+ {/if} + +
+ + +
+
+ {/if} +
+
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 288a0668a2..e91d136574 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -54,6 +54,7 @@ import NotificationToast from '$lib/components/NotificationToast.svelte'; import AppSidebar from '$lib/components/app/AppSidebar.svelte'; + import SyncStatsModal from '$lib/components/chat/Settings/SyncStatsModal.svelte'; import Spinner from '$lib/components/common/Spinner.svelte'; import { getUserSettings } from '$lib/apis/users'; import dayjs from 'dayjs'; @@ -89,6 +90,8 @@ let tokenTimer = null; let showRefresh = false; + let showSyncStatsModal = false; + let syncStatsParams = {}; let heartbeatInterval = null; @@ -600,7 +603,24 @@ } }; + const windowMessageEventHandler = async (event) => { + if ( + !['https://openwebui.com', 'https://www.openwebui.com', 'http://localhost:9999'].includes( + event.origin + ) + ) { + return; + } + + if (event.data === 'export:stats' || event.data?.type === 'export:stats') { + syncStatsParams = event.data?.searchParams ?? {}; + showSyncStatsModal = true; + } + }; + onMount(async () => { + window.addEventListener('message', windowMessageEventHandler); + let touchstartY = 0; function isNavOrDescendant(el) { @@ -814,10 +834,20 @@ loaded = true; } + // Notify opener window that the app has loaded + if (window.opener ?? false) { + window.opener.postMessage('loaded', '*'); + } + return () => { window.removeEventListener('resize', onResize); }; }); + + onDestroy(() => { + window.removeEventListener('message', windowMessageEventHandler); + bc.close(); + }); @@ -855,6 +885,10 @@ {/if} {/if} +{#if $config?.features.enable_community_sharing} + +{/if} +