This commit is contained in:
Timothy Jaeryang Baek 2026-10-21 11:07:10 +09:00
parent 2a86805648
commit 131ddc32ec
17 changed files with 499 additions and 245 deletions

View file

@ -23,6 +23,7 @@ from sqlalchemy import (
cast,
delete,
func,
literal,
select,
)
from sqlalchemy.ext.asyncio import AsyncSession
@ -961,20 +962,36 @@ class ChatMessageTable:
result = await db.execute(stmt)
return {row.chat_id: row.count for row in result.all()}
async def get_daily_message_counts_by_model(
async def get_usage_over_time_by_model(
self,
start_date: Optional[int] = None,
end_date: Optional[int] = None,
group_id: Optional[str] = None,
db: Optional[AsyncSession] = None,
granularity: str = 'daily',
metric: str = 'messages',
) -> dict[str, dict[str, int]]:
"""Get message counts grouped by day and model."""
async with get_async_db_context(db) as db:
from datetime import datetime, timedelta
"""Get message counts or total tokens grouped by time bucket and model."""
bucket_minutes = {'10m': 10, 'hourly': 60, 'daily': 1440}[granularity]
date_format = '%Y-%m-%d' if granularity == 'daily' else '%Y-%m-%d %H:%M'
step = timedelta(minutes=bucket_minutes)
def bucket_start(timestamp):
date = datetime.fromtimestamp(_normalize_timestamp(timestamp))
minutes = date.hour * 60 + date.minute
minutes = minutes // bucket_minutes * bucket_minutes
return date.replace(hour=minutes // 60, minute=minutes % 60, second=0, microsecond=0)
async with get_async_db_context(db) as db:
from open_webui.models.groups import group_user_memberships
stmt = select(ChatMessage.created_at, ChatMessage.model_id).filter(
value = literal(1)
if metric == 'tokens':
bind = await db.connection()
input_tokens, output_tokens = _token_columns(bind.dialect.name)
value = func.coalesce(input_tokens, 0) + func.coalesce(output_tokens, 0)
stmt = select(ChatMessage.created_at, ChatMessage.model_id, value).filter(
ChatMessage.role == 'assistant',
ChatMessage.model_id.isnot(None),
)
@ -988,78 +1005,20 @@ class ChatMessageTable:
stmt = stmt.filter(ChatMessage.user_id.in_(group_users))
result = await db.execute(stmt)
results = result.all()
counts: dict[str, dict[str, int]] = {}
for timestamp, model_id, value in result.all():
date_str = bucket_start(timestamp).strftime(date_format)
bucket = counts.setdefault(date_str, {})
bucket[model_id] = bucket.get(model_id, 0) + int(value)
# Group by date -> model -> count
daily_counts: dict[str, dict[str, int]] = {}
for timestamp, model_id in results:
date_str = datetime.fromtimestamp(_normalize_timestamp(timestamp)).strftime('%Y-%m-%d')
if date_str not in daily_counts:
daily_counts[date_str] = {}
daily_counts[date_str][model_id] = daily_counts[date_str].get(model_id, 0) + 1
# Fill in missing days
if start_date and end_date:
current = datetime.fromtimestamp(_normalize_timestamp(start_date))
current = bucket_start(start_date)
end_dt = datetime.fromtimestamp(_normalize_timestamp(end_date))
while current <= end_dt:
date_str = current.strftime('%Y-%m-%d')
if date_str not in daily_counts:
daily_counts[date_str] = {}
current += timedelta(days=1)
counts.setdefault(current.strftime(date_format), {})
current += step
return daily_counts
async def get_hourly_message_counts_by_model(
self,
start_date: Optional[int] = None,
end_date: Optional[int] = None,
group_id: Optional[str] = None,
db: Optional[AsyncSession] = None,
) -> dict[str, dict[str, int]]:
"""Get message counts grouped by hour and model."""
async with get_async_db_context(db) as db:
from datetime import datetime, timedelta
from open_webui.models.groups import group_user_memberships
stmt = select(ChatMessage.created_at, ChatMessage.model_id).filter(
ChatMessage.role == 'assistant',
ChatMessage.model_id.isnot(None),
)
if start_date:
stmt = stmt.filter(ChatMessage.created_at >= start_date)
if end_date:
stmt = stmt.filter(ChatMessage.created_at <= end_date)
if group_id:
group_users = select(group_user_memberships([group_id], True).c.user_id).scalar_subquery()
stmt = stmt.filter(ChatMessage.user_id.in_(group_users))
result = await db.execute(stmt)
results = result.all()
# Group by hour -> model -> count
hourly_counts: dict[str, dict[str, int]] = {}
for timestamp, model_id in results:
hour_str = datetime.fromtimestamp(_normalize_timestamp(timestamp)).strftime('%Y-%m-%d %H:00')
if hour_str not in hourly_counts:
hourly_counts[hour_str] = {}
hourly_counts[hour_str][model_id] = hourly_counts[hour_str].get(model_id, 0) + 1
# Fill in missing hours
if start_date and end_date:
current = datetime.fromtimestamp(_normalize_timestamp(start_date)).replace(
minute=0, second=0, microsecond=0
)
end_dt = datetime.fromtimestamp(_normalize_timestamp(end_date))
while current <= end_dt:
hour_str = current.strftime('%Y-%m-%d %H:00')
if hour_str not in hourly_counts:
hourly_counts[hour_str] = {}
current += timedelta(hours=1)
return hourly_counts
return counts
ChatMessages = ChatMessageTable()

View file

@ -1,7 +1,7 @@
import logging
from collections import defaultdict
from datetime import datetime, timedelta
from typing import Optional
from typing import Literal, Optional
from fastapi import APIRouter, Depends, Query
from open_webui.internal.db import get_async_session
@ -201,19 +201,20 @@ async def get_daily_stats(
start_date: Optional[int] = Query(None, description='Start timestamp (epoch)'),
end_date: Optional[int] = Query(None, description='End timestamp (epoch)'),
group_id: Optional[str] = Query(None, description='Filter by user group ID'),
granularity: str = Query('daily', description="Granularity: 'hourly' or 'daily'"),
granularity: Literal['10m', 'hourly', 'daily'] = Query('daily'),
metric: Literal['messages', 'tokens'] = Query('messages'),
user=Depends(get_admin_user),
db: AsyncSession = Depends(get_async_session),
):
"""Get message counts grouped by model for time-series chart."""
if granularity == 'hourly':
counts = await ChatMessages.get_hourly_message_counts_by_model(
start_date=start_date, end_date=end_date, group_id=group_id, db=db
)
else:
counts = await ChatMessages.get_daily_message_counts_by_model(
start_date=start_date, end_date=end_date, group_id=group_id, db=db
)
"""Get message counts or total tokens grouped by model for the time-series chart."""
counts = await ChatMessages.get_usage_over_time_by_model(
start_date=start_date,
end_date=end_date,
group_id=group_id,
db=db,
granularity=granularity,
metric=metric,
)
return DailyStatsResponse(
data=[DailyStatsEntry(date=date, models=models) for date, models in sorted(counts.items())]
)

View file

@ -166,8 +166,9 @@ export const getDailyStats = async (
token: string = '',
startDate: number | null = null,
endDate: number | null = null,
granularity: 'hourly' | 'daily' = 'daily',
groupId: string | null = null
granularity: '10m' | 'hourly' | 'daily' = 'daily',
groupId: string | null = null,
metric: 'messages' | 'tokens' = 'messages'
) => {
let error = null;
@ -175,6 +176,7 @@ export const getDailyStats = async (
if (startDate) searchParams.append('start_date', startDate.toString());
if (endDate) searchParams.append('end_date', endDate.toString());
searchParams.append('granularity', granularity);
searchParams.append('metric', metric);
if (groupId) searchParams.append('group_id', groupId);
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/daily?${searchParams.toString()}`, {

View file

@ -86,7 +86,9 @@
{@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 === 'hour'
? 'h:mm A'
: 'M/D h A'
: period === 'year' || period === 'all'
? 'M/D/YY'
: 'M/D'}
@ -106,15 +108,17 @@
{#if hovered}
{@const total = Object.values(hovered.models || {}).reduce((a, b) => a + b, 0)}
<div
class="pointer-events-none absolute top-1 text-[0.6875rem]"
style="left:{Math.min(Math.max((mouseX / w) * 100, 8), 92)}%"
class="pointer-events-none absolute top-1 w-56 max-w-full text-[0.6875rem]"
style="left:clamp(0px, calc({(mouseX / w) * 100}% - 7rem), max(0px, calc(100% - 14rem)))"
>
<div
class="min-w-[8.75rem] -translate-x-1/2 rounded border border-gray-100 bg-white px-2.5 py-1.5 shadow-sm dark:border-gray-800 dark:bg-gray-900"
class="rounded border border-gray-100 bg-white px-2.5 py-1.5 shadow-sm dark:border-gray-800 dark:bg-gray-900"
>
<div class="mb-1.5 text-[0.625rem] text-gray-400">
{#if hovered.date?.includes(':')}
{dayjs(hovered.date).locale($i18n.language).format('MMM D, h A')}
{dayjs(hovered.date)
.locale($i18n.language)
.format(period === 'hour' ? 'MMM D, h:mm A' : 'MMM D, h A')}
{:else}
{dayjs(hovered.date).locale($i18n.language).format('MMM D, YYYY')}
{/if}

View file

@ -25,6 +25,10 @@
// Time period - persist in localStorage
let selectedPeriod =
(typeof localStorage !== 'undefined' && localStorage.getItem('analyticsPeriod')) || '7d';
let granularity: '10m' | 'hourly' | 'daily';
$: granularity =
selectedPeriod === '24h' ? '10m' : ['7d', '30d'].includes(selectedPeriod) ? 'hourly' : 'daily';
let selectedMetric: 'messages' | 'tokens' = 'messages';
// Custom date range (YYYY-MM-DD) - persist in localStorage
let customStart =
@ -130,12 +134,11 @@
loading = true;
try {
const { start, end } = getDateRange(selectedPeriod);
const granularity = selectedPeriod === '24h' ? 'hourly' : 'daily';
const [summaryRes, modelsRes, usersRes, dailyRes, tokensRes] = await Promise.all([
getSummary(localStorage.token, start, end, selectedGroupId),
getModelAnalytics(localStorage.token, start, end, selectedGroupId),
getUserAnalytics(localStorage.token, start, end, 50, selectedGroupId),
getDailyStats(localStorage.token, start, end, granularity, selectedGroupId),
getDailyStats(localStorage.token, start, end, granularity, selectedGroupId, selectedMetric),
getTokenUsage(localStorage.token, start, end, selectedGroupId)
]);
@ -181,6 +184,7 @@
customStart;
customEnd;
selectedGroupId;
selectedMetric;
loadDashboard();
}
@ -354,7 +358,7 @@
>
</div>
<!-- Daily usage chart -->
<!-- Usage chart -->
{#if dailyStats.length > 1}
{@const allModels = [...new Set(dailyStats.flatMap((d) => Object.keys(d.models || {})))]}
{@const topModels = allModels.slice(0, 8)}
@ -370,9 +374,26 @@
]}
{@const periodMap = { '24h': 'hour', '7d': 'week', '30d': 'month', '90d': 'year', all: 'all' }}
<div class="mb-4">
<div class="text-xs font-normal text-gray-600 dark:text-gray-400 mb-2 px-0.5">
{selectedPeriod === '24h' ? $i18n.t('Hourly Messages') : $i18n.t('Daily Messages')}
</div>
<select
bind:value={selectedMetric}
aria-label={$i18n.t('Chart metric')}
class="text-xs font-normal text-gray-600 dark:text-gray-400 mb-2 pl-0.5 pr-8 rounded-sm bg-transparent"
>
<option value="messages">
{granularity === '10m'
? $i18n.t('10-Minute Messages')
: granularity === 'hourly'
? $i18n.t('Hourly Messages')
: $i18n.t('Daily Messages')}
</option>
<option value="tokens">
{granularity === '10m'
? $i18n.t('10-Minute Tokens')
: granularity === 'hourly'
? $i18n.t('Hourly Tokens')
: $i18n.t('Daily Tokens')}
</option>
</select>
<ChartLine
data={dailyStats}
models={topModels}

View file

@ -13,7 +13,7 @@
<label
for={`${labelId}-control`}
id={labelId}
class="min-w-0 text-xs text-gray-600 group-has-[[role=switch]:enabled]/setting-row:cursor-pointer group-has-[[role=switch]:disabled]/setting-row:cursor-not-allowed transition-colors duration-150 group-hover/setting-row:text-gray-900 group-focus-within/setting-row:text-gray-900 dark:text-gray-400 dark:group-hover/setting-row:text-white dark:group-focus-within/setting-row:text-white {labelClassName}"
class="min-w-0 text-xs text-gray-600 group-has-[button[id$=-control]:enabled]/setting-row:cursor-pointer group-has-[button[id$=-control]:disabled]/setting-row:cursor-not-allowed transition-colors duration-150 group-hover/setting-row:text-gray-900 group-focus-within/setting-row:text-gray-900 dark:text-gray-400 dark:group-hover/setting-row:text-white dark:group-focus-within/setting-row:text-white {labelClassName}"
>
<slot name="label">{label}</slot>
</label>

View file

@ -459,7 +459,7 @@
</AdminSettingField>
</AdminSettingSection>
<AdminSettingSection>
<AdminSettingRow label={$i18n.t('Scopes')}>
<AdminSettingRow let:labelId label={$i18n.t('Scopes')}>
<div class="flex items-center gap-3 text-xs">
<span class="text-gray-400 dark:text-gray-600"
>{$i18n.t('{{COUNT}} selected', {
@ -467,6 +467,7 @@
})}</span
>
<button
id={`${labelId}-control`}
type="button"
class="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white hover:underline"
on:click={() => {

View file

@ -86,10 +86,12 @@
<AdminSettingSection title={$i18n.t('settings.admin.db.sections.config.title')} first>
<AdminSettingRow
let:labelId
label={$i18n.t('settings.admin.db.importConfig.label')}
description={$i18n.t('settings.admin.db.importConfig.description')}
>
<button
id={`${labelId}-control`}
class={actionButtonClass}
on:click={() => {
configImportInputElement.click();
@ -101,10 +103,12 @@
</AdminSettingRow>
<AdminSettingRow
let:labelId
label={$i18n.t('settings.admin.db.exportConfig.label')}
description={$i18n.t('settings.admin.db.exportConfig.description')}
>
<button
id={`${labelId}-control`}
class={actionButtonClass}
on:click={async () => {
const config = await exportConfig(localStorage.token);
@ -123,10 +127,12 @@
{#if $config?.features.enable_admin_export ?? true}
<AdminSettingSection title={$i18n.t('settings.admin.db.sections.export.title')}>
<AdminSettingRow
let:labelId
label={$i18n.t('settings.admin.db.database.label')}
description={$i18n.t('settings.admin.db.database.description')}
>
<button
id={`${labelId}-control`}
class={actionButtonClass}
on:click={() => {
downloadDatabase(localStorage.token).catch((error) => {
@ -140,19 +146,31 @@
</AdminSettingRow>
<AdminSettingRow
let:labelId
label={$i18n.t('settings.admin.db.allChats.label')}
description={$i18n.t('settings.admin.db.allChats.description')}
>
<button class={actionButtonClass} on:click={exportAllUserChats} type="button">
<button
id={`${labelId}-control`}
class={actionButtonClass}
on:click={exportAllUserChats}
type="button"
>
{$i18n.t('Export')}
</button>
</AdminSettingRow>
<AdminSettingRow
let:labelId
label={$i18n.t('settings.admin.db.users.label')}
description={$i18n.t('settings.admin.db.users.description')}
>
<button class={actionButtonClass} on:click={exportUsers} type="button">
<button
id={`${labelId}-control`}
class={actionButtonClass}
on:click={exportUsers}
type="button"
>
{$i18n.t('Export')}
</button>
</AdminSettingRow>

View file

@ -1370,10 +1370,12 @@
</AdminSettingField>
<AdminSettingRow
let:labelId
label={$i18n.t('settings.admin.documents.bm25Weight.label')}
description={$i18n.t('settings.admin.documents.bm25Weight.description')}
>
<button
id={`${labelId}-control`}
class={actionButtonClass}
type="button"
on:click={() => {
@ -1543,10 +1545,12 @@
<AdminSettingSection title={$i18n.t('settings.admin.documents.sections.dangerZone.title')}>
<AdminSettingRow
let:labelId
label={$i18n.t('settings.admin.documents.resetUploadDirectory.label')}
description={$i18n.t('settings.admin.documents.resetUploadDirectory.description')}
>
<button
id={`${labelId}-control`}
class={actionButtonClass}
type="button"
on:click={() => {
@ -1557,10 +1561,12 @@
</button>
</AdminSettingRow>
<AdminSettingRow
let:labelId
label={$i18n.t('settings.admin.documents.resetVectorStorageKnowledge.label')}
description={$i18n.t('settings.admin.documents.resetVectorStorageKnowledge.description')}
>
<button
id={`${labelId}-control`}
class={actionButtonClass}
type="button"
on:click={() => {
@ -1571,12 +1577,14 @@
</button>
</AdminSettingRow>
<AdminSettingRow
let:labelId
label={$i18n.t('settings.admin.documents.reindexKnowledgeAndMemoryVectors.label')}
description={$i18n.t(
'settings.admin.documents.reindexKnowledgeAndMemoryVectors.description'
)}
>
<button
id={`${labelId}-control`}
class={actionButtonClass}
type="button"
on:click={() => {

View file

@ -401,10 +401,12 @@
{#each Object.keys(valves_spec.properties) as property}
<div>
<AdminSettingRow
let:labelId
label={valves_spec.properties[property].title}
description={valves_spec.properties[property]?.description ?? ''}
>
<button
id={`${labelId}-control`}
class={actionButtonClass}
type="button"
on:click={() => {

View file

@ -389,9 +389,13 @@
{#if canUseApiKeys({ user: $user, config: $config })}
<UserSettingSection title={$i18n.t('settings.personal.account.sections.apiKeys.title')}>
<UserSettingRow description={$i18n.t('settings.personal.account.secrets.description')}>
<UserSettingRow
let:labelId
description={$i18n.t('settings.personal.account.secrets.description')}
>
<span slot="label">{$i18n.t('settings.personal.account.secrets.label')}</span>
<button
id={`${labelId}-control`}
class={actionButtonClass}
type="button"
on:click={() => {

View file

@ -162,10 +162,12 @@
>
{#if canManageChats({ user: $user, config: null }, 'import')}
<UserSettingRow
let:labelId
label={$i18n.t('settings.personal.dataControls.importChats.label')}
description={$i18n.t('settings.personal.dataControls.importChats.description')}
>
<button
id={`${labelId}-control`}
class={actionButtonClass}
on:click={() => {
chatImportInputElement.click();
@ -179,10 +181,12 @@
{#if canManageChats({ user: $user, config: null }, 'export')}
<UserSettingRow
let:labelId
label={$i18n.t('settings.personal.dataControls.exportChats.label')}
description={$i18n.t('settings.personal.dataControls.exportChats.description')}
>
<button
id={`${labelId}-control`}
class={actionButtonClass}
on:click={() => {
exportChats();
@ -195,10 +199,12 @@
{/if}
<UserSettingRow
let:labelId
label={$i18n.t('settings.personal.dataControls.sharedChats.label')}
description={$i18n.t('settings.personal.dataControls.sharedChats.description')}
>
<button
id={`${labelId}-control`}
class={actionButtonClass}
on:click={() => {
showSharedChatsModal = true;
@ -210,10 +216,12 @@
</UserSettingRow>
<UserSettingRow
let:labelId
label={$i18n.t('settings.personal.dataControls.archiveAllChats.label')}
description={$i18n.t('settings.personal.dataControls.archiveAllChats.description')}
>
<button
id={`${labelId}-control`}
class={actionButtonClass}
on:click={() => {
showArchiveConfirmDialog = true;
@ -226,10 +234,12 @@
{#if canManageChats({ user: $user, config: null }, 'delete')}
<UserSettingRow
let:labelId
label={$i18n.t('settings.personal.dataControls.deleteAllChats.label')}
description={$i18n.t('settings.personal.dataControls.deleteAllChats.description')}
>
<button
id={`${labelId}-control`}
class={actionButtonClass}
on:click={() => {
showDeleteConfirmDialog = true;
@ -244,10 +254,12 @@
<UserSettingSection title={$i18n.t('settings.personal.dataControls.sections.files.title')}>
<UserSettingRow
let:labelId
label={$i18n.t('settings.personal.dataControls.manageFiles.label')}
description={$i18n.t('settings.personal.dataControls.manageFiles.description')}
>
<button
id={`${labelId}-control`}
class={actionButtonClass}
on:click={() => {
showFilesModal = true;

View file

@ -300,10 +300,12 @@
title={$i18n.t('settings.personal.general.sections.advancedParameters.title')}
>
<UserSettingRow
let:labelId
description={$i18n.t('settings.personal.general.modelParameters.description')}
>
<span slot="label">{$i18n.t('settings.personal.general.modelParameters.label')}</span>
<button
id={`${labelId}-control`}
class={actionButtonClass}
type="button"
aria-expanded={showAdvanced}

View file

@ -13,7 +13,7 @@
<label
id={labelId}
for={`${labelId}-control`}
class="min-w-0 text-xs text-gray-600 group-has-[[role=switch]:enabled]/setting-row:cursor-pointer group-has-[[role=switch]:disabled]/setting-row:cursor-not-allowed transition-colors duration-150 group-hover/setting-row:text-gray-900 group-focus-within/setting-row:text-gray-900 dark:text-gray-400 dark:group-hover/setting-row:text-white dark:group-focus-within/setting-row:text-white {labelClassName}"
class="min-w-0 text-xs text-gray-600 group-has-[button[id$=-control]:enabled]/setting-row:cursor-pointer group-has-[button[id$=-control]:disabled]/setting-row:cursor-not-allowed transition-colors duration-150 group-hover/setting-row:text-gray-900 group-focus-within/setting-row:text-gray-900 dark:text-gray-400 dark:group-hover/setting-row:text-white dark:group-focus-within/setting-row:text-white {labelClassName}"
>
<slot name="label">{label}</slot>
</label>

View file

@ -9,7 +9,7 @@
import { getContext, tick } from 'svelte';
import type { Writable } from 'svelte/store';
import { toast } from 'svelte-sonner';
import { config, models, settings, user } from '$lib/stores';
import { config, mobile, models, settings, user } from '$lib/stores';
import type { SettingsModalRequest } from '$lib/stores';
import { getUserSettings, updateUserSettings } from '$lib/apis/users';
import { getBackendConfig, getModels as _getModels } from '$lib/apis';
@ -101,6 +101,7 @@
$: if (show !== lastShow) {
lastShow = show;
search = '';
showSearch = false;
if (show && typeof show === 'object') {
selectedTab = show.tab;
tabState = show.state ?? null;
@ -129,6 +130,7 @@
$: if (!modalShow && show !== false) {
show = false;
search = '';
showSearch = false;
lastShow = false;
selectedTab = 'general';
tabState = null;
@ -377,6 +379,9 @@
let filteredAdminSettings: string[] = [];
let search = '';
let showSearch = false;
let searchInput: HTMLInputElement;
let searchButton: HTMLButtonElement;
let englishRequested = false;
$: if (modalShow && !englishRequested) {
englishRequested = true;
@ -435,11 +440,19 @@
selectedTab = id;
};
const toggleSearch = async () => {
showSearch = !(showSearch || search);
if (!showSearch) search = '';
await tick();
(showSearch ? searchInput : searchButton)?.focus();
};
const searchKeydown = (event: KeyboardEvent) => {
if (event.key === 'Escape' && search) {
if (event.key === 'Escape' && (search || $mobile)) {
event.preventDefault();
event.stopPropagation();
search = '';
if (search) search = '';
else toggleSearch();
} else if (event.key === 'Enter') {
event.preventDefault();
if (filteredSettings.length) selectTab(filteredSettings[0]);
@ -526,34 +539,50 @@
id="settings-tabs-container"
class="shrink-0 min-w-0 md:min-h-0 flex flex-col border-b md:border-b-0 md:border-r border-gray-100/30 dark:border-white/[0.02] md:w-[15rem]"
>
<button
class="flex items-center gap-1.5 h-7 px-2 m-1 md:mb-0 md:w-[calc(100%-0.5rem)] shrink-0 rounded-lg text-xs text-gray-600 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition-colors duration-75"
type="button"
on:click={() => {
show = false;
}}
>
<ChevronLeft className="size-3" strokeWidth="2" />
<span>{$i18n.t('Back')}</span>
</button>
<div
class="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">
<div class="flex items-center gap-1 m-1 md:mb-0 shrink-0">
<button
class="flex flex-1 items-center gap-1.5 h-7 px-2 rounded-lg text-xs text-gray-600 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition-colors duration-75"
type="button"
on:click={() => {
show = false;
}}
>
<ChevronLeft className="size-3" strokeWidth="2" />
<span>{$i18n.t('Back')}</span>
</button>
<button
bind:this={searchButton}
type="button"
class="flex md:hidden size-7 shrink-0 items-center justify-center rounded-lg text-gray-600 hover:bg-gray-50 hover:text-gray-700 dark:text-gray-500 dark:hover:bg-white/[0.04] dark:hover:text-gray-300"
aria-label={$i18n.t('Search')}
aria-expanded={showSearch || !!search}
aria-controls="search-input-settings-modal"
on:click={toggleSearch}
>
<Search className="size-3.5" strokeWidth="1.5" />
</div>
<label class="sr-only" for="search-input-settings-modal">{$i18n.t('Search')}</label>
<input
data-settings-search
class="w-full text-xs bg-transparent py-1 outline-hidden placeholder:text-gray-600 dark:text-gray-300 dark:placeholder:text-gray-500"
bind:value={search}
id="search-input-settings-modal"
on:keydown={searchKeydown}
placeholder={$i18n.t('Search')}
/>
</button>
</div>
{#if !$mobile || showSearch || search}
<div
class="flex items-center gap-1.5 h-7 px-2 mx-1 md: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 className="size-3.5" strokeWidth="1.5" />
</div>
<label class="sr-only" for="search-input-settings-modal">{$i18n.t('Search')}</label>
<input
bind:this={searchInput}
data-settings-search
class="w-full text-xs bg-transparent py-1 outline-hidden placeholder:text-gray-600 dark:text-gray-300 dark:placeholder:text-gray-500"
bind:value={search}
id="search-input-settings-modal"
on:keydown={searchKeydown}
placeholder={$i18n.t('Search')}
/>
</div>
{/if}
<div class="sr-only" role="status" aria-live="polite">
{search
? filteredSettings.length

View file

@ -119,8 +119,9 @@
let fontFamilyInput = '';
let showTextScaleSlider = false;
let showFontFamilyInput = false;
const settingRowClass = 'flex items-center justify-between gap-2.5';
const settingLabelClass = 'min-w-0 text-xs text-gray-600 dark:text-gray-400';
const settingRowClass = 'group/setting-row flex items-center justify-between gap-2.5';
const settingLabelClass =
'min-w-0 text-xs text-gray-600 group-has-[button[id$=-control]:enabled]/setting-row:cursor-pointer group-has-[button[id$=-control]:disabled]/setting-row:cursor-not-allowed transition-colors duration-150 group-hover/setting-row:text-gray-900 group-focus-within/setting-row:text-gray-900 dark:text-gray-400 dark:group-hover/setting-row:text-white dark:group-focus-within/setting-row:text-white';
const settingControlClass = 'flex shrink-0 items-center justify-end gap-1.5';
const sectionHeadingClass = 'mt-4 text-xs text-gray-600 dark:text-gray-500';
const firstSectionHeadingClass = 'text-xs text-gray-600 dark:text-gray-500';
@ -460,12 +461,13 @@
<div>
<div class={settingRowClass}>
<label id="ui-scale-label" class={settingLabelClass} for="ui-scale-slider">
<label id="ui-scale-label" class={settingLabelClass} for="ui-scale-label-control">
{$i18n.t('settings.personal.interface.uiScale.label')}
</label>
<div class={settingControlClass}>
<button
id="ui-scale-label-control"
class={actionButtonClass}
aria-live="polite"
type="button"
@ -548,12 +550,13 @@
<div>
<div class={settingRowClass}>
<label id="font-family-label" class={settingLabelClass} for="font-family-input">
<label id="font-family-label" class={settingLabelClass} for="font-family-label-control">
{$i18n.t('settings.personal.interface.fontFamily.label')}
</label>
<div class={settingControlClass}>
<button
id="font-family-label-control"
class={actionButtonClass}
aria-labelledby="font-family-label font-family-state"
type="button"
@ -588,9 +591,13 @@
<div>
<div class={settingRowClass}>
<div id="accessibility-mode-label" class={settingLabelClass}>
<label
id="accessibility-mode-label"
class={settingLabelClass}
for="accessibility-mode-label-control"
>
{$i18n.t('settings.personal.interface.accessibilityMode.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -611,9 +618,13 @@
<div>
<div class={settingRowClass}>
<div id="use-chat-title-as-tab-title-label" class={settingLabelClass}>
<label
id="use-chat-title-as-tab-title-label"
class={settingLabelClass}
for="use-chat-title-as-tab-title-label-control"
>
{$i18n.t('settings.personal.interface.displayChatTitleInTab.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -633,10 +644,14 @@
</div>
<div>
<div id="allow-user-location-label" class={settingRowClass}>
<div class={settingLabelClass}>
<div class={settingRowClass}>
<label
id="allow-user-location-label"
class={settingLabelClass}
for="allow-user-location-label-control"
>
{$i18n.t('settings.personal.interface.allowUserLocation.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -657,11 +672,15 @@
<div>
<div class={settingRowClass}>
<div id="haptic-feedback-label" class={settingLabelClass}>
<label
id="haptic-feedback-label"
class={settingLabelClass}
for="haptic-feedback-label-control"
>
{$i18n.t('settings.personal.interface.hapticFeedback.label')} ({$i18n.t(
'settings.personal.interface.android.label'
)})
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -682,9 +701,9 @@
<div>
<div class={settingRowClass}>
<div id="copy-formatted-label" class={settingLabelClass}>
<label id="copy-formatted-label" class={settingLabelClass} for="copy-formatted-label-control">
{$i18n.t('settings.personal.interface.copyFormattedText.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -706,9 +725,13 @@
{#if isSettingsAdmin({ user: $user, config: $config })}
<div>
<div class={settingRowClass}>
<div id="toast-notifications-label" class={settingLabelClass}>
<label
id="toast-notifications-label"
class={settingLabelClass}
for="toast-notifications-label-control"
>
{$i18n.t('settings.personal.interface.toastNotificationsForNewUpdates.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -729,9 +752,9 @@
<div>
<div class={settingRowClass}>
<div id="whats-new-label" class={settingLabelClass}>
<label id="whats-new-label" class={settingLabelClass} for="whats-new-label-control">
{$i18n.t(`Show "What's New" Modal on Login`)}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -757,9 +780,13 @@
<div>
<div class={settingRowClass}>
<div id="enable-message-queue-label" class={settingLabelClass}>
<label
id="enable-message-queue-label"
class={settingLabelClass}
for="enable-message-queue-label-control"
>
{$i18n.t('settings.personal.interface.enableMessageQueue.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -780,11 +807,12 @@
<div>
<div class={settingRowClass}>
<div id="chat-direction-label" class={settingLabelClass}>
<label id="chat-direction-label" class={settingLabelClass} for="chat-direction-label-control">
{$i18n.t('settings.personal.interface.chatDirection.label')}
</div>
</label>
<button
id="chat-direction-label-control"
aria-labelledby="chat-direction-label chat-direction-mode"
class={actionButtonClass}
on:click={toggleChangeChatDirection}
@ -806,11 +834,16 @@
<div>
<div class={settingRowClass}>
<div id="landing-page-mode-label" class={settingLabelClass}>
<label
id="landing-page-mode-label"
class={settingLabelClass}
for="landing-page-mode-label-control"
>
{$i18n.t('settings.personal.interface.landingPageMode.label')}
</div>
</label>
<button
id="landing-page-mode-label-control"
aria-labelledby="landing-page-mode-label notification-sound-state"
class={actionButtonClass}
on:click={() => {
@ -832,11 +865,16 @@
<div>
<div class={settingRowClass}>
<div id="chat-background-label" class={settingLabelClass}>
<label
id="chat-background-label"
class={settingLabelClass}
for="chat-background-label-control"
>
{$i18n.t('settings.personal.interface.chatBackgroundImage.label')}
</div>
</label>
<button
id="chat-background-label-control"
aria-labelledby="chat-background-label background-image-url-state"
class={actionButtonClass}
on:click={() => {
@ -861,9 +899,9 @@
<div>
<div class={settingRowClass}>
<div id="chat-bubble-ui-label" class={settingLabelClass}>
<label id="chat-bubble-ui-label" class={settingLabelClass} for="chat-bubble-ui-label-control">
{$i18n.t('settings.personal.interface.chatBubbleUi.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -885,9 +923,13 @@
{#if !chatBubble}
<div>
<div class={settingRowClass}>
<div id="chat-bubble-username-label" class={settingLabelClass}>
<label
id="chat-bubble-username-label"
class={settingLabelClass}
for="chat-bubble-username-label-control"
>
{$i18n.t('settings.personal.interface.displayTheUsernameInsteadOfYouInTheChat.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -909,9 +951,13 @@
<div>
<div class={settingRowClass}>
<div id="widescreen-mode-label" class={settingLabelClass}>
<label
id="widescreen-mode-label"
class={settingLabelClass}
for="widescreen-mode-label-control"
>
{$i18n.t('settings.personal.interface.widescreenMode.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -933,9 +979,13 @@
{#if canUseTemporaryChats({ user: $user, config: $config })}
<div>
<div class={settingRowClass}>
<div id="temp-chat-default-label" class={settingLabelClass}>
<label
id="temp-chat-default-label"
class={settingLabelClass}
for="temp-chat-default-label-control"
>
{$i18n.t('settings.personal.interface.temporaryChatByDefault.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -957,9 +1007,9 @@
<div>
<div class={settingRowClass}>
<div id="fade-streaming-label" class={settingLabelClass}>
<label id="fade-streaming-label" class={settingLabelClass} for="fade-streaming-label-control">
{$i18n.t('settings.personal.interface.fadeEffectForStreamingText.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -980,9 +1030,13 @@
<div>
<div class={settingRowClass}>
<div id="render-markdown-user-label" class={settingLabelClass}>
<label
id="render-markdown-user-label"
class={settingLabelClass}
for="render-markdown-user-label-control"
>
{$i18n.t('settings.personal.interface.renderMarkdownInUserMessages.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1003,9 +1057,13 @@
<div>
<div class={settingRowClass}>
<div id="render-markdown-assistant-label" class={settingLabelClass}>
<label
id="render-markdown-assistant-label"
class={settingLabelClass}
for="render-markdown-assistant-label-control"
>
{$i18n.t('settings.personal.interface.renderMarkdownInAssistantMessages.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1026,9 +1084,13 @@
<div>
<div class={settingRowClass}>
<div id="render-markdown-in-previews-label" class={settingLabelClass}>
<label
id="render-markdown-in-previews-label"
class={settingLabelClass}
for="render-markdown-in-previews-label-control"
>
{$i18n.t('settings.personal.interface.renderMarkdownInPreviews.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1049,9 +1111,13 @@
<div>
<div class={settingRowClass}>
<div id="auto-generation-label" class={settingLabelClass}>
<label
id="auto-generation-label"
class={settingLabelClass}
for="auto-generation-label-control"
>
{$i18n.t('settings.personal.interface.titleAutoGeneration.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1072,9 +1138,13 @@
<div>
<div class={settingRowClass}>
<div class={settingLabelClass} id="follow-up-auto-generation-label">
<label
class={settingLabelClass}
id="follow-up-auto-generation-label"
for="follow-up-auto-generation-label-control"
>
{$i18n.t('settings.personal.interface.followUpAutoGeneration.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1095,9 +1165,9 @@
<div>
<div class={settingRowClass}>
<div id="chat-tags-label" class={settingLabelClass}>
<label id="chat-tags-label" class={settingLabelClass} for="chat-tags-label-control">
{$i18n.t('settings.personal.interface.chatTagsAutoGeneration.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1118,9 +1188,9 @@
<div>
<div class={settingRowClass}>
<div id="auto-copy-label" class={settingLabelClass}>
<label id="auto-copy-label" class={settingLabelClass} for="auto-copy-label-control">
{$i18n.t('settings.personal.interface.autoCopyResponseToClipboard.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1141,9 +1211,13 @@
<div>
<div class={settingRowClass}>
<div id="response-auto-scroll-label" class={settingLabelClass}>
<label
id="response-auto-scroll-label"
class={settingLabelClass}
for="response-auto-scroll-label-control"
>
{$i18n.t('settings.personal.interface.responseAutoScroll.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1164,9 +1238,13 @@
<div>
<div class={settingRowClass}>
<div id="scroll-on-branch-change-label" class={settingLabelClass}>
<label
id="scroll-on-branch-change-label"
class={settingLabelClass}
for="scroll-on-branch-change-label-control"
>
{$i18n.t('settings.personal.interface.scrollOnBranchChange.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1187,9 +1265,13 @@
<div>
<div class={settingRowClass}>
<div id="insert-suggestion-prompt-label" class={settingLabelClass}>
<label
id="insert-suggestion-prompt-label"
class={settingLabelClass}
for="insert-suggestion-prompt-label-control"
>
{$i18n.t('settings.personal.interface.insertSuggestionPromptToInput.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1210,9 +1292,13 @@
<div>
<div class={settingRowClass}>
<div id="keep-follow-up-prompts-label" class={settingLabelClass}>
<label
id="keep-follow-up-prompts-label"
class={settingLabelClass}
for="keep-follow-up-prompts-label-control"
>
{$i18n.t('settings.personal.interface.keepFollowUpPromptsInChat.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1233,9 +1319,13 @@
<div>
<div class={settingRowClass}>
<div id="insert-follow-up-prompt-label" class={settingLabelClass}>
<label
id="insert-follow-up-prompt-label"
class={settingLabelClass}
for="insert-follow-up-prompt-label-control"
>
{$i18n.t('settings.personal.interface.insertFollowUpPromptToInput.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1256,9 +1346,13 @@
<div>
<div class={settingRowClass}>
<div id="regenerate-menu-label" class={settingLabelClass}>
<label
id="regenerate-menu-label"
class={settingLabelClass}
for="regenerate-menu-label-control"
>
{$i18n.t('settings.personal.interface.regenerateMenu.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1279,9 +1373,13 @@
<div>
<div class={settingRowClass}>
<div id="always-collapse-label" class={settingLabelClass}>
<label
id="always-collapse-label"
class={settingLabelClass}
for="always-collapse-label-control"
>
{$i18n.t('settings.personal.interface.alwaysCollapseCodeBlocks.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1302,9 +1400,9 @@
<div>
<div class={settingRowClass}>
<div id="always-expand-label" class={settingLabelClass}>
<label id="always-expand-label" class={settingLabelClass} for="always-expand-label-control">
{$i18n.t('settings.personal.interface.alwaysExpandDetails.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1325,9 +1423,13 @@
<div>
<div class={settingRowClass}>
<div id="chat-hover-preview-label" class={settingLabelClass}>
<label
id="chat-hover-preview-label"
class={settingLabelClass}
for="chat-hover-preview-label-control"
>
{$i18n.t('settings.personal.interface.chatHoverPreviews.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1348,9 +1450,13 @@
<div>
<div class={settingRowClass}>
<div id="keep-followup-prompts-label" class={settingLabelClass}>
<label
id="keep-followup-prompts-label"
class={settingLabelClass}
for="keep-followup-prompts-label-control"
>
{$i18n.t('settings.personal.interface.displayMultiModelResponsesInTabs.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1371,11 +1477,16 @@
<div>
<div class={settingRowClass}>
<div id="terminal-file-display-label" class={settingLabelClass}>
<label
id="terminal-file-display-label"
class={settingLabelClass}
for="terminal-file-display-label-control"
>
{$i18n.t('settings.personal.interface.terminalFileDisplay.label')}
</div>
</label>
<button
id="terminal-file-display-label-control"
aria-labelledby="terminal-file-display-label terminal-file-display-state"
class={actionButtonClass}
on:click={() => {
@ -1396,9 +1507,13 @@
<div>
<div class={settingRowClass}>
<div id="show-files-on-terminal-select-label" class={settingLabelClass}>
<label
id="show-files-on-terminal-select-label"
class={settingLabelClass}
for="show-files-on-terminal-select-label-control"
>
{$i18n.t('settings.personal.interface.showFilesOnTerminalSelect.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1419,9 +1534,13 @@
<div>
<div class={settingRowClass}>
<div id="terminal-preview-allow-same-origin-label" class={settingLabelClass}>
<label
id="terminal-preview-allow-same-origin-label"
class={settingLabelClass}
for="terminal-preview-allow-same-origin-label-control"
>
{$i18n.t('settings.personal.interface.terminalPreviewAllowSameOrigin.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1442,9 +1561,13 @@
<div>
<div class={settingRowClass}>
<div id="stylized-pdf-export-label" class={settingLabelClass}>
<label
id="stylized-pdf-export-label"
class={settingLabelClass}
for="stylized-pdf-export-label-control"
>
{$i18n.t('settings.personal.interface.stylizedPdfExport.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1465,9 +1588,13 @@
<div>
<div class={settingRowClass}>
<div id="floating-action-buttons-label" class={settingLabelClass}>
<label
id="floating-action-buttons-label"
class={settingLabelClass}
for="floating-action-buttons-label-control"
>
{$i18n.t('settings.personal.interface.floatingQuickActions.label')}
</div>
</label>
<div class={settingControlClass}>
{#if showFloatingActionButtons}
@ -1503,11 +1630,16 @@
<div>
<div class={settingRowClass}>
<div id="web-search-in-chat-label" class={settingLabelClass}>
<label
id="web-search-in-chat-label"
class={settingLabelClass}
for="web-search-in-chat-label-control"
>
{$i18n.t('settings.personal.interface.webSearchInChat.label')}
</div>
</label>
<button
id="web-search-in-chat-label-control"
aria-labelledby="web-search-in-chat-label web-search-state"
class={actionButtonClass}
on:click={() => {
@ -1531,11 +1663,16 @@
<div>
<div class={settingRowClass}>
<div id="enter-key-behavior-label ctrl-enter-to-send-state" class={settingLabelClass}>
<label
id="enter-key-behavior-label"
class={settingLabelClass}
for="enter-key-behavior-label-control"
>
{$i18n.t('settings.personal.interface.enterKeyBehavior.label')}
</div>
</label>
<button
id="enter-key-behavior-label-control"
aria-labelledby="enter-key-behavior-label"
class={actionButtonClass}
on:click={() => {
@ -1557,9 +1694,9 @@
<div>
<div class={settingRowClass}>
<div id="rich-input-label" class={settingLabelClass}>
<label id="rich-input-label" class={settingLabelClass} for="rich-input-label-control">
{$i18n.t('settings.personal.interface.richTextInputForChat.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1581,9 +1718,13 @@
{#if $config?.features?.enable_autocomplete_generation}
<div>
<div class={settingRowClass}>
<div id="prompt-autocompletion-label" class={settingLabelClass}>
<label
id="prompt-autocompletion-label"
class={settingLabelClass}
for="prompt-autocompletion-label-control"
>
{$i18n.t('settings.personal.interface.promptAutocompletion.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1606,9 +1747,13 @@
{#if richTextInput}
<div>
<div class={settingRowClass}>
<div id="show-formatting-toolbar-label" class={settingLabelClass}>
<label
id="show-formatting-toolbar-label"
class={settingLabelClass}
for="show-formatting-toolbar-label-control"
>
{$i18n.t('settings.personal.interface.showFormattingToolbar.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1629,9 +1774,13 @@
<div>
<div class={settingRowClass}>
<div id="insert-prompt-as-rich-text-label" class={settingLabelClass}>
<label
id="insert-prompt-as-rich-text-label"
class={settingLabelClass}
for="insert-prompt-as-rich-text-label-control"
>
{$i18n.t('settings.personal.interface.insertPromptAsRichText.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1653,9 +1802,9 @@
<div>
<div class={settingRowClass}>
<div id="paste-large-label" class={settingLabelClass}>
<label id="paste-large-label" class={settingLabelClass} for="paste-large-label-control">
{$i18n.t('settings.personal.interface.pasteLargeTextAsFile.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1680,9 +1829,13 @@
<div>
<div class={settingRowClass}>
<div id="detect-artifacts-label" class={settingLabelClass}>
<label
id="detect-artifacts-label"
class={settingLabelClass}
for="detect-artifacts-label-control"
>
{$i18n.t('settings.personal.interface.detectArtifactsAutomatically.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1703,9 +1856,13 @@
<div>
<div class={settingRowClass}>
<div id="iframe-sandbox-allow-scripts-label" class={settingLabelClass}>
<label
id="iframe-sandbox-allow-scripts-label"
class={settingLabelClass}
for="iframe-sandbox-allow-scripts-label-control"
>
{$i18n.t('settings.personal.interface.iframeSandboxAllowScripts.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1726,9 +1883,13 @@
<div>
<div class={settingRowClass}>
<div id="iframe-sandbox-allow-same-origin-label" class={settingLabelClass}>
<label
id="iframe-sandbox-allow-same-origin-label"
class={settingLabelClass}
for="iframe-sandbox-allow-same-origin-label-control"
>
{$i18n.t('settings.personal.interface.iframeSandboxAllowSameOrigin.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1749,9 +1910,13 @@
<div>
<div class={settingRowClass}>
<div id="iframe-sandbox-allow-forms-label" class={settingLabelClass}>
<label
id="iframe-sandbox-allow-forms-label"
class={settingLabelClass}
for="iframe-sandbox-allow-forms-label-control"
>
{$i18n.t('settings.personal.interface.iframeSandboxAllowForms.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1772,9 +1937,13 @@
<div>
<div class={settingRowClass}>
<div id="iframe-sandbox-allow-downloads-label" class={settingLabelClass}>
<label
id="iframe-sandbox-allow-downloads-label"
class={settingLabelClass}
for="iframe-sandbox-allow-downloads-label-control"
>
{$i18n.t('settings.personal.interface.iframeSandboxAllowDownloads.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1799,9 +1968,13 @@
<div>
<div class={settingRowClass}>
<div class={settingLabelClass} id="allow-voice-interruption-in-call-label">
<label
class={settingLabelClass}
id="allow-voice-interruption-in-call-label"
for="allow-voice-interruption-in-call-label-control"
>
{$i18n.t('settings.personal.interface.allowVoiceInterruptionInCall.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1822,9 +1995,9 @@
<div>
<div class={settingRowClass}>
<div id="display-emoji-label" class={settingLabelClass}>
<label id="display-emoji-label" class={settingLabelClass} for="display-emoji-label-control">
{$i18n.t('settings.personal.interface.displayEmojiInCall.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch
@ -1849,11 +2022,16 @@
<div>
<div class={settingRowClass}>
<div id="default-upload-mode-label" class={settingLabelClass}>
<label
id="default-upload-mode-label"
class={settingLabelClass}
for="default-upload-mode-label-control"
>
{$i18n.t('settings.personal.interface.defaultUploadMode.label')}
</div>
</label>
<button
id="default-upload-mode-label-control"
aria-labelledby="default-upload-mode-label default-upload-mode-state"
class={actionButtonClass}
on:click={() => {
@ -1876,9 +2054,13 @@
<div>
<div class={settingRowClass}>
<div id="image-compression-label" class={settingLabelClass}>
<label
id="image-compression-label"
class={settingLabelClass}
for="image-compression-label-control"
>
{$i18n.t('settings.personal.interface.imageCompression.label')}
</div>
</label>
<div class={settingControlClass}>
{#if imageCompression}
@ -1915,9 +2097,13 @@
{#if imageCompression}
<div>
<div class={settingRowClass}>
<div id="image-compression-in-channels-label" class={settingLabelClass}>
<label
id="image-compression-in-channels-label"
class={settingLabelClass}
for="image-compression-in-channels-label-control"
>
{$i18n.t('settings.personal.interface.compressImagesInChannels.label')}
</div>
</label>
<div class={settingControlClass}>
<Switch

View file

@ -77,6 +77,8 @@
"1 tool": "",
"1 user": "",
"10 minutes before": "",
"10-Minute Messages": "",
"10-Minute Tokens": "",
"15 minutes before": "",
"1m_time_ago": "",
"30 minutes before": "",
@ -419,6 +421,7 @@
"Channel updated successfully": "",
"Channels": "",
"Character": "",
"Chart metric": "",
"Chat": "",
"Chat actions": "",
"Chat archived.": "",
@ -694,6 +697,7 @@
"Daily": "",
"Daily at": "",
"Daily Messages": "",
"Daily Tokens": "",
"Dark": "",
"Data": "",
"Database": "",
@ -1393,6 +1397,7 @@
"Horizontal Layout": "",
"Hourly": "",
"Hourly Messages": "",
"Hourly Tokens": "",
"How can I help you today?": "",
"How would you rate this response?": "",
"HTML": "",