diff --git a/web/default/src/components/status-badge.tsx b/web/default/src/components/status-badge.tsx index 6e3b1da7..b05b8acc 100644 --- a/web/default/src/components/status-badge.tsx +++ b/web/default/src/components/status-badge.tsx @@ -131,7 +131,7 @@ export function StatusBadge({ return ( ) { - return ( - - + - - - {children} - - - - + + {children} + + + ) + + if (isMobile) { + return content + } + + return {content} } function SelectLabel({ diff --git a/web/default/src/components/ui/sidebar.tsx b/web/default/src/components/ui/sidebar.tsx index 5c16eece..2e711b85 100644 --- a/web/default/src/components/ui/sidebar.tsx +++ b/web/default/src/components/ui/sidebar.tsx @@ -40,6 +40,7 @@ import { Skeleton } from '@/components/ui/skeleton' import { Tooltip, TooltipContent, + TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip' @@ -418,7 +419,7 @@ function SidebarGroupLabel({ props: mergeProps<'div'>( { className: cn( - 'flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 ring-sidebar-ring outline-hidden transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', + 'flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 ring-sidebar-ring outline-hidden transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:pointer-events-none group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', className ), }, @@ -556,15 +557,17 @@ function SidebarMenuButton({ } return ( - - {comp} - + + + {comp} + + ) } diff --git a/web/default/src/features/channels/components/channels-columns.tsx b/web/default/src/features/channels/components/channels-columns.tsx index 1676e6d6..0d79c1d3 100644 --- a/web/default/src/features/channels/components/channels-columns.tsx +++ b/web/default/src/features/channels/components/channels-columns.tsx @@ -26,6 +26,7 @@ import { ChevronRight, ListOrdered, Shuffle, + SlidersHorizontal, } from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' @@ -301,15 +302,18 @@ function BalanceCell({ channel }: { channel: Channel }) { const usedDisplay = withSuffix(formatQuotaValue(usedQuota)) const remainingDisplay = withSuffix(formatBalance(balance)) + const usedLabel = `${t('Used:')} ${usedDisplay}` + const remainingLabel = `${t('Remaining:')} ${remainingDisplay}` // Tag row: only show cumulative used quota if (isTagRow) { return ( ) } @@ -354,14 +358,13 @@ function BalanceCell({ channel }: { channel: Channel }) { variant='neutral' size='sm' copyable={false} + showDot={false} className='cursor-help' /> } /> -

- {t('Used:')} {usedDisplay} -

+

{usedLabel}

@@ -384,6 +387,7 @@ function BalanceCell({ channel }: { channel: Channel }) { } size='sm' copyable={false} + showDot={false} className='cursor-pointer' onClick={handleClickUpdate} /> @@ -393,7 +397,7 @@ function BalanceCell({ channel }: { channel: Channel }) {

{channel.type === 57 ? t('Click to view Codex usage') - : `${t('Remaining:')} ${remainingDisplay}`} + : remainingLabel}

{channel.type !== 57 &&

{t('Click to update balance')}

} @@ -494,7 +498,6 @@ export function useChannelsColumns(): ColumnDef[] { const isTagRow = isTagAggregateRow(row.original) const name = row.getValue('name') as string const channel = row.original - const isMultiKey = isMultiKeyChannel(channel) // Tag row with expand/collapse if (isTagRow) { @@ -531,6 +534,7 @@ export function useChannelsColumns(): ColumnDef[] { // Regular channel row const settings = parseChannelSettings(channel.setting) const isPassThrough = settings.pass_through_body_enabled === true + const hasParamOverride = Boolean(channel.param_override?.trim()) return (
@@ -557,13 +561,19 @@ export function useChannelsColumns(): ColumnDef[] { )} - {isMultiKey && ( - + {hasParamOverride && ( + + + + } + > + + {t('Override request parameters')} + + + )}
diff --git a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx index 92302365..6b26cd17 100644 --- a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx +++ b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx @@ -2754,23 +2754,15 @@ export function ChannelMutateDrawer({ - diff --git a/web/default/src/features/dashboard/components/overview/overview-dashboard.tsx b/web/default/src/features/dashboard/components/overview/overview-dashboard.tsx index 7a8e0356..297bdbec 100644 --- a/web/default/src/features/dashboard/components/overview/overview-dashboard.tsx +++ b/web/default/src/features/dashboard/components/overview/overview-dashboard.tsx @@ -26,6 +26,7 @@ import { ChevronDown, ChevronUp, Circle, + Copy, CreditCard, FileText, KeyRound, @@ -38,13 +39,14 @@ import { } from 'lucide-react' import { motion, useReducedMotion } from 'motion/react' import { useTranslation } from 'react-i18next' +import { toast } from 'sonner' import { useAuthStore } from '@/stores/auth-store' import { getUserModels } from '@/lib/api' import { MOTION_TRANSITION } from '@/lib/motion' import { ROLE } from '@/lib/roles' import { cn } from '@/lib/utils' +import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' import { Button } from '@/components/ui/button' -import { CopyButton } from '@/components/copy-button' import { CardStaggerContainer, CardStaggerItem, @@ -104,8 +106,8 @@ interface RequestExample { endpoint: string model: string keyName: string + keyId?: number displayKey: string - curl: string ready: boolean } @@ -179,7 +181,7 @@ function SetupGuideBackdrop(props: { compact?: boolean }) { <>
{props.example.ready ? ( - - {t('Copy')} - + + {isCopying ? t('Loading') : t('Copy')} + ) : (
) @@ -295,6 +302,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { columns.push( { id: 'channel', + accessorFn: (row) => row.channel, header: ({ column }) => ( ), @@ -395,10 +403,11 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { ) }, - meta: { label: t('Channel'), mobileHidden: true }, + meta: { label: t('Channel') }, }, { id: 'user', + accessorFn: (row) => row.username, header: ({ column }) => ( ), @@ -419,7 +428,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { setUserInfoDialogOpen(true) }} > - + [] { ) }, - meta: { label: t('User'), mobileHidden: true }, + meta: { label: t('User') }, } ) } @@ -555,7 +564,9 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { ? log.completion_tokens / useTime : null const timeVariant = getResponseTimeColor(useTime, log.completion_tokens) - const frtVariant = frt ? getFirstResponseTimeColor(frt / 1000) : 'neutral' + const frtVariant = frt + ? getFirstResponseTimeColor(frt / 1000) + : 'neutral' const timingBgMap: Record = { success: @@ -576,7 +587,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { variant={timeVariant as StatusBadgeProps['variant']} size='sm' copyable={false} - className={cn('font-mono', timingBgMap[timeVariant])} + className={cn('rounded-md font-mono', timingBgMap[timeVariant])} /> {log.is_stream && (frt != null && frt > 0 ? ( @@ -586,7 +597,10 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { size='sm' showDot={false} copyable={false} - className={cn('font-mono', timingBgMap[frtVariant])} + className={cn( + 'rounded-md font-mono', + timingBgMap[frtVariant] + )} /> ) : ( [] { size='sm' showDot={false} copyable={false} - className={timingBgMap.neutral} + className={cn('rounded-md font-mono', timingBgMap.neutral)} /> ))} @@ -641,7 +655,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { ) }, - meta: { label: t('Timing'), mobileHidden: true }, + meta: { label: t('Timing') }, }, { @@ -692,7 +706,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { ) }, - meta: { label: 'Tokens', mobileHidden: true }, + meta: { label: 'Tokens' }, }, { @@ -734,11 +748,15 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { } const quotaStr = formatLogQuota(quota) + const quotaDisplay = splitQuotaDisplay(quotaStr) return (
- - {quotaStr} + + {quotaDisplay.prefix && ( + {quotaDisplay.prefix} + )} + {quotaDisplay.amount}
) diff --git a/web/default/src/features/usage-logs/components/columns/drawing-logs-columns.tsx b/web/default/src/features/usage-logs/components/columns/drawing-logs-columns.tsx index 4be90e25..7d1918a9 100644 --- a/web/default/src/features/usage-logs/components/columns/drawing-logs-columns.tsx +++ b/web/default/src/features/usage-logs/components/columns/drawing-logs-columns.tsx @@ -231,7 +231,7 @@ export function useDrawingLogsColumns( ) }, - meta: { label: t('Image'), mobileHidden: true }, + meta: { label: t('Image') }, }, { accessorKey: 'prompt', @@ -268,7 +268,7 @@ export function useDrawingLogsColumns( ) }, - meta: { label: t('Prompt'), mobileHidden: true }, + meta: { label: t('Prompt') }, size: 200, maxSize: 220, }, diff --git a/web/default/src/features/usage-logs/components/columns/task-logs-columns.tsx b/web/default/src/features/usage-logs/components/columns/task-logs-columns.tsx index a7c26a33..ed987f68 100644 --- a/web/default/src/features/usage-logs/components/columns/task-logs-columns.tsx +++ b/web/default/src/features/usage-logs/components/columns/task-logs-columns.tsx @@ -123,6 +123,7 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef[] { if (isAdmin) { columns.push(createChannelColumn({ headerLabel: t('Channel') }), { id: 'user', + accessorFn: (row) => row.username || row.user_id, header: ({ column }) => ( ), @@ -142,7 +143,7 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef[] { setUserInfoDialogOpen(true) }} > - + [] { ) }, - meta: { label: t('User'), mobileHidden: true }, + meta: { label: t('User') }, }) } diff --git a/web/default/src/features/usage-logs/components/model-badge.tsx b/web/default/src/features/usage-logs/components/model-badge.tsx index 1ff031ae..66648c54 100644 --- a/web/default/src/features/usage-logs/components/model-badge.tsx +++ b/web/default/src/features/usage-logs/components/model-badge.tsx @@ -101,12 +101,12 @@ function ModelBadgeContent(props: ModelBadgeProps) { showDot={!provider} autoColor={provider ? undefined : props.modelName} className={cn( - 'border-border/60 bg-muted/30 h-auto min-h-6 gap-1.5 rounded-md border px-2 py-0.5 [font-family:var(--font-body)] break-all whitespace-normal', + 'border-border/60 bg-muted/30 h-6 max-w-full gap-1.5 rounded-md border px-2 [font-family:var(--font-body)]', provider && 'text-foreground', props.className )} > - + {provider && ( )} - {props.modelName} + {props.modelName}
) diff --git a/web/default/src/features/usage-logs/components/usage-logs-mobile-card.tsx b/web/default/src/features/usage-logs/components/usage-logs-mobile-card.tsx new file mode 100644 index 00000000..4a6eb0eb --- /dev/null +++ b/web/default/src/features/usage-logs/components/usage-logs-mobile-card.tsx @@ -0,0 +1,402 @@ +/* +Copyright (C) 2023-2026 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +import { + flexRender, + type Cell, + type Table, +} from '@tanstack/react-table' +import { Database } from 'lucide-react' +import { useTranslation } from 'react-i18next' +import { formatTimestampToDate } from '@/lib/format' +import { cn } from '@/lib/utils' +import { + Empty, + EmptyDescription, + EmptyHeader, + EmptyMedia, + EmptyTitle, +} from '@/components/ui/empty' +import { Skeleton } from '@/components/ui/skeleton' +import { dotColorMap, textColorMap, type StatusVariant } from '@/components/status-badge' +import type { LogCategory } from '../types' +import { LOG_TYPE_ENUM } from '../constants' +import { getLogTypeConfig } from '../lib/utils' + +const logTypeRowTint: Record = { + [LOG_TYPE_ENUM.ERROR]: 'bg-rose-50/40 dark:bg-rose-950/20 border-rose-200/50 dark:border-rose-900/30', + [LOG_TYPE_ENUM.REFUND]: 'bg-blue-50/30 dark:bg-blue-950/15 border-blue-200/50 dark:border-blue-900/30', +} + +interface UsageLogsMobileListProps { + table: Table + isLoading?: boolean + emptyTitle?: string + emptyDescription?: string + logCategory: LogCategory +} + +function UsageLogsMobileSkeleton() { + return ( +
+ {[1, 2, 3].map((i) => ( +
+
+ + +
+
+ {[1, 2, 3, 4, 5, 6].map((j) => ( +
+ + +
+ ))} +
+
+ ))} +
+ ) +} + +function CompactCell({ + cell, + fallback = '-', + className, + primaryOnly = false, +}: { + cell?: Cell + fallback?: string + className?: string + primaryOnly?: boolean +}) { + return ( +
*:not(:first-child)]:hidden [&_.flex-col]:min-w-0', + className + )} + > + {cell ? ( + flexRender(cell.column.columnDef.cell, cell.getContext()) + ) : ( + {fallback} + )} +
+ ) +} + +function SummaryField({ + label, + cell, + className, + valueClassName, + primaryOnly = false, +}: { + label: string + cell?: Cell + className?: string + valueClassName?: string + primaryOnly?: boolean +}) { + if (!cell) return null + + return ( +
+
+ {label} +
+ +
+ ) +} + +function MobileLogTimeStatus({ + createdAt, + type, +}: { + createdAt: unknown + type: unknown +}) { + const { t } = useTranslation() + const timestamp = typeof createdAt === 'number' ? createdAt : undefined + const logType = typeof type === 'number' ? type : undefined + const config = getLogTypeConfig(logType ?? LOG_TYPE_ENUM.UNKNOWN) + const variant = config.color as StatusVariant + + return ( +
+
+ {formatTimestampToDate(timestamp)} +
+
+
+
+ ) +} + +function CommonLogsCard({ + cells, +}: { + cells: Map> +}) { + const { t } = useTranslation() + + const modelCell = cells.get('model_name') + const quotaCell = cells.get('quota') + + return ( +
+
+ + +
+ +
+
+
+ {t('Time')} +
+ +
+ + + + + + +
+
+ ) +} + +function TaskLogsCard({ + cells, +}: { + cells: Map> +}) { + const { t } = useTranslation() + + const taskIdCell = cells.get('task_id') + const statusCell = cells.get('status') + const submitTimeCell = cells.get('submit_time') + + return ( +
+
+ + +
+ +
+ + + +
+
+ ) +} + +function DrawingLogsCard({ + cells, +}: { + cells: Map> +}) { + const { t } = useTranslation() + + const actionCell = cells.get('action') + const codeCell = cells.get('code') + const submitTimeCell = cells.get('submit_time') + + return ( +
+
+ + +
+ +
+ + + + + + + +
+
+ ) +} + +export function UsageLogsMobileList({ + table, + isLoading = false, + emptyTitle, + emptyDescription, + logCategory, +}: UsageLogsMobileListProps) { + const { t } = useTranslation() + + const resolvedEmptyTitle = emptyTitle ?? t('No Logs Found') + const resolvedEmptyDescription = + emptyDescription ?? + t('No usage logs available. Logs will appear here once API calls are made.') + + if (isLoading) { + return + } + + const rows = table.getRowModel().rows + + if (!rows || rows.length === 0) { + return ( +
+ + + + + + {resolvedEmptyTitle} + {resolvedEmptyDescription} + + +
+ ) + } + + return ( +
+ {rows.map((row) => { + const cells = new Map( + row.getVisibleCells().map((cell) => [cell.column.id, cell]) + ) + + const logType = (row.original as Record).type as + | number + | undefined + const tintClass = logType != null ? (logTypeRowTint[logType] ?? '') : '' + + return ( +
+ {logCategory === 'common' && ( + + )} + {logCategory === 'task' && ( + + )} + {logCategory === 'drawing' && ( + + )} +
+ ) + })} +
+ ) +} diff --git a/web/default/src/features/usage-logs/components/usage-logs-table.tsx b/web/default/src/features/usage-logs/components/usage-logs-table.tsx index 7c348500..697b7513 100644 --- a/web/default/src/features/usage-logs/components/usage-logs-table.tsx +++ b/web/default/src/features/usage-logs/components/usage-logs-table.tsx @@ -47,6 +47,7 @@ import { fetchLogsByCategory } from '../lib/utils' import type { LogCategory } from '../types' import { CommonLogsFilterBar } from './common-logs-filter-bar' import { TaskLogsFilterBar } from './task-logs-filter-bar' +import { UsageLogsMobileList } from './usage-logs-mobile-card' const route = getRouteApi('/_authenticated/usage-logs/$section') @@ -190,6 +191,13 @@ export function UsageLogsTable({ logCategory }: UsageLogsTableProps) { '[&_[data-slot=table]]:text-[13px] [&_[data-slot=table]_td]:text-[13px] [&_[data-slot=table]_td_*]:text-[13px] [&_[data-slot=table]_th]:text-[13px] [&_[data-slot=table]_th_*]:text-[13px]' )} tableHeaderClassName='bg-muted/30 sticky top-0 z-10' + mobile={ + + } toolbar={ isCommon ? ( diff --git a/web/default/src/i18n/locales/en.json b/web/default/src/i18n/locales/en.json index 1db3f68f..5e652b55 100644 --- a/web/default/src/i18n/locales/en.json +++ b/web/default/src/i18n/locales/en.json @@ -2778,6 +2778,7 @@ "Override auto-discovered endpoint": "Override auto-discovered endpoint", "Override request headers": "Override request headers", "Override request headers (JSON format)": "Override request headers (JSON format)", + "Override request parameters": "Override request parameters", "Override request parameters (JSON format)": "Override request parameters (JSON format)", "Override request parameters. Cannot override": "Override request parameters. Cannot override", "Override request parameters. Cannot override stream parameter.": "Override request parameters. Cannot override stream parameter.", diff --git a/web/default/src/i18n/locales/fr.json b/web/default/src/i18n/locales/fr.json index 18c8b0b8..9c79b600 100644 --- a/web/default/src/i18n/locales/fr.json +++ b/web/default/src/i18n/locales/fr.json @@ -2778,6 +2778,7 @@ "Override auto-discovered endpoint": "Remplacer le point de terminaison auto-découvert", "Override request headers": "Remplacer les en-têtes de requête", "Override request headers (JSON format)": "Surcharge des en-têtes de requête (format JSON)", + "Override request parameters": "Remplacer les paramètres de requête", "Override request parameters (JSON format)": "Remplacer les paramètres de requête (format JSON)", "Override request parameters. Cannot override": "Remplacer les paramètres de requête. Impossible de remplacer", "Override request parameters. Cannot override stream parameter.": "Remplace les paramètres de requête. Impossible de remplacer le paramètre stream.", diff --git a/web/default/src/i18n/locales/ja.json b/web/default/src/i18n/locales/ja.json index a8eb2383..052a6788 100644 --- a/web/default/src/i18n/locales/ja.json +++ b/web/default/src/i18n/locales/ja.json @@ -2778,6 +2778,7 @@ "Override auto-discovered endpoint": "自動検出されたエンドポイントを上書きする", "Override request headers": "リクエストヘッダーを上書きする", "Override request headers (JSON format)": "リクエストヘッダーのオーバーライド (JSON 形式)", + "Override request parameters": "リクエストパラメータを上書き", "Override request parameters (JSON format)": "リクエストパラメータの上書き (JSON形式)", "Override request parameters. Cannot override": "リクエストパラメーターを上書きします。上書きできません", "Override request parameters. Cannot override stream parameter.": "リクエストパラメータを上書きします。stream パラメータは上書きできません。", diff --git a/web/default/src/i18n/locales/ru.json b/web/default/src/i18n/locales/ru.json index 57e7d529..40e4d8db 100644 --- a/web/default/src/i18n/locales/ru.json +++ b/web/default/src/i18n/locales/ru.json @@ -2778,6 +2778,7 @@ "Override auto-discovered endpoint": "Переопределить автоматически обнаруженную конечную точку", "Override request headers": "Переопределить заголовки запроса", "Override request headers (JSON format)": "Переопределение заголовков запроса (формат JSON)", + "Override request parameters": "Переопределить параметры запроса", "Override request parameters (JSON format)": "Переопределить параметры запроса (формат JSON)", "Override request parameters. Cannot override": "Переопределить параметры запроса. Невозможно переопределить", "Override request parameters. Cannot override stream parameter.": "Переопределяет параметры запроса. Параметр stream переопределить нельзя.", diff --git a/web/default/src/i18n/locales/vi.json b/web/default/src/i18n/locales/vi.json index f829ebd5..a78f4c6a 100644 --- a/web/default/src/i18n/locales/vi.json +++ b/web/default/src/i18n/locales/vi.json @@ -2778,6 +2778,7 @@ "Override auto-discovered endpoint": "Ghi đè điểm cuối tự động phát hiện", "Override request headers": "Ghi đè tiêu đề yêu cầu", "Override request headers (JSON format)": "Ghi đè tiêu đề yêu cầu (định dạng JSON)", + "Override request parameters": "Ghi đè tham số yêu cầu", "Override request parameters (JSON format)": "Ghi đè tham số yêu cầu (định dạng JSON)", "Override request parameters. Cannot override": "Ghi đè tham số yêu cầu. Không thể ghi đè", "Override request parameters. Cannot override stream parameter.": "Ghi đè tham số yêu cầu. Không thể ghi đè tham số stream.", diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json index b930666a..c6ea6ac0 100644 --- a/web/default/src/i18n/locales/zh.json +++ b/web/default/src/i18n/locales/zh.json @@ -2778,6 +2778,7 @@ "Override auto-discovered endpoint": "覆盖自动发现的端点", "Override request headers": "覆盖请求标头", "Override request headers (JSON format)": "覆盖请求头(JSON 格式)", + "Override request parameters": "覆盖请求参数", "Override request parameters (JSON format)": "覆盖请求参数 (JSON 格式)", "Override request parameters. Cannot override": "覆盖请求参数。无法覆盖", "Override request parameters. Cannot override stream parameter.": "覆盖请求参数。无法覆盖 stream 参数。", diff --git a/web/default/src/i18n/static-keys.ts b/web/default/src/i18n/static-keys.ts index 119e6ef4..1ee0eacd 100644 --- a/web/default/src/i18n/static-keys.ts +++ b/web/default/src/i18n/static-keys.ts @@ -315,6 +315,7 @@ export const STATIC_I18N_KEYS = [ 'Regex Replace', 'Return Error', 'Param Override', + 'Override request parameters', // Profile / 2FA 'Backed up', diff --git a/web/default/src/lib/currency.ts b/web/default/src/lib/currency.ts index 555cdbea..f6fe87a3 100644 --- a/web/default/src/lib/currency.ts +++ b/web/default/src/lib/currency.ts @@ -292,7 +292,7 @@ function formatCurrencyValue( maximumFractionDigits: digits, }).format(adjustedValue) - return `${meta.symbol}${decimal}` + return `${meta.symbol} ${decimal}` } /**