chore: format
This commit is contained in:
@@ -50,7 +50,10 @@
|
||||
(e.currentTarget as HTMLElement).releasePointerCapture?.(e.pointerId);
|
||||
|
||||
// Skip if clicking the close button
|
||||
if (closeButtonElement && (e.target === closeButtonElement || closeButtonElement.contains(e.target as Node))) {
|
||||
if (
|
||||
closeButtonElement &&
|
||||
(e.target === closeButtonElement || closeButtonElement.contains(e.target as Node))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,13 @@
|
||||
let loadingOverview = false;
|
||||
|
||||
// Chats tab state
|
||||
let chatList: Array<{ id: string; title: string; updated_at: number; user_id?: string; user_name?: string }> = [];
|
||||
let chatList: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
updated_at: number;
|
||||
user_id?: string;
|
||||
user_name?: string;
|
||||
}> = [];
|
||||
let chatListLoading = false;
|
||||
let allChatsLoaded = false;
|
||||
const PAGE_SIZE = 50;
|
||||
@@ -76,7 +82,14 @@
|
||||
chatList = [];
|
||||
allChatsLoaded = false;
|
||||
try {
|
||||
const res = await getModelChats(localStorage.token, model.id, startDate, endDate, 0, PAGE_SIZE);
|
||||
const res = await getModelChats(
|
||||
localStorage.token,
|
||||
model.id,
|
||||
startDate,
|
||||
endDate,
|
||||
0,
|
||||
PAGE_SIZE
|
||||
);
|
||||
const chats = res?.chats ?? [];
|
||||
chatList = chats.map((c: any) => ({
|
||||
id: c.chat_id,
|
||||
@@ -99,7 +112,14 @@
|
||||
chatListLoading = true;
|
||||
try {
|
||||
const skip = chatList.length;
|
||||
const res = await getModelChats(localStorage.token, model.id, startDate, endDate, skip, PAGE_SIZE);
|
||||
const res = await getModelChats(
|
||||
localStorage.token,
|
||||
model.id,
|
||||
startDate,
|
||||
endDate,
|
||||
skip,
|
||||
PAGE_SIZE
|
||||
);
|
||||
const chats = res?.chats ?? [];
|
||||
const newChats = chats.map((c: any) => ({
|
||||
id: c.chat_id,
|
||||
@@ -157,14 +177,14 @@
|
||||
{$i18n.t('Overview')}
|
||||
</button>
|
||||
{#if $config?.features?.enable_admin_chat_access}
|
||||
<button
|
||||
class="py-2 text-sm font-medium 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>
|
||||
<button
|
||||
class="py-2 text-sm font-medium 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>
|
||||
@@ -175,10 +195,10 @@
|
||||
<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-medium uppercase tracking-wide cursor-help">
|
||||
{$i18n.t('Feedback Activity')}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div class="text-xs text-gray-500 font-medium 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"
|
||||
>
|
||||
|
||||
@@ -83,7 +83,11 @@
|
||||
{@const labelCount = Math.min(7, data.length)}
|
||||
{@const step = labelCount > 1 ? Math.floor((data.length - 1) / (labelCount - 1)) || 1 : 1}
|
||||
{@const isHourly = data[0]?.date?.includes(':')}
|
||||
{@const dateFormat = isHourly ? 'h A' : period === 'year' || period === 'all' ? 'M/D/YY' : 'M/D'}
|
||||
{@const dateFormat = isHourly
|
||||
? 'h A'
|
||||
: period === 'year' || period === 'all'
|
||||
? 'M/D/YY'
|
||||
: 'M/D'}
|
||||
<div class="flex justify-between px-0.5 text-[10px] text-gray-400">
|
||||
{#each Array(labelCount) as _, i}
|
||||
{@const idx = i === labelCount - 1 ? data.length - 1 : Math.min(i * step, data.length - 1)}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { models } from '$lib/stores';
|
||||
import { getSummary, getModelAnalytics, getUserAnalytics, getDailyStats, getTokenUsage } from '$lib/apis/analytics';
|
||||
import {
|
||||
getSummary,
|
||||
getModelAnalytics,
|
||||
getUserAnalytics,
|
||||
getDailyStats,
|
||||
getTokenUsage
|
||||
} from '$lib/apis/analytics';
|
||||
import { getGroups } from '$lib/apis/groups';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||
@@ -16,7 +22,8 @@
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
// Time period - persist in localStorage
|
||||
let selectedPeriod = (typeof localStorage !== 'undefined' && localStorage.getItem('analyticsPeriod')) || '7d';
|
||||
let selectedPeriod =
|
||||
(typeof localStorage !== 'undefined' && localStorage.getItem('analyticsPeriod')) || '7d';
|
||||
const periods = [
|
||||
{ value: '24h', label: 'Last 24 hours' },
|
||||
{ value: '7d', label: 'Last 7 days' },
|
||||
@@ -33,11 +40,16 @@
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const day = 86400;
|
||||
switch (period) {
|
||||
case '24h': return { start: now - day, end: now };
|
||||
case '7d': return { start: now - 7 * day, end: now };
|
||||
case '30d': return { start: now - 30 * day, end: now };
|
||||
case '90d': return { start: now - 90 * day, end: now };
|
||||
default: return { start: null, end: null };
|
||||
case '24h':
|
||||
return { start: now - day, end: now };
|
||||
case '7d':
|
||||
return { start: now - 7 * day, end: now };
|
||||
case '30d':
|
||||
return { start: now - 30 * day, end: now };
|
||||
case '90d':
|
||||
return { start: now - 90 * day, end: now };
|
||||
default:
|
||||
return { start: null, end: null };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -46,7 +58,10 @@
|
||||
let modelStats: Array<{ model_id: string; count: number; name?: string }> = [];
|
||||
let userStats: Array<{ user_id: string; name?: string; email?: string; count: number }> = [];
|
||||
let dailyStats: Array<{ date: string; models: Record<string, number> }> = [];
|
||||
let tokenStats: Record<string, { input_tokens: number; output_tokens: number; total_tokens: number }> = {};
|
||||
let tokenStats: Record<
|
||||
string,
|
||||
{ input_tokens: number; output_tokens: number; total_tokens: number }
|
||||
> = {};
|
||||
let totalTokens = { input: 0, output: 0, total: 0 };
|
||||
|
||||
let loading = true;
|
||||
@@ -141,9 +156,7 @@
|
||||
|
||||
$: sortedModels = [...modelStats].sort((a, b) => {
|
||||
if (modelOrderBy === 'name') {
|
||||
return modelDirection === 'asc'
|
||||
? a.name.localeCompare(b.name)
|
||||
: b.name.localeCompare(a.name);
|
||||
return modelDirection === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
|
||||
}
|
||||
return modelDirection === 'asc' ? a.count - b.count : b.count - a.count;
|
||||
});
|
||||
@@ -152,9 +165,7 @@
|
||||
if (userOrderBy === 'name') {
|
||||
const nameA = a.name || a.user_id;
|
||||
const nameB = b.name || b.user_id;
|
||||
return userDirection === 'asc'
|
||||
? nameA.localeCompare(nameB)
|
||||
: nameB.localeCompare(nameA);
|
||||
return userDirection === 'asc' ? nameA.localeCompare(nameB) : nameB.localeCompare(nameA);
|
||||
}
|
||||
return userDirection === 'asc' ? a.count - b.count : b.count - a.count;
|
||||
});
|
||||
@@ -170,7 +181,9 @@
|
||||
</script>
|
||||
|
||||
<!-- Header with title and period selector -->
|
||||
<div class="pt-0.5 pb-1 gap-1 flex flex-row justify-between items-center sticky top-0 z-10 bg-white dark:bg-gray-900">
|
||||
<div
|
||||
class="pt-0.5 pb-1 gap-1 flex flex-row justify-between items-center sticky top-0 z-10 bg-white dark:bg-gray-900"
|
||||
>
|
||||
<div class="text-lg font-medium px-0.5">
|
||||
{$i18n.t('Analytics')}
|
||||
</div>
|
||||
@@ -208,27 +221,54 @@
|
||||
<!-- Summary stats -->
|
||||
{#if !loading}
|
||||
<div class="flex gap-3 text-xs text-gray-500 dark:text-gray-400 px-0.5 pb-2">
|
||||
<span><span class="font-medium text-gray-900 dark:text-gray-300">{summary.total_messages.toLocaleString()}</span> {$i18n.t('messages')}</span>
|
||||
<span
|
||||
><span class="font-medium text-gray-900 dark:text-gray-300"
|
||||
>{summary.total_messages.toLocaleString()}</span
|
||||
>
|
||||
{$i18n.t('messages')}</span
|
||||
>
|
||||
<Tooltip content={$i18n.t('Token counts are estimates and may not reflect actual API usage')}>
|
||||
<span class="cursor-help"><span class="font-medium text-gray-900 dark:text-gray-300">{formatNumber(totalTokens.total)}</span> {$i18n.t('tokens')}</span>
|
||||
<span class="cursor-help"
|
||||
><span class="font-medium text-gray-900 dark:text-gray-300"
|
||||
>{formatNumber(totalTokens.total)}</span
|
||||
>
|
||||
{$i18n.t('tokens')}</span
|
||||
>
|
||||
</Tooltip>
|
||||
<span><span class="font-medium text-gray-900 dark:text-gray-300">{summary.total_chats.toLocaleString()}</span> {$i18n.t('chats')}</span>
|
||||
<span><span class="font-medium text-gray-900 dark:text-gray-300">{summary.total_users}</span> {$i18n.t('users')}</span>
|
||||
<span
|
||||
><span class="font-medium text-gray-900 dark:text-gray-300"
|
||||
>{summary.total_chats.toLocaleString()}</span
|
||||
>
|
||||
{$i18n.t('chats')}</span
|
||||
>
|
||||
<span
|
||||
><span class="font-medium text-gray-900 dark:text-gray-300">{summary.total_users}</span>
|
||||
{$i18n.t('users')}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Daily usage chart -->
|
||||
{#if dailyStats.length > 1}
|
||||
{@const allModels = [...new Set(dailyStats.flatMap(d => Object.keys(d.models || {})))]}
|
||||
{@const allModels = [...new Set(dailyStats.flatMap((d) => Object.keys(d.models || {})))]}
|
||||
{@const topModels = allModels.slice(0, 8)}
|
||||
{@const chartColors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#06b6d4', '#84cc16']}
|
||||
{@const periodMap = { '24h': 'hour', '7d': 'week', '30d': 'month', '90d': 'year', 'all': 'all' }}
|
||||
{@const chartColors = [
|
||||
'#3b82f6',
|
||||
'#10b981',
|
||||
'#f59e0b',
|
||||
'#ef4444',
|
||||
'#8b5cf6',
|
||||
'#ec4899',
|
||||
'#06b6d4',
|
||||
'#84cc16'
|
||||
]}
|
||||
{@const periodMap = { '24h': 'hour', '7d': 'week', '30d': 'month', '90d': 'year', all: 'all' }}
|
||||
<div class="mb-4">
|
||||
<div class="text-xs font-medium text-gray-600 dark:text-gray-400 mb-2 px-0.5">
|
||||
{$i18n.t(selectedPeriod === '24h' ? 'Hourly Messages' : 'Daily Messages')}
|
||||
</div>
|
||||
<ChartLine
|
||||
data={dailyStats}
|
||||
models={topModels}
|
||||
<ChartLine
|
||||
data={dailyStats}
|
||||
models={topModels}
|
||||
colors={chartColors}
|
||||
height={200}
|
||||
period={periodMap[selectedPeriod] || 'week'}
|
||||
@@ -262,7 +302,9 @@
|
||||
{$i18n.t('Model')}
|
||||
{#if modelOrderBy === 'name'}
|
||||
<span class="font-normal">
|
||||
{#if modelDirection === 'asc'}<ChevronUp className="size-2" />{:else}<ChevronDown className="size-2" />{/if}
|
||||
{#if modelDirection === 'asc'}<ChevronUp
|
||||
className="size-2"
|
||||
/>{:else}<ChevronDown className="size-2" />{/if}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="invisible"><ChevronUp className="size-2" /></span>
|
||||
@@ -278,7 +320,9 @@
|
||||
{$i18n.t('Messages')}
|
||||
{#if modelOrderBy === 'count'}
|
||||
<span class="font-normal">
|
||||
{#if modelDirection === 'asc'}<ChevronUp className="size-2" />{:else}<ChevronDown className="size-2" />{/if}
|
||||
{#if modelDirection === 'asc'}<ChevronUp
|
||||
className="size-2"
|
||||
/>{:else}<ChevronDown className="size-2" />{/if}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="invisible"><ChevronUp className="size-2" /></span>
|
||||
@@ -292,9 +336,12 @@
|
||||
<tbody>
|
||||
{#each sortedModels as model, idx (model.model_id)}
|
||||
<tr
|
||||
class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
on:click={() => { selectedModel = { id: model.model_id, name: model.name }; showModelModal = true; }}
|
||||
>
|
||||
class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
on:click={() => {
|
||||
selectedModel = { id: model.model_id, name: model.name };
|
||||
showModelModal = true;
|
||||
}}
|
||||
>
|
||||
<td class="px-3 py-1 text-gray-400">{idx + 1}</td>
|
||||
<td class="px-3 py-1 font-medium text-gray-900 dark:text-white">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -307,14 +354,22 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-1 text-right">{model.count.toLocaleString()}</td>
|
||||
<td class="px-3 py-1 text-right">{formatNumber(tokenStats[model.model_id]?.total_tokens ?? 0)}</td>
|
||||
<td class="px-3 py-1 text-right"
|
||||
>{formatNumber(tokenStats[model.model_id]?.total_tokens ?? 0)}</td
|
||||
>
|
||||
<td class="px-3 py-1 text-right text-gray-400">
|
||||
{totalModelMessages > 0 ? ((model.count / totalModelMessages) * 100).toFixed(1) : 0}%
|
||||
{totalModelMessages > 0
|
||||
? ((model.count / totalModelMessages) * 100).toFixed(1)
|
||||
: 0}%
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{#if sortedModels.length === 0}
|
||||
<tr><td colspan="5" class="px-3 py-2 text-center text-gray-400">{$i18n.t('No data')}</td></tr>
|
||||
<tr
|
||||
><td colspan="5" class="px-3 py-2 text-center text-gray-400"
|
||||
>{$i18n.t('No data')}</td
|
||||
></tr
|
||||
>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -340,7 +395,9 @@
|
||||
{$i18n.t('User')}
|
||||
{#if userOrderBy === 'name'}
|
||||
<span class="font-normal">
|
||||
{#if userDirection === 'asc'}<ChevronUp className="size-2" />{:else}<ChevronDown className="size-2" />{/if}
|
||||
{#if userDirection === 'asc'}<ChevronUp
|
||||
className="size-2"
|
||||
/>{:else}<ChevronDown className="size-2" />{/if}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="invisible"><ChevronUp className="size-2" /></span>
|
||||
@@ -356,7 +413,9 @@
|
||||
{$i18n.t('Messages')}
|
||||
{#if userOrderBy === 'count'}
|
||||
<span class="font-normal">
|
||||
{#if userDirection === 'asc'}<ChevronUp className="size-2" />{:else}<ChevronDown className="size-2" />{/if}
|
||||
{#if userDirection === 'asc'}<ChevronUp
|
||||
className="size-2"
|
||||
/>{:else}<ChevronDown className="size-2" />{/if}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="invisible"><ChevronUp className="size-2" /></span>
|
||||
@@ -377,7 +436,9 @@
|
||||
alt={user.name || 'User'}
|
||||
class="size-5 rounded-full object-cover shrink-0"
|
||||
/>
|
||||
<span class="truncate max-w-[150px]">{user.name || user.email || user.user_id.substring(0, 8)}</span>
|
||||
<span class="truncate max-w-[150px]"
|
||||
>{user.name || user.email || user.user_id.substring(0, 8)}</span
|
||||
>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-1 text-right">{user.count.toLocaleString()}</td>
|
||||
@@ -385,7 +446,11 @@
|
||||
</tr>
|
||||
{/each}
|
||||
{#if sortedUsers.length === 0}
|
||||
<tr><td colspan="4" class="px-3 py-2 text-center text-gray-400">{$i18n.t('No data')}</td></tr>
|
||||
<tr
|
||||
><td colspan="4" class="px-3 py-2 text-center text-gray-400"
|
||||
>{$i18n.t('No data')}</td
|
||||
></tr
|
||||
>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -41,9 +41,7 @@
|
||||
|
||||
$: sortedModels = [...modelStats].sort((a, b) => {
|
||||
if (orderBy === 'name') {
|
||||
return direction === 'asc'
|
||||
? a.name.localeCompare(b.name)
|
||||
: b.name.localeCompare(a.name);
|
||||
return direction === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
|
||||
}
|
||||
return direction === 'asc' ? a.count - b.count : b.count - a.count;
|
||||
});
|
||||
@@ -53,16 +51,22 @@
|
||||
onMount(loadAnalytics);
|
||||
</script>
|
||||
|
||||
<div class="pt-0.5 pb-1 gap-1 flex flex-col md:flex-row justify-between sticky top-0 z-10 bg-white dark:bg-gray-900">
|
||||
<div
|
||||
class="pt-0.5 pb-1 gap-1 flex flex-col md:flex-row justify-between sticky top-0 z-10 bg-white dark:bg-gray-900"
|
||||
>
|
||||
<div class="flex items-center text-xl font-medium px-0.5 gap-2 shrink-0">
|
||||
{$i18n.t('Model Usage')}
|
||||
<span class="text-lg text-gray-500">{totalMessages} {$i18n.t('messages')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded-sm min-h-[100px]">
|
||||
<div
|
||||
class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded-sm min-h-[100px]"
|
||||
>
|
||||
{#if loading}
|
||||
<div class="absolute inset-0 flex items-center justify-center z-10 bg-white/50 dark:bg-gray-900/50">
|
||||
<div
|
||||
class="absolute inset-0 flex items-center justify-center z-10 bg-white/50 dark:bg-gray-900/50"
|
||||
>
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
{/if}
|
||||
@@ -70,7 +74,11 @@
|
||||
{#if !modelStats.length && !loading}
|
||||
<div class="text-center text-xs text-gray-500 py-1">{$i18n.t('No data found')}</div>
|
||||
{:else if modelStats.length}
|
||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400 {loading ? 'opacity-20' : ''}">
|
||||
<table
|
||||
class="w-full text-sm text-left text-gray-500 dark:text-gray-400 {loading
|
||||
? 'opacity-20'
|
||||
: ''}"
|
||||
>
|
||||
<thead class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200">
|
||||
<tr class="border-b-[1.5px] border-gray-50 dark:border-gray-850/30">
|
||||
<th scope="col" class="px-2.5 py-2 w-8">#</th>
|
||||
@@ -82,7 +90,9 @@
|
||||
<div class="flex gap-1.5 items-center">
|
||||
{$i18n.t('Model')}
|
||||
{#if orderBy === 'name'}
|
||||
{#if direction === 'asc'}<ChevronUp className="size-2" />{:else}<ChevronDown className="size-2" />{/if}
|
||||
{#if direction === 'asc'}<ChevronUp className="size-2" />{:else}<ChevronDown
|
||||
className="size-2"
|
||||
/>{/if}
|
||||
{:else}
|
||||
<span class="invisible"><ChevronUp className="size-2" /></span>
|
||||
{/if}
|
||||
@@ -96,7 +106,9 @@
|
||||
<div class="flex gap-1.5 items-center justify-end">
|
||||
{$i18n.t('Messages')}
|
||||
{#if orderBy === 'count'}
|
||||
{#if direction === 'asc'}<ChevronUp className="size-2" />{:else}<ChevronDown className="size-2" />{/if}
|
||||
{#if direction === 'asc'}<ChevronUp className="size-2" />{:else}<ChevronDown
|
||||
className="size-2"
|
||||
/>{/if}
|
||||
{:else}
|
||||
<span class="invisible"><ChevronUp className="size-2" /></span>
|
||||
{/if}
|
||||
@@ -107,7 +119,9 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each sortedModels as model, idx (model.model_id)}
|
||||
<tr class="bg-white dark:bg-gray-900 text-xs hover:bg-gray-50 dark:hover:bg-gray-850/50 transition">
|
||||
<tr
|
||||
class="bg-white dark:bg-gray-900 text-xs hover:bg-gray-50 dark:hover:bg-gray-850/50 transition"
|
||||
>
|
||||
<td class="px-3 py-1.5 font-medium text-gray-900 dark:text-white">
|
||||
{idx + 1}
|
||||
</td>
|
||||
|
||||
@@ -46,16 +46,22 @@
|
||||
onMount(loadAnalytics);
|
||||
</script>
|
||||
|
||||
<div class="pt-0.5 pb-1 gap-1 flex flex-col md:flex-row justify-between sticky top-0 z-10 bg-white dark:bg-gray-900">
|
||||
<div
|
||||
class="pt-0.5 pb-1 gap-1 flex flex-col md:flex-row justify-between sticky top-0 z-10 bg-white dark:bg-gray-900"
|
||||
>
|
||||
<div class="flex items-center text-xl font-medium px-0.5 gap-2 shrink-0">
|
||||
{$i18n.t('User Activity')}
|
||||
<span class="text-lg text-gray-500">{userStats.length} {$i18n.t('users')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded-sm min-h-[100px]">
|
||||
<div
|
||||
class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded-sm min-h-[100px]"
|
||||
>
|
||||
{#if loading}
|
||||
<div class="absolute inset-0 flex items-center justify-center z-10 bg-white/50 dark:bg-gray-900/50">
|
||||
<div
|
||||
class="absolute inset-0 flex items-center justify-center z-10 bg-white/50 dark:bg-gray-900/50"
|
||||
>
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
{/if}
|
||||
@@ -63,7 +69,11 @@
|
||||
{#if !userStats.length && !loading}
|
||||
<div class="text-center text-xs text-gray-500 py-1">{$i18n.t('No data found')}</div>
|
||||
{:else if userStats.length}
|
||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400 {loading ? 'opacity-20' : ''}">
|
||||
<table
|
||||
class="w-full text-sm text-left text-gray-500 dark:text-gray-400 {loading
|
||||
? 'opacity-20'
|
||||
: ''}"
|
||||
>
|
||||
<thead class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200">
|
||||
<tr class="border-b-[1.5px] border-gray-50 dark:border-gray-850/30">
|
||||
<th scope="col" class="px-2.5 py-2 w-8">#</th>
|
||||
@@ -75,7 +85,9 @@
|
||||
<div class="flex gap-1.5 items-center">
|
||||
{$i18n.t('User')}
|
||||
{#if orderBy === 'user_id'}
|
||||
{#if direction === 'asc'}<ChevronUp className="size-2" />{:else}<ChevronDown className="size-2" />{/if}
|
||||
{#if direction === 'asc'}<ChevronUp className="size-2" />{:else}<ChevronDown
|
||||
className="size-2"
|
||||
/>{/if}
|
||||
{:else}
|
||||
<span class="invisible"><ChevronUp className="size-2" /></span>
|
||||
{/if}
|
||||
@@ -89,7 +101,9 @@
|
||||
<div class="flex gap-1.5 items-center justify-end">
|
||||
{$i18n.t('Messages')}
|
||||
{#if orderBy === 'count'}
|
||||
{#if direction === 'asc'}<ChevronUp className="size-2" />{:else}<ChevronDown className="size-2" />{/if}
|
||||
{#if direction === 'asc'}<ChevronUp className="size-2" />{:else}<ChevronDown
|
||||
className="size-2"
|
||||
/>{/if}
|
||||
{:else}
|
||||
<span class="invisible"><ChevronUp className="size-2" /></span>
|
||||
{/if}
|
||||
@@ -100,7 +114,9 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each sortedUsers as user, idx (user.user_id)}
|
||||
<tr class="bg-white dark:bg-gray-900 text-xs hover:bg-gray-50 dark:hover:bg-gray-850/50 transition">
|
||||
<tr
|
||||
class="bg-white dark:bg-gray-900 text-xs hover:bg-gray-50 dark:hover:bg-gray-850/50 transition"
|
||||
>
|
||||
<td class="px-3 py-1.5 font-medium text-gray-900 dark:text-white">
|
||||
{idx + 1}
|
||||
</td>
|
||||
|
||||
@@ -303,10 +303,12 @@
|
||||
<div class="flex flex-col h-full">
|
||||
{#if feedback.data?.sibling_model_ids}
|
||||
<Tooltip content={feedback.data?.model_id} placement="top-start">
|
||||
<div class="font-medium text-gray-600 dark:text-gray-400 flex-1 line-clamp-1">
|
||||
{feedback.data?.model_id}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div
|
||||
class="font-medium text-gray-600 dark:text-gray-400 flex-1 line-clamp-1"
|
||||
>
|
||||
{feedback.data?.model_id}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={feedback.data.sibling_model_ids.join(', ')}>
|
||||
<div class=" text-[0.65rem] text-gray-600 dark:text-gray-400 line-clamp-1">
|
||||
@@ -323,10 +325,12 @@
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<Tooltip content={feedback.data?.model_id} placement="top-start">
|
||||
<div class="text-sm font-medium text-gray-600 dark:text-gray-400 flex-1 py-1.5 line-clamp-1">
|
||||
{feedback.data?.model_id}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div
|
||||
class="text-sm font-medium text-gray-600 dark:text-gray-400 flex-1 py-1.5 line-clamp-1"
|
||||
>
|
||||
{feedback.data?.model_id}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -183,7 +183,9 @@
|
||||
class="size-5 rounded-full object-cover"
|
||||
/>
|
||||
<Tooltip content={`${model.name} (${model.id})`} placement="top-start">
|
||||
<span class="font-medium text-gray-800 dark:text-gray-200 line-clamp-1">{model.name}</span>
|
||||
<span class="font-medium text-gray-800 dark:text-gray-200 line-clamp-1"
|
||||
>{model.name}</span
|
||||
>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -104,7 +104,9 @@
|
||||
modelsToDisable.forEach((m) => (m.is_active = false));
|
||||
models = models;
|
||||
// Sync with server
|
||||
await Promise.all(modelsToDisable.map((model) => toggleModelById(localStorage.token, model.id)));
|
||||
await Promise.all(
|
||||
modelsToDisable.map((model) => toggleModelById(localStorage.token, model.id))
|
||||
);
|
||||
};
|
||||
|
||||
const downloadModels = async (models) => {
|
||||
|
||||
@@ -738,7 +738,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{:else if webConfig.WEB_SEARCH_ENGINE === 'yandex'}
|
||||
<div class="mb-2.5 flex w-full flex-col">
|
||||
<div class="mb-2.5 flex w-full flex-col">
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Yandex Web Search URL')}
|
||||
@@ -769,20 +769,22 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-2.5">
|
||||
<div class=" mb-1 text-xs font-medium">{$i18n.t('Yandex Web Search config')}</div>
|
||||
<div class=" mb-1 text-xs font-medium">{$i18n.t('Yandex Web Search config')}</div>
|
||||
|
||||
<Tooltip
|
||||
content={$i18n.t('Leave empty to use the default config, or enter a valid json (see https://yandex.cloud/en/docs/search-api/api-ref/WebSearch/search#yandex.cloud.searchapi.v2.WebSearchRequest)')}
|
||||
placement="top-start"
|
||||
>
|
||||
<Textarea
|
||||
bind:value={webConfig.YANDEX_WEB_SEARCH_CONFIG}
|
||||
placeholder={$i18n.t(
|
||||
'Leave empty to use the default config, or enter a valid json (see https://yandex.cloud/en/docs/search-api/api-ref/WebSearch/search#yandex.cloud.searchapi.v2.WebSearchRequest)'
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'Leave empty to use the default config, or enter a valid json (see https://yandex.cloud/en/docs/search-api/api-ref/WebSearch/search#yandex.cloud.searchapi.v2.WebSearchRequest)'
|
||||
)}
|
||||
placement="top-start"
|
||||
>
|
||||
<Textarea
|
||||
bind:value={webConfig.YANDEX_WEB_SEARCH_CONFIG}
|
||||
placeholder={$i18n.t(
|
||||
'Leave empty to use the default config, or enter a valid json (see https://yandex.cloud/en/docs/search-api/api-ref/WebSearch/search#yandex.cloud.searchapi.v2.WebSearchRequest)'
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -201,7 +201,7 @@
|
||||
if (storedQueueData) {
|
||||
try {
|
||||
const restoredQueue = JSON.parse(storedQueueData);
|
||||
|
||||
|
||||
if (restoredQueue.length > 0) {
|
||||
sessionStorage.removeItem(`chat-queue-${chatIdProp}`);
|
||||
// Check if there are pending tasks (still generating)
|
||||
@@ -1971,8 +1971,8 @@
|
||||
..._messages.map((message) => ({
|
||||
...message,
|
||||
content: processDetails(message.content),
|
||||
// Include output for temp chats (backend will use it and strip before LLM)
|
||||
...(message.output ? { output: message.output } : {})
|
||||
// Include output for temp chats (backend will use it and strip before LLM)
|
||||
...(message.output ? { output: message.output } : {})
|
||||
}))
|
||||
].filter((message) => message);
|
||||
|
||||
@@ -2027,7 +2027,8 @@
|
||||
const skillMentionRegex = /<\$([^|>]+)\|?[^>]*>/g;
|
||||
const skillIds = [];
|
||||
for (const message of messages) {
|
||||
const content = typeof message.content === 'string' ? message.content : message.content?.[0]?.text ?? '';
|
||||
const content =
|
||||
typeof message.content === 'string' ? message.content : (message.content?.[0]?.text ?? '');
|
||||
for (const match of content.matchAll(skillMentionRegex)) {
|
||||
if (!skillIds.includes(match[1])) {
|
||||
skillIds.push(match[1]);
|
||||
|
||||
@@ -172,7 +172,9 @@
|
||||
>{$i18n.t('Select a tool')}</option
|
||||
>
|
||||
|
||||
{#each $tools.filter((tool) => !tool?.id?.startsWith('server:')).sort((a, b) => (a.name ?? '').localeCompare(b.name ?? '')) as tool, toolIdx}
|
||||
{#each $tools
|
||||
.filter((tool) => !tool?.id?.startsWith('server:'))
|
||||
.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? '')) as tool, toolIdx}
|
||||
<option value={tool.id} class="bg-gray-100 dark:bg-gray-800">{tool.name}</option>
|
||||
{/each}
|
||||
{:else if tab === 'functions'}
|
||||
@@ -180,7 +182,7 @@
|
||||
>{$i18n.t('Select a function')}</option
|
||||
>
|
||||
|
||||
{#each $functions.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? '')) as func, funcIdx}
|
||||
{#each $functions.sort( (a, b) => (a.name ?? '').localeCompare(b.name ?? '') ) as func, funcIdx}
|
||||
<option value={func.id} class="bg-gray-100 dark:bg-gray-800">{func.name}</option>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
@@ -390,7 +390,8 @@
|
||||
|
||||
let command = '';
|
||||
export let showCommands = false;
|
||||
$: showCommands = ['/', '#', '@', '$'].includes(command?.charAt(0)) || '\\#' === command?.slice(0, 2);
|
||||
$: showCommands =
|
||||
['/', '#', '@', '$'].includes(command?.charAt(0)) || '\\#' === command?.slice(0, 2);
|
||||
let suggestions = null;
|
||||
|
||||
let showTools = false;
|
||||
|
||||
@@ -43,7 +43,11 @@
|
||||
stroke="currentColor"
|
||||
class="size-3.5"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 10.5 12 3m0 0 7.5 7.5M12 3v18" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M4.5 10.5 12 3m0 0 7.5 7.5M12 3v18"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
@@ -46,7 +46,11 @@
|
||||
marked.use(footnoteExtension(options));
|
||||
marked.use(disableSingleTilde);
|
||||
marked.use({
|
||||
extensions: [mentionExtension({ triggerChar: '@' }), mentionExtension({ triggerChar: '#' }), mentionExtension({ triggerChar: '$' })]
|
||||
extensions: [
|
||||
mentionExtension({ triggerChar: '@' }),
|
||||
mentionExtension({ triggerChar: '#' }),
|
||||
mentionExtension({ triggerChar: '$' })
|
||||
]
|
||||
});
|
||||
|
||||
$: (async () => {
|
||||
|
||||
@@ -258,8 +258,12 @@
|
||||
class="flex cursor-pointer items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-850 w-full px-3 py-2 rounded-xl transition"
|
||||
>
|
||||
<div class="self-center">
|
||||
<div class="text-sm font-medium">{$i18n.t('Leave a public review for {{modelName}}', { modelName: message.model })}</div>
|
||||
<div class="text-xs text-gray-500">{$i18n.t('Help the community discover great models')}</div>
|
||||
<div class="text-sm font-medium">
|
||||
{$i18n.t('Leave a public review for {{modelName}}', { modelName: message.model })}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{$i18n.t('Help the community discover great models')}
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight className="size-4" />
|
||||
</a>
|
||||
|
||||
@@ -105,7 +105,10 @@
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
window.open(`https://openwebui.com/models?q=${encodeURIComponent(model?.id ?? '')}`, '_blank');
|
||||
window.open(
|
||||
`https://openwebui.com/models?q=${encodeURIComponent(model?.id ?? '')}`,
|
||||
'_blank'
|
||||
);
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -44,78 +44,78 @@
|
||||
</div>
|
||||
{/if}
|
||||
<div class="max-h-[22rem] overflow-y-scroll">
|
||||
{#if loading && (!chatList || chatList.length === 0)}
|
||||
<div class="flex justify-center py-8">
|
||||
<Spinner />
|
||||
</div>
|
||||
{:else if !chatList || chatList.length === 0}
|
||||
<div class="text-center text-gray-500 text-sm py-8">
|
||||
{$i18n.t(emptyMessage)}
|
||||
</div>
|
||||
{:else}
|
||||
{#each chatList as chat, idx (chat.id)}
|
||||
{#if chat.time_range && (idx === 0 || chat.time_range !== chatList[idx - 1]?.time_range)}
|
||||
<div
|
||||
class="w-full text-xs text-gray-500 dark:text-gray-500 font-medium {idx === 0
|
||||
? ''
|
||||
: 'pt-5'} pb-2 px-2"
|
||||
>
|
||||
{$i18n.t(chat.time_range)}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="w-full flex items-center rounded-lg text-sm py-2 px-3 hover:bg-gray-50 dark:hover:bg-gray-850"
|
||||
>
|
||||
{#if showUserInfo && chat.user_id}
|
||||
<div class="w-32 shrink-0 flex items-center gap-2">
|
||||
<img
|
||||
src="{WEBUI_API_BASE_URL}/users/{chat.user_id}/profile/image"
|
||||
alt={chat.user_name || 'User'}
|
||||
class="size-5 rounded-full object-cover shrink-0"
|
||||
/>
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400 truncate"
|
||||
>{chat.user_name || 'Unknown'}</span
|
||||
>
|
||||
{#if loading && (!chatList || chatList.length === 0)}
|
||||
<div class="flex justify-center py-8">
|
||||
<Spinner />
|
||||
</div>
|
||||
{:else if !chatList || chatList.length === 0}
|
||||
<div class="text-center text-gray-500 text-sm py-8">
|
||||
{$i18n.t(emptyMessage)}
|
||||
</div>
|
||||
{:else}
|
||||
{#each chatList as chat, idx (chat.id)}
|
||||
{#if chat.time_range && (idx === 0 || chat.time_range !== chatList[idx - 1]?.time_range)}
|
||||
<div
|
||||
class="w-full text-xs text-gray-500 dark:text-gray-500 font-medium {idx === 0
|
||||
? ''
|
||||
: 'pt-5'} pb-2 px-2"
|
||||
>
|
||||
{$i18n.t(chat.time_range)}
|
||||
</div>
|
||||
{/if}
|
||||
<a
|
||||
class={showUserInfo ? 'flex-1' : 'basis-3/5'}
|
||||
href={shareUrl ? `/s/${chat.id}` : `/c/${chat.id}`}
|
||||
on:click={() => onChatClick?.(chat.id)}
|
||||
|
||||
<div
|
||||
class="w-full flex items-center rounded-lg text-sm py-2 px-3 hover:bg-gray-50 dark:hover:bg-gray-850"
|
||||
>
|
||||
<div class="text-ellipsis line-clamp-1 w-full">
|
||||
{chat.title}
|
||||
</div>
|
||||
</a>
|
||||
{#if showUserInfo && chat.user_id}
|
||||
<div class="w-32 shrink-0 flex items-center gap-2">
|
||||
<img
|
||||
src="{WEBUI_API_BASE_URL}/users/{chat.user_id}/profile/image"
|
||||
alt={chat.user_name || 'User'}
|
||||
class="size-5 rounded-full object-cover shrink-0"
|
||||
/>
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400 truncate"
|
||||
>{chat.user_name || 'Unknown'}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
<a
|
||||
class={showUserInfo ? 'flex-1' : 'basis-3/5'}
|
||||
href={shareUrl ? `/s/${chat.id}` : `/c/${chat.id}`}
|
||||
on:click={() => onChatClick?.(chat.id)}
|
||||
>
|
||||
<div class="text-ellipsis line-clamp-1 w-full">
|
||||
{chat.title}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="{showUserInfo ? 'w-28' : 'basis-2/5'} flex items-center justify-end">
|
||||
<div class="hidden sm:flex text-gray-500 dark:text-gray-400 text-xs">
|
||||
{dayjs(chat.updated_at * 1000).calendar(null, {
|
||||
sameDay: '[Today] h:mm A',
|
||||
lastDay: '[Yesterday] h:mm A',
|
||||
lastWeek: 'MMM D',
|
||||
sameElse: 'MMM D, YYYY'
|
||||
})}
|
||||
<div class="{showUserInfo ? 'w-28' : 'basis-2/5'} flex items-center justify-end">
|
||||
<div class="hidden sm:flex text-gray-500 dark:text-gray-400 text-xs">
|
||||
{dayjs(chat.updated_at * 1000).calendar(null, {
|
||||
sameDay: '[Today] h:mm A',
|
||||
lastDay: '[Yesterday] h:mm A',
|
||||
lastWeek: 'MMM D',
|
||||
sameElse: 'MMM D, YYYY'
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
|
||||
{#if !allLoaded && onLoadMore}
|
||||
<Loader
|
||||
on:visible={() => {
|
||||
if (!loading) {
|
||||
onLoadMore();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2">
|
||||
<Spinner className="size-4" />
|
||||
<div>{$i18n.t('Loading...')}</div>
|
||||
</div>
|
||||
</Loader>
|
||||
{#if !allLoaded && onLoadMore}
|
||||
<Loader
|
||||
on:visible={() => {
|
||||
if (!loading) {
|
||||
onLoadMore();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2">
|
||||
<Spinner className="size-4" />
|
||||
<div>{$i18n.t('Loading...')}</div>
|
||||
</div>
|
||||
</Loader>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -60,60 +60,89 @@
|
||||
</script>
|
||||
|
||||
<div {id} class={className}>
|
||||
{#if title !== null}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
{#if title !== null}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="{buttonClassName} {disabled ? '' : 'cursor-pointer'}"
|
||||
on:pointerup={() => {
|
||||
if (!disabled) {
|
||||
open = !open;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="{buttonClassName} {disabled ? '' : 'cursor-pointer'}"
|
||||
on:pointerup={() => {
|
||||
if (!disabled) {
|
||||
open = !open;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class=" w-full font-medium flex items-center justify-between gap-2 {attributes?.done &&
|
||||
attributes?.done !== 'true'
|
||||
? 'shimmer'
|
||||
: ''}
|
||||
class=" w-full font-medium flex items-center justify-between gap-2 {attributes?.done &&
|
||||
attributes?.done !== 'true'
|
||||
? 'shimmer'
|
||||
: ''}
|
||||
"
|
||||
>
|
||||
{#if attributes?.done && attributes?.done !== 'true'}
|
||||
<div>
|
||||
<Spinner className="size-4" />
|
||||
</div>
|
||||
{/if}
|
||||
>
|
||||
{#if attributes?.done && attributes?.done !== 'true'}
|
||||
<div>
|
||||
<Spinner className="size-4" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="">
|
||||
{#if attributes?.type === 'reasoning'}
|
||||
{#if attributes?.done === 'true' && attributes?.duration}
|
||||
{#if attributes.duration < 1}
|
||||
{$i18n.t('Thought for less than a second')}
|
||||
{:else if attributes.duration < 60}
|
||||
{$i18n.t('Thought for {{DURATION}} seconds', {
|
||||
DURATION: attributes.duration
|
||||
})}
|
||||
{:else}
|
||||
{$i18n.t('Thought for {{DURATION}}', {
|
||||
DURATION: dayjs.duration(attributes.duration, 'seconds').humanize()
|
||||
})}
|
||||
{/if}
|
||||
<div class="">
|
||||
{#if attributes?.type === 'reasoning'}
|
||||
{#if attributes?.done === 'true' && attributes?.duration}
|
||||
{#if attributes.duration < 1}
|
||||
{$i18n.t('Thought for less than a second')}
|
||||
{:else if attributes.duration < 60}
|
||||
{$i18n.t('Thought for {{DURATION}} seconds', {
|
||||
DURATION: attributes.duration
|
||||
})}
|
||||
{:else}
|
||||
{$i18n.t('Thinking...')}
|
||||
{/if}
|
||||
{:else if attributes?.type === 'code_interpreter'}
|
||||
{#if attributes?.done === 'true'}
|
||||
{$i18n.t('Analyzed')}
|
||||
{:else}
|
||||
{$i18n.t('Analyzing...')}
|
||||
{$i18n.t('Thought for {{DURATION}}', {
|
||||
DURATION: dayjs.duration(attributes.duration, 'seconds').humanize()
|
||||
})}
|
||||
{/if}
|
||||
{:else}
|
||||
{title}
|
||||
{$i18n.t('Thinking...')}
|
||||
{/if}
|
||||
{:else if attributes?.type === 'code_interpreter'}
|
||||
{#if attributes?.done === 'true'}
|
||||
{$i18n.t('Analyzed')}
|
||||
{:else}
|
||||
{$i18n.t('Analyzing...')}
|
||||
{/if}
|
||||
{:else}
|
||||
{title}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !disabled}
|
||||
<div class="flex self-center translate-y-[1px]">
|
||||
{#if open}
|
||||
<ChevronUp strokeWidth="3.5" className="size-3.5" />
|
||||
{:else}
|
||||
<ChevronDown strokeWidth="3.5" className="size-3.5" />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="{buttonClassName} cursor-pointer"
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
on:pointerup={(e) => {
|
||||
if (!disabled) {
|
||||
open = !open;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div class="flex items-start justify-between">
|
||||
<slot />
|
||||
|
||||
{#if !disabled}
|
||||
<div class="flex self-center translate-y-[1px]">
|
||||
{#if chevron}
|
||||
<div class="flex self-start translate-y-1">
|
||||
{#if open}
|
||||
<ChevronUp strokeWidth="3.5" className="size-3.5" />
|
||||
{:else}
|
||||
@@ -122,57 +151,28 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="{buttonClassName} cursor-pointer"
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
on:pointerup={(e) => {
|
||||
if (!disabled) {
|
||||
open = !open;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div class="flex items-start justify-between">
|
||||
<slot />
|
||||
|
||||
{#if chevron}
|
||||
<div class="flex self-start translate-y-1">
|
||||
{#if open}
|
||||
<ChevronUp strokeWidth="3.5" className="size-3.5" />
|
||||
{:else}
|
||||
<ChevronDown strokeWidth="3.5" className="size-3.5" />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if grow}
|
||||
{#if open && !hide}
|
||||
<div
|
||||
transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}
|
||||
on:pointerup={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<slot name="content" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if grow}
|
||||
{#if open && !hide}
|
||||
<div
|
||||
transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}
|
||||
on:pointerup={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<slot name="content" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !grow}
|
||||
{#if open && !hide}
|
||||
<div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
|
||||
<slot name="content" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !grow}
|
||||
{#if open && !hide}
|
||||
<div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
|
||||
<slot name="content" />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -119,11 +119,13 @@
|
||||
const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1:A1');
|
||||
rowCount = range.e.r - range.s.r + 1;
|
||||
|
||||
excelHtml = DOMPurify.sanitize(XLSX.utils.sheet_to_html(worksheet, {
|
||||
id: 'excel-table',
|
||||
editable: false,
|
||||
header: ''
|
||||
}));
|
||||
excelHtml = DOMPurify.sanitize(
|
||||
XLSX.utils.sheet_to_html(worksheet, {
|
||||
id: 'excel-table',
|
||||
editable: false,
|
||||
header: ''
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
$: if (selectedSheet && excelWorkbook) {
|
||||
|
||||
@@ -165,19 +165,11 @@
|
||||
{#each files ?? [] as file, idx}
|
||||
{#if typeof file === 'string'}
|
||||
{#if file.startsWith('data:image/')}
|
||||
<Image
|
||||
id={`${componentId}-tool-call-result-${idx}`}
|
||||
src={file}
|
||||
alt="Image"
|
||||
/>
|
||||
<Image id={`${componentId}-tool-call-result-${idx}`} src={file} alt="Image" />
|
||||
{/if}
|
||||
{:else if typeof file === 'object'}
|
||||
{#if (file.type === 'image' || (file?.content_type ?? '').startsWith('image/')) && file.url}
|
||||
<Image
|
||||
id={`${componentId}-tool-call-result-${idx}`}
|
||||
src={file.url}
|
||||
alt="Image"
|
||||
/>
|
||||
<Image id={`${componentId}-tool-call-result-${idx}`} src={file.url} alt="Image" />
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
@@ -105,7 +105,9 @@
|
||||
/>
|
||||
{:else if valvesSpec.properties[property]?.input ?? null}
|
||||
{#if valvesSpec.properties[property]?.input?.type === 'password'}
|
||||
<div class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 border border-gray-100/30 dark:border-gray-850/30">
|
||||
<div
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 border border-gray-100/30 dark:border-gray-850/30"
|
||||
>
|
||||
<SensitiveInput
|
||||
id="valve-{property}"
|
||||
placeholder={valvesSpec.properties[property]?.description ?? ''}
|
||||
@@ -124,7 +126,10 @@
|
||||
dispatch('change');
|
||||
}}
|
||||
>
|
||||
<option value="" disabled>{valvesSpec.properties[property]?.description ?? $i18n.t('Select an option')}</option>
|
||||
<option value="" disabled
|
||||
>{valvesSpec.properties[property]?.description ??
|
||||
$i18n.t('Select an option')}</option
|
||||
>
|
||||
{#each valvesSpec.properties[property].input.options as option}
|
||||
{#if typeof option === 'object' && option !== null}
|
||||
<option value={option.value} selected={option.value === valves[property]}>
|
||||
|
||||
@@ -12,5 +12,9 @@
|
||||
class={className}
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.25 19.25L16.75 15.75L13.25 12.25" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.75 15.75H10.75C8.54086 15.75 6.75 13.9591 6.75 11.75V7.5" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.75 15.75H10.75C8.54086 15.75 6.75 13.9591 6.75 11.75V7.5"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
@@ -339,7 +339,9 @@
|
||||
<div class="flex justify-end pl-2.5 text-gray-600 dark:text-gray-300">
|
||||
<Tooltip content={shiftKey ? $i18n.t('Delete File') : $i18n.t('Delete File')}>
|
||||
<button
|
||||
class="self-center w-fit px-1 text-sm rounded-xl {shiftKey ? 'text-red-500' : ''}"
|
||||
class="self-center w-fit px-1 text-sm rounded-xl {shiftKey
|
||||
? 'text-red-500'
|
||||
: ''}"
|
||||
on:click|stopPropagation={() => {
|
||||
if (shiftKey) {
|
||||
deleteHandler(file.id);
|
||||
@@ -365,7 +367,9 @@
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2">
|
||||
<div
|
||||
class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2"
|
||||
>
|
||||
<Spinner className="size-4" />
|
||||
<div>{$i18n.t('Loading...')}</div>
|
||||
</div>
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
return '1m';
|
||||
}
|
||||
|
||||
|
||||
let chat = null;
|
||||
|
||||
let mouseOver = false;
|
||||
|
||||
@@ -60,7 +60,12 @@
|
||||
// Assuming $i18n.languages is an array of language codes
|
||||
$: loadLocale($i18n.languages);
|
||||
|
||||
import { deleteNoteById, getNoteById, updateNoteById, updateNoteAccessGrants } from '$lib/apis/notes';
|
||||
import {
|
||||
deleteNoteById,
|
||||
getNoteById,
|
||||
updateNoteById,
|
||||
updateNoteAccessGrants
|
||||
} from '$lib/apis/notes';
|
||||
|
||||
import RichTextInput from '../common/RichTextInput.svelte';
|
||||
import Spinner from '../common/Spinner.svelte';
|
||||
|
||||
@@ -173,12 +173,7 @@
|
||||
}, 300);
|
||||
}
|
||||
|
||||
$: if (
|
||||
loaded &&
|
||||
sortKey !== undefined &&
|
||||
permission !== undefined &&
|
||||
viewOption !== undefined
|
||||
) {
|
||||
$: if (loaded && sortKey !== undefined && permission !== undefined && viewOption !== undefined) {
|
||||
init();
|
||||
}
|
||||
|
||||
|
||||
@@ -388,7 +388,9 @@
|
||||
exportToJson();
|
||||
}}
|
||||
>
|
||||
<div class="flex items-center line-clamp-1">{$i18n.t('Export chat (.json)')}</div>
|
||||
<div class="flex items-center line-clamp-1">
|
||||
{$i18n.t('Export chat (.json)')}
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl select-none w-full"
|
||||
|
||||
@@ -163,7 +163,9 @@
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="h-full flex items-center justify-center text-gray-400 dark:text-gray-600 text-sm">
|
||||
<div
|
||||
class="h-full flex items-center justify-center text-gray-400 dark:text-gray-600 text-sm"
|
||||
>
|
||||
{$i18n.t('Generated images will appear here')}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -173,38 +175,40 @@
|
||||
|
||||
<!-- Input Area -->
|
||||
<div class="pb-3">
|
||||
<div class="border border-gray-100/30 dark:border-gray-850/30 w-full px-3 py-2.5 rounded-xl">
|
||||
<div
|
||||
class="border border-gray-100/30 dark:border-gray-850/30 w-full px-3 py-2.5 rounded-xl"
|
||||
>
|
||||
<!-- Source Images -->
|
||||
{#if sourceImages.length > 0}
|
||||
<div class="flex flex-wrap gap-2 mb-2">
|
||||
{#each sourceImages as image, index}
|
||||
<div class=" relative group">
|
||||
<div class="relative flex items-center">
|
||||
<img src={image} alt="" class="size-10 rounded-xl object-cover" />
|
||||
</div>
|
||||
<div class=" absolute -top-1 -right-1">
|
||||
<button
|
||||
class=" bg-white text-black border border-white rounded-full group-hover:visible invisible transition"
|
||||
type="button"
|
||||
on:click={() => removeImage(index)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
class="size-4"
|
||||
{#if sourceImages.length > 0}
|
||||
<div class="flex flex-wrap gap-2 mb-2">
|
||||
{#each sourceImages as image, index}
|
||||
<div class=" relative group">
|
||||
<div class="relative flex items-center">
|
||||
<img src={image} alt="" class="size-10 rounded-xl object-cover" />
|
||||
</div>
|
||||
<div class=" absolute -top-1 -right-1">
|
||||
<button
|
||||
class=" bg-white text-black border border-white rounded-full group-hover:visible invisible transition"
|
||||
type="button"
|
||||
on:click={() => removeImage(index)}
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Prompt Textarea -->
|
||||
<div class="py-0.5">
|
||||
|
||||
@@ -28,14 +28,11 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await createNewKnowledge(
|
||||
localStorage.token,
|
||||
name,
|
||||
description,
|
||||
accessGrants
|
||||
).catch((e) => {
|
||||
toast.error(`${e}`);
|
||||
});
|
||||
const res = await createNewKnowledge(localStorage.token, name, description, accessGrants).catch(
|
||||
(e) => {
|
||||
toast.error(`${e}`);
|
||||
}
|
||||
);
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Knowledge created successfully.'));
|
||||
|
||||
@@ -59,15 +59,12 @@
|
||||
|
||||
loading = true;
|
||||
try {
|
||||
const res = await getSkillItems(
|
||||
localStorage.token,
|
||||
query,
|
||||
viewOption,
|
||||
page
|
||||
).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
const res = await getSkillItems(localStorage.token, query, viewOption, page).catch(
|
||||
(error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
if (res) {
|
||||
filteredItems = res.items;
|
||||
@@ -246,12 +243,10 @@
|
||||
<button
|
||||
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-200 transition"
|
||||
on:click={async () => {
|
||||
const _skills = await exportSkills(localStorage.token).catch(
|
||||
(error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
const _skills = await exportSkills(localStorage.token).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
if (_skills) {
|
||||
let blob = new Blob([JSON.stringify(_skills)], {
|
||||
type: 'application/json'
|
||||
@@ -456,12 +451,8 @@
|
||||
</SkillMenu>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
on:click|stopPropagation|preventDefault
|
||||
>
|
||||
<Tooltip
|
||||
content={skill.is_active ? $i18n.t('Enabled') : $i18n.t('Disabled')}
|
||||
>
|
||||
<button on:click|stopPropagation|preventDefault>
|
||||
<Tooltip content={skill.is_active ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
|
||||
<Switch
|
||||
bind:state={skill.is_active}
|
||||
on:change={async () => {
|
||||
|
||||
@@ -128,10 +128,7 @@
|
||||
|
||||
<div class=" flex flex-col justify-between w-full overflow-y-auto h-full">
|
||||
<div class="mx-auto w-full md:px-0 h-full">
|
||||
<form
|
||||
class=" flex flex-col max-h-[100dvh] h-full"
|
||||
on:submit|preventDefault={submitHandler}
|
||||
>
|
||||
<form class=" flex flex-col max-h-[100dvh] h-full" on:submit|preventDefault={submitHandler}>
|
||||
<div class="flex flex-col flex-1 overflow-auto h-0 rounded-lg">
|
||||
<div class="w-full mb-2 flex flex-col gap-0.5">
|
||||
<div class="flex w-full items-center">
|
||||
@@ -177,7 +174,8 @@
|
||||
</div>
|
||||
</button>
|
||||
{:else}
|
||||
<span class="text-xs text-gray-500 bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded-full"
|
||||
<span
|
||||
class="text-xs text-gray-500 bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded-full"
|
||||
>{$i18n.t('Read Only')}</span
|
||||
>
|
||||
{/if}
|
||||
@@ -190,7 +188,11 @@
|
||||
{id}
|
||||
</div>
|
||||
{:else}
|
||||
<Tooltip className="w-full" content={$i18n.t('e.g. code-review-guidelines')} placement="top-start">
|
||||
<Tooltip
|
||||
className="w-full"
|
||||
content={$i18n.t('e.g. code-review-guidelines')}
|
||||
placement="top-start"
|
||||
>
|
||||
<input
|
||||
class="w-full text-sm disabled:text-gray-500 bg-transparent outline-hidden"
|
||||
type="text"
|
||||
|
||||
Reference in New Issue
Block a user