This commit is contained in:
Timothy Jaeryang Baek
2026-07-15 05:17:41 -04:00
parent a3908f1281
commit 3ce3c52936
24 changed files with 210 additions and 177 deletions
+1 -1
View File
@@ -18,7 +18,7 @@
</script>
{#if loaded}
<div class="w-full h-full pb-2 px-3">
<div class="w-full h-full pb-2">
<Dashboard />
</div>
{/if}
@@ -230,13 +230,10 @@
}
</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="text-lg font-normal px-0.5 shrink-0">
<div class="flex items-center justify-between mb-2 gap-2">
<h2 class="text-sm font-medium text-gray-900 dark:text-white shrink-0">
{$i18n.t('Analytics')}
</div>
</h2>
<div class="flex items-center gap-2 flex-wrap justify-end min-w-0">
{#if groups.length > 0}
<select
+15
View File
@@ -20,6 +20,7 @@
import WebSearch from './Settings/WebSearch.svelte';
import Evaluations from './Settings/Evaluations.svelte';
import Analytics from './Analytics.svelte';
import CodeExecution from './Settings/CodeExecution.svelte';
import Integrations from './Settings/Integrations.svelte';
import Subagents from './Settings/Subagents.svelte';
@@ -42,6 +43,7 @@
'models',
'subagents',
'evaluations',
'analytics',
'integrations',
'documents',
'web',
@@ -157,6 +159,12 @@
route: '/admin/settings/evaluations',
keywords: ['evaluations', 'feedback', 'rating', 'arena', 'leaderboard', 'preference']
},
{
id: 'analytics',
title: 'Analytics',
route: '/admin/settings/analytics',
keywords: ['analytics', 'usage', 'stats', 'dashboard', 'models', 'users', 'messages']
},
{
id: 'integrations',
title: 'Integrations',
@@ -289,6 +297,10 @@
const setFilteredSettings = () => {
filteredSettings = allSettings.filter((tab) => {
const searchTerm = search.toLowerCase().trim();
if (tab.id === 'analytics' && !($config?.features.enable_admin_analytics ?? true)) {
return false;
}
return (
search === '' ||
tab.title.toLowerCase().includes(searchTerm) ||
@@ -367,6 +379,7 @@
<!-- {$i18n.t('Models')} -->
<!-- {$i18n.t('Sub-agents')} -->
<!-- {$i18n.t('Evaluations')} -->
<!-- {$i18n.t('Analytics')} -->
<!-- {$i18n.t('Integrations')} -->
<!-- {$i18n.t('Documents')} -->
<!-- {$i18n.t('Web Search')} -->
@@ -416,6 +429,8 @@
<Subagents />
{:else if selectedTab === 'evaluations'}
<Evaluations />
{:else if selectedTab === 'analytics'}
<Analytics />
{:else if selectedTab === 'integrations'}
<Integrations />
{:else if selectedTab === 'documents'}
@@ -1,5 +1,6 @@
<script lang="ts">
import CodeBracket from '$lib/components/icons/CodeBracket.svelte';
import BarsArrowUp from '$lib/components/icons/BarsArrowUp.svelte';
import Computer from '$lib/components/icons/Computer.svelte';
import Database from '$lib/components/icons/Database.svelte';
import Document from '$lib/components/icons/Document.svelte';
@@ -66,6 +67,8 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M12 20V4" />
<path stroke-linecap="round" stroke-linejoin="round" d="M19 20v-7" />
</svg>
{:else if id === 'analytics'}
<BarsArrowUp {className} {strokeWidth} />
{:else if id === 'integrations'}
<WrenchAlt {className} {strokeWidth} />
{:else if id === 'documents'}
+22 -10
View File
@@ -30,7 +30,6 @@
import ModelEditor from '$lib/components/workspace/Models/ModelEditor.svelte';
import { toast } from 'svelte-sonner';
import Badge from '$lib/components/common/Badge.svelte';
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import ModelSettingsModal from './Models/ModelSettingsModal.svelte';
import ManageModelsModal from './Models/ManageModelsModal.svelte';
@@ -85,14 +84,24 @@
const isSharedModel = (model) => (model?.access_grants ?? []).length > 0 && !isPublicModel(model);
const modelAccessBadge = (model) => {
const modelAccessLabel = (model) => {
if (isPublicModel(model)) {
return { type: 'success', content: $i18n.t('Public') };
return $i18n.t('Public');
}
if (isSharedModel(model)) {
return { type: 'info', content: $i18n.t('Shared') };
return $i18n.t('Shared');
}
return { type: 'muted', content: $i18n.t('Private') };
return $i18n.t('Private');
};
const modelAccessClass = (model) => {
if (isPublicModel(model)) {
return 'text-green-700/50 dark:text-green-300/50';
}
if (isSharedModel(model)) {
return 'text-blue-700/50 dark:text-blue-300/50';
}
return 'text-gray-500 dark:text-gray-400';
};
$: if (models) {
@@ -670,7 +679,7 @@
<img
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model.id}&lang=${$i18n.language}`}
alt="modelfile profile"
class=" rounded-xl size-8 object-cover"
class=" rounded-xl size-7 object-cover"
loading="lazy"
decoding="async"
on:error={(e) => {
@@ -711,10 +720,13 @@
: model.id}
</span>
<Badge
type={modelAccessBadge(model).type}
content={modelAccessBadge(model).content}
/>
<span
class="shrink-0 text-[11px] font-normal leading-4 {modelAccessClass(
model
)}"
>
{modelAccessLabel(model)}
</span>
</div>
</Tooltip>
</div>
@@ -16,7 +16,6 @@
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Checkbox from '$lib/components/common/Checkbox.svelte';
import Badge from '$lib/components/common/Badge.svelte';
import Search from '$lib/components/icons/Search.svelte';
import Pagination from '$lib/components/common/Pagination.svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
@@ -46,6 +45,16 @@
page = 1;
};
const roleClass = (role) => {
if (role === 'admin') {
return 'text-blue-700/50 dark:text-blue-300/50';
}
if (role === 'user') {
return 'text-green-700/50 dark:text-green-300/50';
}
return 'text-gray-500 dark:text-gray-400';
};
const getUserList = async () => {
try {
const res = await getUsers(localStorage.token, query, orderBy, direction, page).catch(
@@ -154,12 +163,12 @@
<th
scope="col"
class="px-2.5 py-2 cursor-pointer select-none"
on:click={() => setSortKey('role')}
on:click={() => setSortKey('name')}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('Role')}
{$i18n.t('Name')}
{#if orderBy === 'role'}
{#if orderBy === 'name'}
<span class="font-normal"
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
@@ -177,12 +186,12 @@
<th
scope="col"
class="px-2.5 py-2 cursor-pointer select-none"
on:click={() => setSortKey('name')}
on:click={() => setSortKey('role')}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('Name')}
{$i18n.t('Role')}
{#if orderBy === 'name'}
{#if orderBy === 'role'}
<span class="font-normal"
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
@@ -236,23 +245,11 @@
/>
</div>
</td>
<td class="px-3 py-1 min-w-[7rem] w-28">
<div class=" translate-y-0.5">
<Badge
type={user.role === 'admin'
? 'info'
: user.role === 'user'
? 'success'
: 'muted'}
content={$i18n.t(user.role)}
/>
</div>
</td>
<td class="px-3 py-1 font-normal text-gray-900 dark:text-white max-w-48">
<Tooltip content={user.email} placement="top-start">
<div class="flex items-center">
<div class="flex items-center gap-2">
<img
class="rounded-full w-6 h-6 object-cover mr-2.5 flex-shrink-0"
class="rounded-full w-6 h-6 object-cover flex-shrink-0"
src={`${WEBUI_API_BASE_URL}/users/${user.id}/profile/image`}
alt="user"
/>
@@ -261,6 +258,11 @@
</div>
</Tooltip>
</td>
<td class="px-3 py-1 min-w-[5rem] w-20">
<span class="text-xs font-normal leading-4 capitalize {roleClass(user.role)}">
{$i18n.t(user.role)}
</span>
</td>
<td class=" px-3 py-1">
{dayjs(user.last_active_at * 1000).fromNow()}
+28 -22
View File
@@ -23,7 +23,6 @@
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import Badge from '$lib/components/common/Badge.svelte';
import Plus from '$lib/components/icons/Plus.svelte';
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
@@ -79,6 +78,16 @@
}
};
const roleClass = (role) => {
if (role === 'admin') {
return 'text-blue-700/50 dark:text-blue-300/50';
}
if (role === 'user') {
return 'text-green-700/50 dark:text-green-300/50';
}
return 'text-gray-500 dark:text-gray-400';
};
const getUserList = async () => {
try {
const res = await getUsers(localStorage.token, query, orderBy, direction, page).catch(
@@ -239,12 +248,12 @@
<th
scope="col"
class="px-2.5 py-2 cursor-pointer select-none"
on:click={() => setSortKey('role')}
on:click={() => setSortKey('name')}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('Role')}
{$i18n.t('Name')}
{#if orderBy === 'role'}
{#if orderBy === 'name'}
<span class="font-normal"
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
@@ -262,12 +271,12 @@
<th
scope="col"
class="px-2.5 py-2 cursor-pointer select-none"
on:click={() => setSortKey('name')}
on:click={() => setSortKey('role')}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('Name')}
{$i18n.t('Role')}
{#if orderBy === 'name'}
{#if orderBy === 'role'}
<span class="font-normal"
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
@@ -359,21 +368,6 @@
<tbody class="">
{#each users as user (user.id)}
<tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs">
<td class="px-3 py-1 min-w-[7rem] w-28">
<button
class=" translate-y-0.5"
aria-label={$i18n.t('Change User Role')}
on:click={() => {
selectedUser = user;
showEditUserModal = !showEditUserModal;
}}
>
<Badge
type={user.role === 'admin' ? 'info' : user.role === 'user' ? 'success' : 'muted'}
content={$i18n.t(user.role)}
/>
</button>
</td>
<td class="px-3 py-1 font-normal text-gray-900 dark:text-white max-w-48">
<div class="flex items-center gap-2">
<ProfilePreview {user} side="right" align="center" sideOffset={6}>
@@ -401,6 +395,18 @@
{/if}
</div>
</td>
<td class="px-3 py-1 min-w-[5rem] w-20">
<button
class="text-xs font-normal leading-4 capitalize transition {roleClass(user.role)}"
aria-label={$i18n.t('Change User Role')}
on:click={() => {
selectedUser = user;
showEditUserModal = !showEditUserModal;
}}
>
{$i18n.t(user.role)}
</button>
</td>
<td class=" px-3 py-1 max-w-48 truncate"> {user.email} </td>
<td class=" px-3 py-1">
@@ -21,6 +21,7 @@
} from '$lib/apis/chats';
import { chatId, showSettings } from '$lib/stores';
import { refreshChatList } from '$lib/stores/chat-list';
import { formatNumber } from '$lib/utils';
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import Dropdown from '$lib/components/common/Dropdown.svelte';
import DropdownMenu from '$lib/components/common/DropdownMenu.svelte';
@@ -185,7 +186,7 @@
{$i18n.t('Archived Chats')}
{#if chatCount !== null}
<span class="ml-2 font-normal text-gray-500 dark:text-gray-500">
{chatCount}
{formatNumber(chatCount)}
</span>
{/if}
</h2>
+13 -1
View File
@@ -38,6 +38,7 @@
import AdminModels from '$lib/components/admin/Settings/Models.svelte';
import AdminSubagents from '$lib/components/admin/Settings/Subagents.svelte';
import AdminEvaluations from '$lib/components/admin/Settings/Evaluations.svelte';
import AdminAnalytics from '$lib/components/admin/Analytics.svelte';
import AdminIntegrations from '$lib/components/admin/Settings/Integrations.svelte';
import AdminDocuments from '$lib/components/admin/Settings/Documents.svelte';
import AdminWebSearch from '$lib/components/admin/Settings/WebSearch.svelte';
@@ -602,6 +603,11 @@
title: 'Evaluations',
keywords: ['evaluations', 'feedback', 'rating', 'arena', 'leaderboard', 'preference']
},
{
id: 'admin:analytics',
title: 'Analytics',
keywords: ['analytics', 'usage', 'stats', 'dashboard', 'models', 'users', 'messages']
},
{
id: 'admin:integrations',
title: 'Integrations',
@@ -692,6 +698,10 @@
filteredSettings = availableSettings
.filter((tab) => {
const query = search.toLowerCase().trim();
if (tab.id === 'admin:analytics' && !($config?.features.enable_admin_analytics ?? true)) {
return false;
}
return (
query === '' ||
tab.title.toLowerCase().includes(query) ||
@@ -784,7 +794,7 @@
</button>
<div
class="hidden md:flex items-center gap-1.5 h-7 px-2 mx-1 mt-1 mb-2 shrink-0 rounded-lg text-xs bg-gray-50/70 dark:bg-white/[0.03]"
class="hidden md:flex items-center gap-1.5 h-7 px-2 mx-1 mt-1 mb-0.5 shrink-0 rounded-lg text-xs bg-gray-50/70 dark:bg-white/[0.03]"
>
<div class="self-center rounded-l-xl bg-transparent">
<Search
@@ -1075,6 +1085,8 @@
<AdminSubagents />
{:else if selectedTab === 'admin:evaluations'}
<AdminEvaluations />
{:else if selectedTab === 'admin:analytics'}
<AdminAnalytics />
{:else if selectedTab === 'admin:integrations'}
<AdminIntegrations {saveSettings} />
{:else if selectedTab === 'admin:documents'}
+5 -5
View File
@@ -30,19 +30,19 @@
{id}
aria-labelledby={ariaLabelledbyId || undefined}
aria-label={ariaLabel || undefined}
class="flex h-[1.125rem] min-h-[1.125rem] w-8 shrink-0 cursor-pointer items-center rounded-full px-1 mx-[1px] transition {($settings?.highContrastMode ??
class="relative h-4 min-h-4 w-7 shrink-0 cursor-pointer rounded-full mx-[1px] transition-colors duration-150 {($settings?.highContrastMode ??
false)
? 'focus:outline focus:outline-2 focus:outline-gray-800 focus:dark:outline-gray-200'
: 'outline outline-1 outline-gray-100 dark:outline-gray-800'} {state
? ' bg-emerald-500 dark:bg-emerald-700'
: 'bg-gray-200 dark:bg-transparent'}"
: 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-400 dark:focus-visible:outline-gray-500'} {state
? 'bg-gray-900 dark:bg-white'
: 'bg-gray-300 dark:bg-gray-700'}"
onCheckedChange={async () => {
await tick();
dispatch('change', state);
}}
>
<Switch.Thumb
class="pointer-events-none block size-3 shrink-0 rounded-full bg-white transition-transform data-[state=checked]:translate-x-3 data-[state=unchecked]:translate-x-0 data-[state=unchecked]:shadow-mini "
class="pointer-events-none absolute top-[0.125rem] block h-3 w-3 shrink-0 rounded-full transition-all duration-150 data-[state=checked]:left-[0.875rem] data-[state=checked]:bg-white data-[state=checked]:dark:bg-black data-[state=unchecked]:left-[0.125rem] data-[state=unchecked]:bg-white data-[state=unchecked]:dark:bg-gray-500"
/>
</Switch.Root>
</Tooltip>
+3 -2
View File
@@ -11,6 +11,7 @@
import { deleteChatById } from '$lib/apis/chats';
import { WEBUI_API_BASE_URL } from '$lib/constants';
import { formatNumber } from '$lib/utils';
import Modal from '$lib/components/common/Modal.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
@@ -97,11 +98,11 @@
<div>{title}</div>
{#if count !== null}
<div class="text-sm font-medium text-gray-500 dark:text-gray-500">
{count}
{formatNumber(count)}
</div>
{:else if chatList}
<div class="text-sm font-medium text-gray-500 dark:text-gray-500">
{chatList.length}
{formatNumber(chatList.length)}
</div>
{/if}
</div>
+2 -2
View File
@@ -40,7 +40,7 @@
toggleNotePinnedStatusById,
getPinnedNoteList
} from '$lib/apis/notes';
import { capitalizeFirstLetter, copyToClipboard, getTimeRange } from '$lib/utils';
import { capitalizeFirstLetter, copyToClipboard, formatNumber, getTimeRange } from '$lib/utils';
import { downloadPdf, createNoteHandler } from './utils';
import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
@@ -388,7 +388,7 @@
<div class="flex items-center gap-1 py-1 min-w-0">
<span class="min-w-fit px-1 text-sm select-none">{$i18n.t('Notes')}</span>
<span class="text-sm text-gray-500 dark:text-gray-500">
{total ?? ''}
{total === null ? '' : formatNumber(total)}
</span>
</div>
+9 -10
View File
@@ -297,11 +297,10 @@
{#if items !== null && total !== null}
{#if (items ?? []).length !== 0}
<!-- The Aleph dreams itself into being, and the void learns its own name -->
<div class="my-1 grid grid-cols-1 lg:grid-cols-2 gap-x-2 gap-y-0.5">
<div class="my-1 grid grid-cols-1 gap-x-2 gap-y-0.5 lg:grid-cols-2">
{#each items as item}
<button
class="flex space-x-4 cursor-pointer text-left w-full px-2.5 py-1.5 transition rounded-2xl hover:bg-gray-50/70 dark:hover:bg-gray-850/50"
class="flex w-full cursor-pointer rounded-xl px-2 py-1 text-left transition hover:bg-gray-50/60 dark:hover:bg-gray-850/40"
on:click={() => {
if (item?.meta?.document) {
toast.error(
@@ -314,9 +313,9 @@
}
}}
>
<div class=" w-full">
<div class=" self-center flex-1 justify-between">
<div class="flex items-center justify-between -my-1 h-8">
<div class="w-full min-w-0">
<div class="flex-1 self-center justify-between">
<div class="flex h-7 items-center justify-between">
<div class=" flex gap-2 items-center justify-between w-full">
{#if item?.meta?.source === 'external'}
<div>
@@ -360,14 +359,14 @@
{/if}
</div>
<div class=" flex items-center gap-1 justify-between px-1.5">
<div class="flex items-center justify-between gap-2 px-0.5">
<Tooltip content={item?.description ?? item.name}>
<div class=" flex items-center gap-2">
<div class=" text-sm font-normal line-clamp-1 capitalize">{item.name}</div>
<div class="flex min-w-0 items-center gap-2">
<div class="line-clamp-1 text-sm font-normal capitalize">{item.name}</div>
</div>
</Tooltip>
<div class="flex items-center gap-2 shrink-0">
<div class="flex shrink-0 items-center gap-2">
<Tooltip content={dayjs(item.updated_at * 1000).format('LLLL')}>
<div class=" text-xs text-gray-500 line-clamp-1 hidden sm:block">
{$i18n.t('Updated')}
@@ -29,7 +29,7 @@
<Tooltip content={$i18n.t('More')}>
<slot
><button
class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
class="self-center w-fit rounded-lg p-1 text-sm hover:bg-black/5 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
type="button"
aria-label={$i18n.t('More Options')}
on:click={(e) => {
@@ -37,7 +37,7 @@
show = true;
}}
>
<EllipsisHorizontal className="size-5" />
<EllipsisHorizontal className="size-4" />
</button>
</slot>
</Tooltip>
+17 -17
View File
@@ -601,13 +601,13 @@
{#if models !== null}
{#if (models ?? []).length !== 0}
<div class="my-1 gap-x-2 gap-y-0.5 grid lg:grid-cols-2" id="model-list">
<div class="my-1 grid gap-x-2 gap-y-0.5 lg:grid-cols-2" id="model-list">
{#each models as model (model.id)}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
class="flex transition w-full px-2.5 py-1.5 rounded-2xl {model.write_access
? 'cursor-pointer hover:bg-gray-50/70 dark:hover:bg-gray-850/50'
class="flex w-full rounded-xl px-2 py-1 transition {model.write_access
? 'cursor-pointer hover:bg-gray-50/60 dark:hover:bg-gray-850/40'
: ''}"
id="model-item-{model.id}"
on:click={() => {
@@ -616,18 +616,18 @@
}
}}
>
<div class="flex group/item min-w-0 gap-3.5 w-full">
<div class="self-center pl-0.5">
<div class="flex bg-white rounded-2xl">
<div class="flex group/item min-w-0 gap-2.5 w-full">
<div class="self-center">
<div class="flex">
<div
class="{model.is_active
? ''
: 'opacity-50 dark:opacity-50'} bg-transparent rounded-2xl"
: 'opacity-50 dark:opacity-50'} bg-transparent rounded-xl"
>
<img
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model.id}&lang=${$i18n.language}`}
alt="modelfile profile"
class=" rounded-2xl size-12 object-cover"
class="size-8 rounded-xl object-cover"
loading="lazy"
decoding="async"
on:error={(e) => {
@@ -638,7 +638,7 @@
</div>
</div>
<div class=" shrink-0 flex w-full min-w-0 flex-1 pr-1 self-center">
<div class="flex min-w-0 w-full flex-1 self-center pr-1">
<div class="flex h-full w-full flex-1 flex-col justify-start self-center group">
<div class="min-w-0 flex-1 w-full">
<div class="flex min-w-0 items-center justify-between w-full gap-2">
@@ -648,7 +648,7 @@
placement="top-start"
>
<a
class="font-normal line-clamp-1 hover:underline capitalize"
class="line-clamp-1 text-sm font-normal capitalize hover:underline"
href={`/?models=${encodeURIComponent(model.id)}`}
>
{model.name}
@@ -669,7 +669,7 @@
content={model?.meta?.hidden ? $i18n.t('Show') : $i18n.t('Hide')}
>
<button
class="self-center w-fit text-sm p-1.5 dark:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
class="self-center w-fit rounded-lg p-1 text-sm hover:bg-black/5 dark:text-white dark:hover:bg-white/5"
type="button"
aria-label={model?.meta?.hidden
? $i18n.t('Show')
@@ -689,7 +689,7 @@
<Tooltip content={$i18n.t('Delete')}>
<button
class="self-center w-fit text-sm p-1.5 dark:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
class="self-center w-fit rounded-lg p-1 text-sm hover:bg-black/5 dark:text-white dark:hover:bg-white/5"
type="button"
aria-label={$i18n.t('Delete')}
on:click={(e) => {
@@ -735,9 +735,9 @@
onClose={() => {}}
>
<div
class="self-center w-fit p-1 text-sm dark:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
class="self-center w-fit rounded-lg p-1 text-sm hover:bg-black/5 dark:text-white dark:hover:bg-white/5"
>
<EllipsisHorizontal className="size-5" />
<EllipsisHorizontal className="size-4" />
</div>
</ModelMenu>
{/if}
@@ -772,13 +772,13 @@
</div>
</div>
<div class="flex min-w-0 gap-1 pr-2 -mt-1 items-center">
<div class="flex min-w-0 items-center gap-1 pr-2 text-xs text-gray-500">
<Tooltip
content={model?.user?.email ?? $i18n.t('Deleted User')}
className="flex shrink-0"
placement="top-start"
>
<div class="shrink-0 text-gray-500 text-xs">
<div class="shrink-0">
{$i18n.t('By {{name}}', {
name: capitalizeFirstLetter(
model?.user?.name ?? model?.user?.email ?? $i18n.t('Deleted User')
@@ -794,7 +794,7 @@
className="min-w-0 text-left"
placement="top-start"
>
<div class="flex min-w-0 gap-1 text-xs overflow-hidden">
<div class="flex min-w-0 gap-1 overflow-hidden">
<div class="line-clamp-1 min-w-0">
{#if (model?.meta?.description ?? '').trim()}
{model?.meta?.description}
+12 -13
View File
@@ -469,18 +469,17 @@
<Spinner className="size-5" />
</div>
{:else if (prompts ?? []).length !== 0}
<!-- Before they call, I will answer; while they are yet speaking, I will hear. -->
<div class="gap-x-2 gap-y-0.5 grid my-1 lg:grid-cols-2">
<div class="my-1 grid gap-x-2 gap-y-0.5 lg:grid-cols-2">
{#each prompts as prompt (prompt.id)}
<a
class="flex space-x-4 cursor-pointer text-left w-full px-2.5 py-1.5 transition rounded-2xl hover:bg-gray-50/70 dark:hover:bg-gray-850/50"
class="flex w-full cursor-pointer rounded-xl px-2 py-1 text-left transition hover:bg-gray-50/60 dark:hover:bg-gray-850/40"
href={`/workspace/prompts/${prompt.id}`}
>
<div class=" flex flex-col flex-1 space-x-4 cursor-pointer w-full pl-1">
<div class="flex w-full min-w-0 flex-1 cursor-pointer flex-col pl-0.5">
<div class="flex items-center justify-between w-full mb-0.5">
<div class="flex items-center gap-2">
<div class="font-normal line-clamp-1 capitalize">{prompt.name}</div>
<div class="text-xs overflow-hidden text-ellipsis line-clamp-1 text-gray-500">
<div class="flex min-w-0 items-center gap-2">
<div class="line-clamp-1 text-sm font-normal capitalize">{prompt.name}</div>
<div class="line-clamp-1 overflow-hidden text-ellipsis text-xs text-gray-500">
/{prompt.command}
</div>
</div>
@@ -489,13 +488,13 @@
{/if}
</div>
<div class="flex gap-1 text-xs">
<div class="flex min-w-0 gap-1 text-xs text-gray-500">
<Tooltip
content={prompt?.user?.email ?? $i18n.t('Deleted User')}
className="flex shrink-0"
placement="top-start"
>
<div class="shrink-0 text-gray-500">
<div class="shrink-0">
{$i18n.t('By {{name}}', {
name: capitalizeFirstLetter(
prompt?.user?.name ?? prompt?.user?.email ?? $i18n.t('Deleted User')
@@ -519,7 +518,7 @@
{#if shiftKey}
<Tooltip content={$i18n.t('Delete')}>
<button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
class="self-center w-fit rounded-lg p-1 text-sm hover:bg-black/5 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
type="button"
aria-label={$i18n.t('Delete')}
on:click={() => {
@@ -532,7 +531,7 @@
{:else}
<Tooltip content={$i18n.t('Copy Prompt')}>
<button
class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
class="self-center w-fit rounded-lg p-1 text-sm hover:bg-black/5 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
type="button"
aria-label={$i18n.t('Copy Prompt')}
on:click={(e) => {
@@ -568,10 +567,10 @@
onClose={() => {}}
>
<button
class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
class="self-center w-fit rounded-lg p-1 text-sm hover:bg-black/5 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
type="button"
>
<EllipsisHorizontal className="size-5" />
<EllipsisHorizontal className="size-4" />
</button>
</PromptMenu>
+16 -16
View File
@@ -343,23 +343,23 @@
<Spinner className="size-5" />
</div>
{:else if (filteredItems ?? []).length !== 0}
<div class="my-1 gap-x-2 gap-y-0.5 grid lg:grid-cols-2">
<div class="my-1 grid gap-x-2 gap-y-0.5 lg:grid-cols-2">
{#each filteredItems as skill}
<Tooltip content={skill?.description ?? skill?.id}>
<div
class="flex space-x-4 text-left w-full px-2.5 py-1.5 transition rounded-2xl {skill.write_access
? 'cursor-pointer hover:bg-gray-50/70 dark:hover:bg-gray-850/50'
class="flex w-full rounded-xl px-2 py-1 text-left transition {skill.write_access
? 'cursor-pointer hover:bg-gray-50/60 dark:hover:bg-gray-850/40'
: 'cursor-not-allowed opacity-60'}"
>
{#if skill.write_access}
<a
class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
class="flex w-full min-w-0 flex-1 cursor-pointer"
href={`/workspace/skills/edit?id=${encodeURIComponent(skill.id)}`}
>
<div class="flex items-center text-left">
<div class=" flex-1 self-center">
<div class="flex min-w-0 items-center text-left">
<div class="min-w-0 flex-1 self-center">
<Tooltip content={skill.id} placement="top-start">
<div class="flex items-center gap-2">
<div class="flex min-w-0 items-center gap-2">
<div class="line-clamp-1 text-sm">
{skill.name}
</div>
@@ -369,7 +369,7 @@
</div>
</Tooltip>
<div class="px-0.5">
<div class="text-xs text-gray-500 shrink-0">
<div class="shrink-0 text-xs text-gray-500">
<Tooltip
content={skill?.user?.email ?? $i18n.t('Deleted User')}
className="flex shrink-0"
@@ -387,12 +387,12 @@
</div>
</a>
{:else}
<div class=" flex flex-1 space-x-3.5 w-full">
<div class="flex items-center text-left w-full">
<div class="flex-1 self-center w-full">
<div class="flex w-full min-w-0 flex-1">
<div class="flex w-full min-w-0 items-center text-left">
<div class="w-full min-w-0 flex-1 self-center">
<div class="flex items-center justify-between w-full gap-2">
<Tooltip content={skill.id} placement="top-start">
<div class="flex items-center gap-2">
<div class="flex min-w-0 items-center gap-2">
<div class="line-clamp-1 text-sm">
{skill.name}
</div>
@@ -404,7 +404,7 @@
<Badge type="muted" content={$i18n.t('Read Only')} />
</div>
<div class="px-0.5">
<div class="text-xs text-gray-500 shrink-0">
<div class="shrink-0 text-xs text-gray-500">
<Tooltip
content={skill?.user?.email ?? $i18n.t('Deleted User')}
className="flex shrink-0"
@@ -427,7 +427,7 @@
{#if shiftKey}
<Tooltip content={$i18n.t('Delete')}>
<button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
class="self-center w-fit rounded-lg p-1 text-sm hover:bg-black/5 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
type="button"
aria-label={$i18n.t('Delete')}
on:click={() => {
@@ -455,10 +455,10 @@
onClose={() => {}}
>
<button
class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
class="self-center w-fit rounded-lg p-1 text-sm hover:bg-black/5 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
type="button"
>
<EllipsisHorizontal className="size-5" />
<EllipsisHorizontal className="size-4" />
</button>
</SkillMenu>
{/if}
+18 -19
View File
@@ -274,7 +274,6 @@
/>
<div class="space-y-1">
<!-- The iron remembers its forge. -->
<div class="flex h-8 w-full items-center gap-2">
<div class="flex min-w-0 flex-1">
<div class=" self-center ml-1 mr-3">
@@ -330,23 +329,23 @@
</div>
{#if (filteredItems ?? []).length !== 0}
<div class="my-1 gap-x-2 gap-y-0.5 grid lg:grid-cols-2">
<div class="my-1 grid gap-x-2 gap-y-0.5 lg:grid-cols-2">
{#each filteredItems as tool}
<Tooltip content={tool?.meta?.description ?? tool?.id}>
<div
class="flex space-x-4 text-left w-full px-2.5 py-1.5 transition rounded-2xl {tool.write_access
? 'cursor-pointer hover:bg-gray-50/70 dark:hover:bg-gray-850/50'
class="flex w-full rounded-xl px-2 py-1 text-left transition {tool.write_access
? 'cursor-pointer hover:bg-gray-50/60 dark:hover:bg-gray-850/40'
: 'cursor-not-allowed opacity-60'}"
>
{#if tool.write_access}
<a
class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
class="flex w-full min-w-0 flex-1 cursor-pointer"
href={`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`}
>
<div class="flex items-center text-left">
<div class=" flex-1 self-center">
<div class="flex min-w-0 items-center text-left">
<div class="min-w-0 flex-1 self-center">
<Tooltip content={tool.id} placement="top-start">
<div class="flex items-center gap-2">
<div class="flex min-w-0 items-center gap-2">
<div class="line-clamp-1 text-sm">
{tool.name}
</div>
@@ -358,7 +357,7 @@
</div>
</Tooltip>
<div class="px-0.5">
<div class="text-xs text-gray-500 shrink-0">
<div class="shrink-0 text-xs text-gray-500">
<Tooltip
content={tool?.user?.email ?? $i18n.t('Deleted User')}
className="flex shrink-0"
@@ -376,12 +375,12 @@
</div>
</a>
{:else}
<div class=" flex flex-1 space-x-3.5 w-full">
<div class="flex items-center text-left w-full">
<div class="flex-1 self-center w-full">
<div class="flex w-full min-w-0 flex-1">
<div class="flex w-full min-w-0 items-center text-left">
<div class="w-full min-w-0 flex-1 self-center">
<div class="flex items-center justify-between w-full gap-2">
<Tooltip content={tool.id} placement="top-start">
<div class="flex items-center gap-2">
<div class="flex min-w-0 items-center gap-2">
<div class="line-clamp-1 text-sm">
{tool.name}
</div>
@@ -395,7 +394,7 @@
<Badge type="muted" content={$i18n.t('Read Only')} />
</div>
<div class="px-0.5">
<div class="text-xs text-gray-500 shrink-0">
<div class="shrink-0 text-xs text-gray-500">
<Tooltip
content={tool?.user?.email ?? $i18n.t('Deleted User')}
className="flex shrink-0"
@@ -418,7 +417,7 @@
{#if shiftKey}
<Tooltip content={$i18n.t('Delete')}>
<button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
class="self-center w-fit rounded-lg p-1 text-sm hover:bg-black/5 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
type="button"
aria-label={$i18n.t('Delete')}
on:click={() => {
@@ -432,7 +431,7 @@
{#if tool?.meta?.manifest?.funding_url ?? false}
<Tooltip content="Support">
<button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
class="self-center w-fit rounded-lg p-1 text-sm hover:bg-black/5 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
type="button"
aria-label={$i18n.t('Support')}
on:click={() => {
@@ -447,7 +446,7 @@
<Tooltip content={$i18n.t('Valves')}>
<button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
class="self-center w-fit rounded-lg p-1 text-sm hover:bg-black/5 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
type="button"
aria-label={$i18n.t('Valves')}
on:click={() => {
@@ -497,10 +496,10 @@
onClose={() => {}}
>
<button
class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
class="self-center w-fit rounded-lg p-1 text-sm hover:bg-black/5 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
type="button"
>
<EllipsisHorizontal className="size-5" />
<EllipsisHorizontal className="size-4" />
</button>
</ToolMenu>
{/if}
+3 -3
View File
@@ -34,9 +34,9 @@ import { decode } from 'html-entities';
export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export const formatNumber = (num: number): string => {
return new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1 }).format(
num
);
return new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1 })
.format(num)
.toLowerCase();
};
function escapeRegExp(string: string): string {
-10
View File
@@ -73,16 +73,6 @@
href="/admin">{$i18n.t('Users')}</a
>
{#if $config?.features.enable_admin_analytics ?? true}
<a
draggable="false"
class="min-w-fit p-1.5 {$page.url.pathname.includes('/admin/analytics')
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition select-none"
href="/admin/analytics">{$i18n.t('Analytics')}</a
>
{/if}
<a
draggable="false"
class="min-w-fit p-1.5 {$page.url.pathname.includes('/admin/evaluations')
@@ -2,15 +2,12 @@
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { config } from '$lib/stores';
import Analytics from '$lib/components/admin/Analytics.svelte';
onMount(() => {
if (!($config?.features.enable_admin_analytics ?? true)) {
goto('/admin');
} else {
goto('/?settings=admin%3Aanalytics', { replaceState: true });
}
});
</script>
{#if $config?.features.enable_admin_analytics ?? true}
<Analytics />
{/if}
@@ -2,15 +2,12 @@
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { config } from '$lib/stores';
import Analytics from '$lib/components/admin/Analytics.svelte';
onMount(() => {
if (!($config?.features.enable_admin_analytics ?? true)) {
goto('/admin');
} else {
goto('/?settings=admin%3Aanalytics', { replaceState: true });
}
});
</script>
{#if $config?.features.enable_admin_analytics ?? true}
<Analytics />
{/if}
+2 -1
View File
@@ -35,6 +35,7 @@
import Check from '$lib/components/icons/Check.svelte';
import CheckCircle from '$lib/components/icons/CheckCircle.svelte';
import Minus from '$lib/components/icons/Minus.svelte';
import { formatNumber } from '$lib/utils';
const { saveAs } = fileSaver;
@@ -378,7 +379,7 @@
<div class="flex items-center gap-1 py-1 min-w-0">
<span class="min-w-fit px-1 text-sm select-none">{$i18n.t('Automations')}</span>
<span class="text-sm text-gray-500 dark:text-gray-500">
{total ?? ''}
{total === null ? '' : formatNumber(total)}
</span>
</div>
+7 -5
View File
@@ -21,6 +21,7 @@
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Sidebar from '$lib/components/icons/Sidebar.svelte';
import SplitCreateButton from '$lib/components/common/SplitCreateButton.svelte';
import { formatNumber } from '$lib/utils';
const i18n = getContext<Writable<i18nType>>('i18n');
@@ -42,6 +43,7 @@
$: visibleActions = $workspaceActions.filter((action) => action.visible ?? true);
const getCount = (res: any) => res?.total ?? (Array.isArray(res) ? res.length : null);
const formatCount = (count: number | null) => formatNumber(count ?? 0);
const loadWorkspaceCounts = async () => {
const canViewModels = $user?.role === 'admin' || $user?.permissions?.workspace?.models;
@@ -155,7 +157,7 @@
>
<span>{$i18n.t('Models')}</span>
<span class="text-sm text-gray-500 dark:text-gray-500">
{$workspaceCounts.models ?? 0}
{formatCount($workspaceCounts.models)}
</span>
</a>
{/if}
@@ -172,7 +174,7 @@
>
<span>{$i18n.t('Knowledge')}</span>
<span class="text-sm text-gray-500 dark:text-gray-500">
{$workspaceCounts.knowledge ?? 0}
{formatCount($workspaceCounts.knowledge)}
</span>
</a>
{/if}
@@ -189,7 +191,7 @@
>
<span>{$i18n.t('Prompts')}</span>
<span class="text-sm text-gray-500 dark:text-gray-500">
{$workspaceCounts.prompts ?? 0}
{formatCount($workspaceCounts.prompts)}
</span>
</a>
{/if}
@@ -206,7 +208,7 @@
>
<span>{$i18n.t('Skills')}</span>
<span class="text-sm text-gray-500 dark:text-gray-500">
{$workspaceCounts.skills ?? 0}
{formatCount($workspaceCounts.skills)}
</span>
</a>
{/if}
@@ -223,7 +225,7 @@
>
<span>{$i18n.t('Tools')}</span>
<span class="text-sm text-gray-500 dark:text-gray-500">
{$workspaceCounts.tools ?? 0}
{formatCount($workspaceCounts.tools)}
</span>
</a>
{/if}