mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-28 06:22:33 +00:00
refac
This commit is contained in:
parent
2a86805648
commit
131ddc32ec
17 changed files with 499 additions and 245 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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())]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()}`, {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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={() => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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={() => {
|
||||
|
|
|
|||
|
|
@ -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={() => {
|
||||
|
|
|
|||
|
|
@ -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={() => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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": "",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue