mirror of
https://github.com/open-webui/open-webui.git
synced 2026-08-28 05:27:35 +00:00
295 lines
8.4 KiB
Svelte
295 lines
8.4 KiB
Svelte
<script lang="ts">
|
|
import Modal from '$lib/components/common/Modal.svelte';
|
|
import { getContext } from 'svelte';
|
|
import { getModelChats, getModelOverview } from '$lib/apis/analytics';
|
|
import ModelActivityChart from '$lib/components/admin/Evaluations/ModelActivityChart.svelte';
|
|
import ChatList from '$lib/components/common/ChatList.svelte';
|
|
import XMark from '$lib/components/icons/XMark.svelte';
|
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
|
import { config } from '$lib/stores';
|
|
|
|
export let show = false;
|
|
export let model: { id: string; name: string } | null = null;
|
|
export let startDate: number | null = null;
|
|
export let endDate: number | null = null;
|
|
export let onClose: () => void = () => {};
|
|
|
|
const i18n = getContext('i18n');
|
|
|
|
type Tab = 'overview' | 'chats';
|
|
type ChatSortKey = 'title' | 'updated_at' | 'user_name';
|
|
let selectedTab: Tab = 'overview';
|
|
|
|
// Overview tab state
|
|
type TimeRange = '30d' | '1y' | 'all';
|
|
const TIME_RANGES: { key: TimeRange; label: string; days: number }[] = [
|
|
{ key: '30d', label: '30D', days: 30 },
|
|
{ key: '1y', label: '1Y', days: 365 },
|
|
{ key: 'all', label: 'All', days: 0 }
|
|
];
|
|
let selectedRange: TimeRange = '30d';
|
|
let history: Array<{ date: string; won: number; lost: number }> = [];
|
|
let tags: Array<{ tag: string; count: number }> = [];
|
|
let loadingOverview = false;
|
|
|
|
// Chats tab state
|
|
let chatList: Array<{
|
|
id: string;
|
|
title: string;
|
|
updated_at: number;
|
|
user_id?: string;
|
|
user_name?: string;
|
|
}> = [];
|
|
let chatListLoading = false;
|
|
let allChatsLoaded = false;
|
|
let chatOrderBy: ChatSortKey = 'updated_at';
|
|
let chatDirection: 'asc' | 'desc' = 'desc';
|
|
const PAGE_SIZE = 50;
|
|
|
|
const close = () => {
|
|
show = false;
|
|
selectedTab = 'overview';
|
|
chatList = [];
|
|
allChatsLoaded = false;
|
|
chatOrderBy = 'updated_at';
|
|
chatDirection = 'desc';
|
|
history = [];
|
|
tags = [];
|
|
onClose();
|
|
};
|
|
|
|
const loadOverview = async (days: number) => {
|
|
if (!model?.id) return;
|
|
loadingOverview = true;
|
|
try {
|
|
const result = await getModelOverview(localStorage.token, model.id, days);
|
|
history = result?.history ?? [];
|
|
tags = result?.tags ?? [];
|
|
} catch (err) {
|
|
console.error('Failed to load overview:', err);
|
|
history = [];
|
|
tags = [];
|
|
}
|
|
loadingOverview = false;
|
|
};
|
|
|
|
const selectRange = (range: TimeRange) => {
|
|
selectedRange = range;
|
|
const config = TIME_RANGES.find((r) => r.key === range);
|
|
if (config) {
|
|
loadOverview(config.days);
|
|
}
|
|
};
|
|
|
|
const loadChats = async () => {
|
|
if (!model?.id) return;
|
|
chatListLoading = true;
|
|
chatList = [];
|
|
allChatsLoaded = false;
|
|
try {
|
|
const res = await getModelChats(
|
|
localStorage.token,
|
|
model.id,
|
|
startDate,
|
|
endDate,
|
|
0,
|
|
PAGE_SIZE,
|
|
chatOrderBy,
|
|
chatDirection
|
|
);
|
|
const chats = res?.chats ?? [];
|
|
chatList = chats.map((c: any) => ({
|
|
id: c.chat_id,
|
|
title: c.first_message || 'No preview',
|
|
updated_at: c.updated_at,
|
|
user_id: c.user_id,
|
|
user_name: c.user_name
|
|
}));
|
|
allChatsLoaded = chatList.length >= (res?.total ?? chats.length);
|
|
} catch (err) {
|
|
console.error('Failed to load chats:', err);
|
|
chatList = [];
|
|
allChatsLoaded = true;
|
|
}
|
|
chatListLoading = false;
|
|
};
|
|
|
|
const loadMoreChats = async () => {
|
|
if (!model?.id || chatListLoading || allChatsLoaded) return;
|
|
chatListLoading = true;
|
|
try {
|
|
const skip = chatList.length;
|
|
const res = await getModelChats(
|
|
localStorage.token,
|
|
model.id,
|
|
startDate,
|
|
endDate,
|
|
skip,
|
|
PAGE_SIZE,
|
|
chatOrderBy,
|
|
chatDirection
|
|
);
|
|
const chats = res?.chats ?? [];
|
|
const newChats = chats.map((c: any) => ({
|
|
id: c.chat_id,
|
|
title: c.first_message || 'No preview',
|
|
updated_at: c.updated_at,
|
|
user_id: c.user_id,
|
|
user_name: c.user_name
|
|
}));
|
|
const existingIds = new Set(chatList.map((c) => c.id));
|
|
const uniqueNewChats = newChats.filter((c) => !existingIds.has(c.id));
|
|
chatList = [...chatList, ...uniqueNewChats];
|
|
allChatsLoaded = chatList.length >= (res?.total ?? chatList.length);
|
|
} catch (err) {
|
|
console.error('Failed to load more chats:', err);
|
|
}
|
|
chatListLoading = false;
|
|
};
|
|
|
|
const setChatSort = (key: ChatSortKey) => {
|
|
if (chatOrderBy === key) {
|
|
chatDirection = chatDirection === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
chatOrderBy = key;
|
|
chatDirection = key === 'updated_at' ? 'desc' : 'asc';
|
|
}
|
|
loadChats();
|
|
};
|
|
|
|
const selectTab = (tab: Tab) => {
|
|
selectedTab = tab;
|
|
if (tab === 'chats' && chatList.length === 0) {
|
|
loadChats();
|
|
}
|
|
};
|
|
|
|
// Load overview when modal opens
|
|
$: if (show && model?.id) {
|
|
selectedTab = 'overview';
|
|
chatList = [];
|
|
allChatsLoaded = false;
|
|
chatOrderBy = 'updated_at';
|
|
chatDirection = 'desc';
|
|
selectedRange = '30d';
|
|
loadOverview(30);
|
|
}
|
|
</script>
|
|
|
|
<Modal size="md" bind:show>
|
|
{#if model}
|
|
<div class="flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
|
|
<Tooltip content={`${model.name} (${model.id})`} placement="top-start">
|
|
<div class="text-lg font-normal self-center line-clamp-1">
|
|
{model.name}
|
|
</div>
|
|
</Tooltip>
|
|
<button class="self-center" on:click={close} aria-label="Close">
|
|
<XMark className={'size-5'} />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<div class="px-5 border-b border-gray-100 dark:border-gray-850">
|
|
<div class="flex gap-4">
|
|
<button
|
|
class="py-2 text-sm font-normal border-b-2 transition-colors {selectedTab === 'overview'
|
|
? 'border-black dark:border-white text-gray-900 dark:text-white'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'}"
|
|
on:click={() => selectTab('overview')}
|
|
>
|
|
{$i18n.t('Overview')}
|
|
</button>
|
|
{#if $config?.features?.enable_admin_chat_access}
|
|
<button
|
|
class="py-2 text-sm font-normal border-b-2 transition-colors {selectedTab === 'chats'
|
|
? 'border-black dark:border-white text-gray-900 dark:text-white'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'}"
|
|
on:click={() => selectTab('chats')}
|
|
>
|
|
{$i18n.t('Chats')}
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="px-5 pb-4 dark:text-gray-200">
|
|
{#if selectedTab === 'overview'}
|
|
<!-- Activity Chart -->
|
|
<div class="mb-4 mt-3">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<Tooltip content={$i18n.t('Thumbs up/down ratings from users on model responses')}>
|
|
<div class="text-xs text-gray-500 font-normal uppercase tracking-wide cursor-help">
|
|
{$i18n.t('Feedback Activity')}
|
|
</div>
|
|
</Tooltip>
|
|
<div
|
|
class="inline-flex rounded-full bg-gray-100/80 p-0.5 dark:bg-gray-800/80 backdrop-blur-sm"
|
|
>
|
|
{#each TIME_RANGES as range}
|
|
<button
|
|
type="button"
|
|
class="rounded-full transition-all duration-200 px-2.5 py-0.5 text-xs font-normal {selectedRange ===
|
|
range.key
|
|
? 'bg-white text-gray-900 shadow-sm dark:bg-gray-700 dark:text-white'
|
|
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'}"
|
|
on:click={() => selectRange(range.key)}
|
|
>
|
|
{range.label}
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
<ModelActivityChart
|
|
{history}
|
|
loading={loadingOverview}
|
|
aggregateWeekly={selectedRange === '1y' || selectedRange === 'all'}
|
|
/>
|
|
</div>
|
|
|
|
<!-- Tags -->
|
|
<div class="mb-4">
|
|
<div class="text-xs text-gray-500 mb-2 font-normal uppercase tracking-wide">
|
|
{$i18n.t('Tags')}
|
|
</div>
|
|
{#if tags.length}
|
|
<div class="flex flex-wrap gap-1 -mx-1">
|
|
{#each tags as tagInfo}
|
|
<span class="px-2 py-0.5 rounded-full bg-gray-100 dark:bg-gray-850 text-xs">
|
|
{tagInfo.tag} <span class="text-gray-500 font-normal">{tagInfo.count}</span>
|
|
</span>
|
|
{/each}
|
|
</div>
|
|
{:else}
|
|
<span class="text-gray-500 text-sm">-</span>
|
|
{/if}
|
|
</div>
|
|
{:else if selectedTab === 'chats'}
|
|
<div class="mt-3">
|
|
<ChatList
|
|
{chatList}
|
|
loading={chatListLoading}
|
|
allLoaded={allChatsLoaded}
|
|
showUserInfo={true}
|
|
shareUrl={true}
|
|
orderBy={chatOrderBy}
|
|
direction={chatDirection}
|
|
onSort={setChatSort}
|
|
onLoadMore={loadMoreChats}
|
|
onChatClick={() => (show = false)}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="flex justify-end pt-4">
|
|
<button
|
|
class="px-3.5 py-1.5 text-sm font-normal bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
|
type="button"
|
|
on:click={close}
|
|
>
|
|
{$i18n.t('Close')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</Modal>
|