Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
a95655a2
authored
Jun 17, 2026
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(performance): implement success rate grading and enhance UI representation
parent
3cc2b1be
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
106 additions
and
673 deletions
+106
-673
web/default/src/features/dashboard/components/models/performance-overview.tsx
+5
-17
web/default/src/features/dashboard/components/overview/performance-health-panel.tsx
+5
-17
web/default/src/features/performance-metrics/lib/format.ts
+64
-0
web/default/src/features/pricing/components/model-details-api.tsx
+0
-104
web/default/src/features/pricing/components/model-details-capabilities.tsx
+0
-181
web/default/src/features/pricing/components/model-details-charts.tsx
+2
-5
web/default/src/features/pricing/components/model-details-modalities.tsx
+0
-160
web/default/src/features/pricing/components/model-details-performance.tsx
+4
-11
web/default/src/features/pricing/components/model-details-quick-stats.tsx
+0
-147
web/default/src/features/pricing/components/model-details-uptime-sparkline.tsx
+7
-18
web/default/src/features/pricing/components/model-details.tsx
+0
-0
web/default/src/features/pricing/components/model-perf-badge.tsx
+2
-6
web/default/src/features/pricing/lib/index.ts
+0
-1
web/default/src/features/pricing/lib/model-metadata.ts
+0
-0
web/default/src/features/pricing/types.ts
+2
-4
web/default/src/features/redemption-codes/components/redemptions-mutate-drawer.tsx
+15
-2
No files found.
web/default/src/features/dashboard/components/models/performance-overview.tsx
View file @
a95655a2
...
@@ -27,6 +27,8 @@ import {
...
@@ -27,6 +27,8 @@ import {
formatLatency
,
formatLatency
,
formatThroughput
,
formatThroughput
,
formatUptimePct
,
formatUptimePct
,
getSuccessRateDotClass
,
getSuccessRateTextClass
,
}
from
'@/features/performance-metrics/lib/format'
}
from
'@/features/performance-metrics/lib/format'
import
type
{
PerfModelSummary
}
from
'@/features/performance-metrics/types'
import
type
{
PerfModelSummary
}
from
'@/features/performance-metrics/types'
...
@@ -79,20 +81,6 @@ function buildPerformanceSummary(rows: PerfModelSummary[]): PerformanceSummary {
...
@@ -79,20 +81,6 @@ function buildPerformanceSummary(rows: PerfModelSummary[]): PerformanceSummary {
}
}
}
}
function
successRateClassName
(
successRate
:
number
):
string
{
if
(
!
Number
.
isFinite
(
successRate
))
return
'text-muted-foreground'
if
(
successRate
>=
99.9
)
return
'text-success'
if
(
successRate
>=
99
)
return
'text-warning'
return
'text-destructive'
}
function
successDotClassName
(
successRate
:
number
):
string
{
if
(
!
Number
.
isFinite
(
successRate
))
return
'bg-muted-foreground'
if
(
successRate
>=
99.9
)
return
'bg-success'
if
(
successRate
>=
99
)
return
'bg-warning'
return
'bg-destructive'
}
export
function
PerformanceOverview
()
{
export
function
PerformanceOverview
()
{
const
{
t
}
=
useTranslation
()
const
{
t
}
=
useTranslation
()
const
metricsQuery
=
useQuery
({
const
metricsQuery
=
useQuery
({
...
@@ -152,7 +140,7 @@ export function PerformanceOverview() {
...
@@ -152,7 +140,7 @@ export function PerformanceOverview() {
icon=
{
HeartPulse
}
icon=
{
HeartPulse
}
label=
{
t
(
'Success rate'
)
}
label=
{
t
(
'Success rate'
)
}
value=
{
formatUptimePct
(
summary
.
successRate
)
}
value=
{
formatUptimePct
(
summary
.
successRate
)
}
valueClassName=
{
successRateClassName
(
summary
.
successRate
)
}
valueClassName=
{
getSuccessRateTextClass
(
summary
.
successRate
)
}
/>
/>
<
InlineMetric
<
InlineMetric
icon=
{
Timer
}
icon=
{
Timer
}
...
@@ -221,14 +209,14 @@ function ModelBadge(props: { model: PerfModelSummary }) {
...
@@ -221,14 +209,14 @@ function ModelBadge(props: { model: PerfModelSummary }) {
<
span
<
span
className=
{
cn
(
className=
{
cn
(
'size-1.5 rounded-full'
,
'size-1.5 rounded-full'
,
successDotClassName
(
model
.
success_rate
)
getSuccessRateDotClass
(
model
.
success_rate
)
)
}
)
}
aria
-
hidden=
'true'
aria
-
hidden=
'true'
/>
/>
<
span
<
span
className=
{
cn
(
className=
{
cn
(
'font-mono text-[11px] font-semibold tabular-nums'
,
'font-mono text-[11px] font-semibold tabular-nums'
,
successRateClassName
(
model
.
success_rate
)
getSuccessRateTextClass
(
model
.
success_rate
)
)
}
)
}
>
>
{
formatUptimePct
(
model
.
success_rate
)
}
{
formatUptimePct
(
model
.
success_rate
)
}
...
...
web/default/src/features/dashboard/components/overview/performance-health-panel.tsx
View file @
a95655a2
...
@@ -27,6 +27,8 @@ import {
...
@@ -27,6 +27,8 @@ import {
formatLatency
,
formatLatency
,
formatThroughput
,
formatThroughput
,
formatUptimePct
,
formatUptimePct
,
getSuccessRateDotClass
,
getSuccessRateTextClass
,
}
from
'@/features/performance-metrics/lib/format'
}
from
'@/features/performance-metrics/lib/format'
import
type
{
PerfModelSummary
}
from
'@/features/performance-metrics/types'
import
type
{
PerfModelSummary
}
from
'@/features/performance-metrics/types'
...
@@ -51,20 +53,6 @@ function simpleAverage(
...
@@ -51,20 +53,6 @@ function simpleAverage(
return
count
>
0
?
total
/
count
:
NaN
return
count
>
0
?
total
/
count
:
NaN
}
}
function
rateTextClass
(
rate
:
number
):
string
{
if
(
!
Number
.
isFinite
(
rate
))
return
'text-muted-foreground'
if
(
rate
>=
99.9
)
return
'text-success'
if
(
rate
>=
99
)
return
'text-warning'
return
'text-destructive'
}
function
rateDotClass
(
rate
:
number
):
string
{
if
(
!
Number
.
isFinite
(
rate
))
return
'bg-muted-foreground'
if
(
rate
>=
99.9
)
return
'bg-success'
if
(
rate
>=
99
)
return
'bg-warning'
return
'bg-destructive'
}
export
function
PerformanceHealthPanel
()
{
export
function
PerformanceHealthPanel
()
{
const
{
t
}
=
useTranslation
()
const
{
t
}
=
useTranslation
()
const
metricsQuery
=
useQuery
({
const
metricsQuery
=
useQuery
({
...
@@ -121,7 +109,7 @@ export function PerformanceHealthPanel() {
...
@@ -121,7 +109,7 @@ export function PerformanceHealthPanel() {
label=
{
t
(
'Success rate'
)
}
label=
{
t
(
'Success rate'
)
}
value=
{
formatUptimePct
(
summary
.
successRate
)
}
value=
{
formatUptimePct
(
summary
.
successRate
)
}
loading=
{
loading
}
loading=
{
loading
}
valueClassName=
{
r
ateTextClass
(
summary
.
successRate
)
}
valueClassName=
{
getSuccessR
ateTextClass
(
summary
.
successRate
)
}
/>
/>
<
MetricCell
<
MetricCell
icon=
{
Timer
}
icon=
{
Timer
}
...
@@ -162,14 +150,14 @@ export function PerformanceHealthPanel() {
...
@@ -162,14 +150,14 @@ export function PerformanceHealthPanel() {
<
span
<
span
className=
{
cn
(
className=
{
cn
(
'size-1.5 rounded-full'
,
'size-1.5 rounded-full'
,
r
ateDotClass
(
model
.
success_rate
)
getSuccessR
ateDotClass
(
model
.
success_rate
)
)
}
)
}
aria
-
hidden=
'true'
aria
-
hidden=
'true'
/>
/>
<
span
<
span
className=
{
cn
(
className=
{
cn
(
'font-mono text-[11px] font-semibold tabular-nums'
,
'font-mono text-[11px] font-semibold tabular-nums'
,
r
ateTextClass
(
model
.
success_rate
)
getSuccessR
ateTextClass
(
model
.
success_rate
)
)
}
)
}
>
>
{
formatUptimePct
(
model
.
success_rate
)
}
{
formatUptimePct
(
model
.
success_rate
)
}
...
...
web/default/src/features/performance-metrics/lib/format.ts
View file @
a95655a2
...
@@ -32,3 +32,67 @@ export function formatUptimePct(pct: number): string {
...
@@ -32,3 +32,67 @@ export function formatUptimePct(pct: number): string {
if
(
!
Number
.
isFinite
(
pct
))
return
'—'
if
(
!
Number
.
isFinite
(
pct
))
return
'—'
return
`
${
pct
.
toFixed
(
2
)}
%`
return
`
${
pct
.
toFixed
(
2
)}
%`
}
}
export
type
SuccessRateLevel
=
|
'excellent'
|
'good'
|
'warning'
|
'critical'
|
'unknown'
const
SUCCESS_RATE_EXCELLENT_MIN
=
100
const
SUCCESS_RATE_GOOD_MIN
=
90
const
SUCCESS_RATE_WARNING_MIN
=
70
/**
* Single source of truth for grading a success rate (0-100).
* - excellent: 100% (full green)
* - good: >= 90% (slightly lighter green)
* - warning: >= 70%
* - critical: below 70%
* - unknown: non-finite values
*/
export
function
getSuccessRateLevel
(
rate
:
number
):
SuccessRateLevel
{
if
(
!
Number
.
isFinite
(
rate
))
return
'unknown'
if
(
rate
>=
SUCCESS_RATE_EXCELLENT_MIN
)
return
'excellent'
if
(
rate
>=
SUCCESS_RATE_GOOD_MIN
)
return
'good'
if
(
rate
>=
SUCCESS_RATE_WARNING_MIN
)
return
'warning'
return
'critical'
}
const
SUCCESS_RATE_TEXT_CLASS
:
Record
<
SuccessRateLevel
,
string
>
=
{
excellent
:
'text-success'
,
good
:
'text-success/70'
,
warning
:
'text-warning'
,
critical
:
'text-destructive'
,
unknown
:
'text-muted-foreground'
,
}
const
SUCCESS_RATE_DOT_CLASS
:
Record
<
SuccessRateLevel
,
string
>
=
{
excellent
:
'bg-success'
,
good
:
'bg-success/60'
,
warning
:
'bg-warning'
,
critical
:
'bg-destructive'
,
unknown
:
'bg-muted-foreground'
,
}
// Hex colors for non-CSS contexts (e.g. chart libraries that need raw values).
const
SUCCESS_RATE_HEX_COLOR
:
Record
<
SuccessRateLevel
,
string
>
=
{
excellent
:
'#10b981'
,
// emerald-500 (full green)
good
:
'#34d399'
,
// emerald-400 (slightly lighter green)
warning
:
'#f59e0b'
,
// amber-500
critical
:
'#ef4444'
,
// red-500
unknown
:
'#9ca3af'
,
// gray-400
}
export
function
getSuccessRateTextClass
(
rate
:
number
):
string
{
return
SUCCESS_RATE_TEXT_CLASS
[
getSuccessRateLevel
(
rate
)]
}
export
function
getSuccessRateDotClass
(
rate
:
number
):
string
{
return
SUCCESS_RATE_DOT_CLASS
[
getSuccessRateLevel
(
rate
)]
}
export
function
getSuccessRateColor
(
rate
:
number
):
string
{
return
SUCCESS_RATE_HEX_COLOR
[
getSuccessRateLevel
(
rate
)]
}
web/default/src/features/pricing/components/model-details-api.tsx
View file @
a95655a2
...
@@ -19,17 +19,14 @@ For commercial licensing, please contact support@quantumnous.com
...
@@ -19,17 +19,14 @@ For commercial licensing, please contact support@quantumnous.com
import
{
useMemo
,
useState
}
from
'react'
import
{
useMemo
,
useState
}
from
'react'
import
{
import
{
ChevronRight
,
ChevronRight
,
ExternalLink
,
Gauge
,
Gauge
,
KeyRound
,
KeyRound
,
ScrollText
,
ScrollText
,
ShieldCheck
,
Sigma
,
Sigma
,
Zap
,
Zap
,
}
from
'lucide-react'
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
useTranslation
}
from
'react-i18next'
import
type
{
BundledLanguage
}
from
'shiki/bundle/web'
import
type
{
BundledLanguage
}
from
'shiki/bundle/web'
import
{
cn
}
from
'@/lib/utils'
import
{
useStatus
}
from
'@/hooks/use-status'
import
{
useStatus
}
from
'@/hooks/use-status'
import
{
Badge
}
from
'@/components/ui/badge'
import
{
Badge
}
from
'@/components/ui/badge'
import
{
Tabs
,
TabsList
,
TabsTrigger
}
from
'@/components/ui/tabs'
import
{
Tabs
,
TabsList
,
TabsTrigger
}
from
'@/components/ui/tabs'
...
@@ -48,7 +45,6 @@ import {
...
@@ -48,7 +45,6 @@ import {
type
SupportedParameter
,
type
SupportedParameter
,
}
from
'../lib/mock-stats'
}
from
'../lib/mock-stats'
import
{
replaceModelInPath
}
from
'../lib/model-helpers'
import
{
replaceModelInPath
}
from
'../lib/model-helpers'
import
{
inferApiInfo
}
from
'../lib/model-metadata'
import
type
{
PricingModel
}
from
'../types'
import
type
{
PricingModel
}
from
'../types'
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
...
@@ -723,106 +719,6 @@ function RateLimitsSection(props: { model: PricingModel }) {
...
@@ -723,106 +719,6 @@ function RateLimitsSection(props: { model: PricingModel }) {
}
}
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Provider info card (vendor / tokenizer / license / privacy)
// ---------------------------------------------------------------------------
//
// Exported separately so the Overview tab can render it alongside capabilities
// and modalities (i.e. "what is this model?" rather than "how do I call it?").
export
function
ModelDetailsProviderInfo
(
props
:
{
model
:
PricingModel
})
{
const
{
t
}
=
useTranslation
()
const
info
=
useMemo
(()
=>
inferApiInfo
(
props
.
model
),
[
props
.
model
])
return
(
<
section
>
<
SectionTitle
icon=
{
ShieldCheck
}
>
{
t
(
'Provider & data privacy'
)
}
</
SectionTitle
>
<
div
className=
'border-border/60 bg-border/60 grid grid-cols-1 gap-px overflow-hidden rounded-lg border sm:grid-cols-2'
>
<
InfoCell
label=
{
t
(
'Provider'
)
}
>
<
div
className=
'flex items-center gap-1.5'
>
<
span
className=
'text-sm font-medium'
>
{
info
.
vendor_label
}
</
span
>
{
info
.
homepage
&&
(
<
a
href=
{
info
.
homepage
}
target=
'_blank'
rel=
'noopener noreferrer'
className=
'text-muted-foreground hover:text-foreground inline-flex items-center gap-0.5 text-[11px]'
>
{
t
(
'Docs'
)
}
<
ExternalLink
className=
'size-3'
/>
</
a
>
)
}
</
div
>
</
InfoCell
>
<
InfoCell
label=
{
t
(
'Tokenizer'
)
}
>
<
div
className=
'flex flex-col gap-0.5'
>
<
code
className=
'font-mono text-xs'
>
{
info
.
tokenizer
}
</
code
>
{
info
.
tokenizer_note
&&
(
<
span
className=
'text-muted-foreground text-[10px]'
>
{
info
.
tokenizer_note
}
</
span
>
)
}
</
div
>
</
InfoCell
>
<
InfoCell
label=
{
t
(
'License'
)
}
>
<
div
className=
'flex flex-col gap-1'
>
<
span
className=
'text-sm'
>
{
info
.
license
}
</
span
>
<
Badge
variant=
'outline'
className=
{
cn
(
'h-4 w-fit px-1.5 text-[9px] font-medium'
,
info
.
license_kind
===
'open'
&&
'border-emerald-500/40 text-emerald-600 dark:text-emerald-400'
,
info
.
license_kind
===
'open-weight'
&&
'border-sky-500/40 text-sky-600 dark:text-sky-400'
,
info
.
license_kind
===
'proprietary'
&&
'border-amber-500/40 text-amber-600 dark:text-amber-400'
)
}
>
{
info
.
license_kind
===
'open'
?
t
(
'Open source'
)
:
info
.
license_kind
===
'open-weight'
?
t
(
'Open weights'
)
:
info
.
license_kind
===
'proprietary'
?
t
(
'Proprietary'
)
:
t
(
'Unknown'
)
}
</
Badge
>
</
div
>
</
InfoCell
>
<
InfoCell
label=
{
t
(
'Data retention'
)
}
>
<
span
className=
'text-sm'
>
{
info
.
data_retention_days
===
0
?
t
(
'Zero retention'
)
:
`${info.data_retention_days} ${t('days')}`
}
</
span
>
<
span
className=
'text-muted-foreground text-[10px]'
>
{
info
.
training_opt_out
?
t
(
'Not used for upstream training by default'
)
:
t
(
'May be used for training by upstream provider'
)
}
</
span
>
</
InfoCell
>
</
div
>
</
section
>
)
}
function
InfoCell
(
props
:
{
label
:
string
;
children
:
React
.
ReactNode
})
{
return
(
<
div
className=
'bg-card flex flex-col gap-1 px-3 py-2.5'
>
<
span
className=
'text-muted-foreground text-[10px] font-medium tracking-wider uppercase'
>
{
props
.
label
}
</
span
>
{
props
.
children
}
</
div
>
)
}
// ---------------------------------------------------------------------------
// Authentication preview
// Authentication preview
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
...
...
web/default/src/features/pricing/components/model-details-capabilities.tsx
deleted
100644 → 0
View file @
3cc2b1be
/*
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 <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import
{
BookOpenCheck
,
Braces
,
Code2
,
Database
,
FileCode
,
Globe
,
type
LucideIcon
,
PanelTopOpen
,
ScanEye
,
Settings2
,
Sparkles
,
Workflow
,
Zap
,
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
cn
}
from
'@/lib/utils'
import
type
{
ModelCapability
}
from
'../types'
type
CapabilityMeta
=
{
icon
:
LucideIcon
labelKey
:
string
descriptionKey
:
string
}
const
CAPABILITY_META
:
Record
<
ModelCapability
,
CapabilityMeta
>
=
{
function_calling
:
{
icon
:
Workflow
,
labelKey
:
'Function calling'
,
descriptionKey
:
'Invoke developer-defined functions with structured arguments'
,
},
streaming
:
{
icon
:
Zap
,
labelKey
:
'Streaming'
,
descriptionKey
:
'Stream tokens incrementally as they are generated'
,
},
vision
:
{
icon
:
ScanEye
,
labelKey
:
'Vision'
,
descriptionKey
:
'Understand image inputs alongside text'
,
},
json_mode
:
{
icon
:
Braces
,
labelKey
:
'JSON mode'
,
descriptionKey
:
'Force a syntactically valid JSON response'
,
},
structured_output
:
{
icon
:
FileCode
,
labelKey
:
'Structured output'
,
descriptionKey
:
'Return data conforming to a JSON schema'
,
},
reasoning
:
{
icon
:
Sparkles
,
labelKey
:
'Reasoning'
,
descriptionKey
:
'Multi-step thinking before final answer'
,
},
tools
:
{
icon
:
Settings2
,
labelKey
:
'Tools'
,
descriptionKey
:
'Use external tools to extend capabilities'
,
},
system_prompt
:
{
icon
:
PanelTopOpen
,
labelKey
:
'System prompt'
,
descriptionKey
:
'Steer behaviour with a system instruction'
,
},
web_search
:
{
icon
:
Globe
,
labelKey
:
'Web search'
,
descriptionKey
:
'Search the public web at inference time'
,
},
code_interpreter
:
{
icon
:
Code2
,
labelKey
:
'Code interpreter'
,
descriptionKey
:
'Execute code in a sandbox during the response'
,
},
caching
:
{
icon
:
Database
,
labelKey
:
'Prompt caching'
,
descriptionKey
:
'Cache repeated prompt prefixes for cheaper, faster reuse'
,
},
embeddings
:
{
icon
:
BookOpenCheck
,
labelKey
:
'Embeddings'
,
descriptionKey
:
'Return vector embeddings for inputs'
,
},
}
/**
* Order capabilities for display. We put the most user-facing capabilities
* first, then the rest. Anything not listed sinks to the bottom in a stable
* order so the layout looks tidy across models.
*/
const
CAPABILITY_ORDER
:
ModelCapability
[]
=
[
'streaming'
,
'function_calling'
,
'tools'
,
'json_mode'
,
'structured_output'
,
'vision'
,
'reasoning'
,
'caching'
,
'system_prompt'
,
'web_search'
,
'code_interpreter'
,
'embeddings'
,
]
function
orderCapabilities
(
capabilities
:
ModelCapability
[]):
ModelCapability
[]
{
const
set
=
new
Set
(
capabilities
)
const
ordered
=
CAPABILITY_ORDER
.
filter
((
c
)
=>
set
.
has
(
c
))
for
(
const
c
of
capabilities
)
{
if
(
!
ordered
.
includes
(
c
))
ordered
.
push
(
c
)
}
return
ordered
}
export
function
ModelDetailsCapabilities
(
props
:
{
capabilities
:
ModelCapability
[]
})
{
const
{
t
}
=
useTranslation
()
const
ordered
=
orderCapabilities
(
props
.
capabilities
)
if
(
ordered
.
length
===
0
)
{
return
(
<
p
className=
'text-muted-foreground text-sm'
>
{
t
(
'No capabilities reported for this model.'
)
}
</
p
>
)
}
return
(
<
div
className=
'grid grid-cols-2 gap-2 @md/details:grid-cols-3 @2xl/details:grid-cols-4'
>
{
ordered
.
map
((
capability
)
=>
{
const
meta
=
CAPABILITY_META
[
capability
]
if
(
!
meta
)
return
null
const
Icon
=
meta
.
icon
return
(
<
div
key=
{
capability
}
className=
{
cn
(
'group flex items-start gap-2 rounded-lg border p-3 transition-colors'
,
'hover:bg-muted/30'
)
}
>
<
span
className=
'bg-muted text-foreground inline-flex size-7 shrink-0 items-center justify-center rounded-md transition-colors group-hover:bg-emerald-100 group-hover:text-emerald-700 dark:group-hover:bg-emerald-500/20 dark:group-hover:text-emerald-300'
>
<
Icon
className=
'size-3.5'
/>
</
span
>
<
div
className=
'min-w-0 flex-1'
>
<
div
className=
'text-foreground truncate text-xs font-semibold'
>
{
t
(
meta
.
labelKey
)
}
</
div
>
<
p
className=
'text-muted-foreground mt-0.5 line-clamp-2 text-[11px] leading-snug'
>
{
t
(
meta
.
descriptionKey
)
}
</
p
>
</
div
>
</
div
>
)
})
}
</
div
>
)
}
web/default/src/features/pricing/components/model-details-charts.tsx
View file @
a95655a2
...
@@ -24,6 +24,7 @@ import { useChartTheme } from '@/lib/use-chart-theme'
...
@@ -24,6 +24,7 @@ import { useChartTheme } from '@/lib/use-chart-theme'
import
{
cn
}
from
'@/lib/utils'
import
{
cn
}
from
'@/lib/utils'
import
{
VCHART_OPTION
}
from
'@/lib/vchart'
import
{
VCHART_OPTION
}
from
'@/lib/vchart'
import
{
useThemeCustomization
}
from
'@/context/theme-customization-provider'
import
{
useThemeCustomization
}
from
'@/context/theme-customization-provider'
import
{
getSuccessRateColor
}
from
'@/features/performance-metrics/lib/format'
import
type
{
LatencyTimePoint
,
UptimeDayPoint
}
from
'../lib/mock-stats'
import
type
{
LatencyTimePoint
,
UptimeDayPoint
}
from
'../lib/mock-stats'
function
formatHourLabel
(
iso
:
string
):
string
{
function
formatHourLabel
(
iso
:
string
):
string
{
...
@@ -229,11 +230,7 @@ export function UptimeTrendChart(props: {
...
@@ -229,11 +230,7 @@ export function UptimeTrendChart(props: {
size
:
5
,
size
:
5
,
stroke
:
'#ffffff'
,
stroke
:
'#ffffff'
,
lineWidth
:
1.5
,
lineWidth
:
1.5
,
fill
:
(
datum
:
{
uptime
:
number
})
=>
{
fill
:
(
datum
:
{
uptime
:
number
})
=>
getSuccessRateColor
(
datum
.
uptime
),
if
(
datum
.
uptime
>=
99.9
)
return
'#10b981'
if
(
datum
.
uptime
>=
99.0
)
return
'#f59e0b'
return
'#ef4444'
},
},
},
},
},
tooltip
:
{
tooltip
:
{
...
...
web/default/src/features/pricing/components/model-details-modalities.tsx
deleted
100644 → 0
View file @
3cc2b1be
/*
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 <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import
{
FileText
,
Image
as
ImageIcon
,
Mic2
,
Type
as
TypeIcon
,
Video
,
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
cn
}
from
'@/lib/utils'
import
{
Tooltip
,
TooltipContent
,
TooltipTrigger
,
}
from
'@/components/ui/tooltip'
import
{
StaticDataTable
}
from
'@/components/data-table'
import
type
{
Modality
}
from
'../types'
type
IconComponent
=
React
.
ComponentType
<
{
className
?:
string
}
>
const
MODALITY_META
:
Record
<
Modality
,
{
icon
:
IconComponent
;
labelKey
:
string
}
>
=
{
text
:
{
icon
:
TypeIcon
,
labelKey
:
'Text'
},
image
:
{
icon
:
ImageIcon
,
labelKey
:
'Image'
},
audio
:
{
icon
:
Mic2
,
labelKey
:
'Audio'
},
video
:
{
icon
:
Video
,
labelKey
:
'Video'
},
file
:
{
icon
:
FileText
,
labelKey
:
'File'
},
}
const
ALL_MODALITIES
:
Modality
[]
=
[
'text'
,
'image'
,
'audio'
,
'video'
,
'file'
]
/** Inline modality icons (used by the quick-stats flow). */
export
function
ModalityIcons
(
props
:
{
modalities
:
Modality
[]
className
?:
string
})
{
const
{
t
}
=
useTranslation
()
if
(
props
.
modalities
.
length
===
0
)
{
return
<
span
className=
'text-muted-foreground text-xs'
>
—
</
span
>
}
return
(
<
span
className=
'inline-flex items-center gap-1'
>
{
props
.
modalities
.
map
((
modality
)
=>
{
const
meta
=
MODALITY_META
[
modality
]
const
Icon
=
meta
.
icon
return
(
<
Tooltip
key=
{
modality
}
>
<
TooltipTrigger
render=
{
<
span
aria
-
label=
{
t
(
meta
.
labelKey
)
}
className=
'text-foreground/80 inline-flex'
/>
}
>
<
Icon
className=
{
cn
(
'size-3.5'
,
props
.
className
)
}
/>
</
TooltipTrigger
>
<
TooltipContent
side=
'top'
className=
'text-xs'
>
{
t
(
meta
.
labelKey
)
}
</
TooltipContent
>
</
Tooltip
>
)
})
}
</
span
>
)
}
/**
* 2 × N matrix showing which modalities are supported as input vs output.
* Cells with a checkmark indicate support; empty cells show a dash.
*/
export
function
ModalitiesMatrix
(
props
:
{
input
:
Modality
[]
output
:
Modality
[]
})
{
const
{
t
}
=
useTranslation
()
const
inputSet
=
new
Set
(
props
.
input
)
const
outputSet
=
new
Set
(
props
.
output
)
return
(
<
StaticDataTable
className=
'rounded-lg'
tableClassName=
'text-sm'
headerRowClassName=
'bg-muted/40'
data=
{
[
{
label
:
t
(
'Input'
),
set
:
inputSet
},
{
label
:
t
(
'Output'
),
set
:
outputSet
},
]
}
getRowKey=
{
(
row
)
=>
row
.
label
}
columns=
{
[
{
id
:
'modality'
,
header
:
t
(
'Modality'
),
className
:
'text-muted-foreground px-3 py-2 text-left text-[11px] font-medium tracking-wider uppercase'
,
cellClassName
:
'text-muted-foreground bg-muted/30 px-3 py-2 text-left text-[11px] font-medium tracking-wider uppercase'
,
cell
:
(
row
)
=>
row
.
label
,
},
...
ALL_MODALITIES
.
map
((
modality
)
=>
({
id
:
modality
,
header
:
t
(
MODALITY_META
[
modality
].
labelKey
),
className
:
'text-muted-foreground border-l px-3 py-2 text-center text-[11px] font-medium tracking-wider uppercase'
,
cellClassName
:
(
row
:
{
label
:
string
;
set
:
Set
<
Modality
>
})
=>
cn
(
'border-l px-3 py-2 text-center'
,
row
.
set
.
has
(
modality
)
?
'bg-emerald-50/40 dark:bg-emerald-500/10'
:
'bg-background'
),
cell
:
(
row
:
{
label
:
string
;
set
:
Set
<
Modality
>
})
=>
{
const
enabled
=
row
.
set
.
has
(
modality
)
const
Icon
=
MODALITY_META
[
modality
].
icon
return
(
<
span
className=
{
cn
(
'inline-flex items-center justify-center'
,
enabled
?
'text-emerald-700 dark:text-emerald-300'
:
'text-muted-foreground/40'
)
}
aria
-
label=
{
enabled
?
t
(
'
{{
modality
}}
supported
'
,
{
modality
:
t
(
MODALITY_META
[
modality
].
labelKey
),
})
:
t
(
'
{{
modality
}}
not
supported
'
,
{
modality
:
t
(
MODALITY_META
[
modality
].
labelKey
),
})
}
>
<
Icon
className=
'size-4'
/>
</
span
>
)
},
})),
]
}
/>
)
}
web/default/src/features/pricing/components/model-details-performance.tsx
View file @
a95655a2
...
@@ -31,6 +31,7 @@ import {
...
@@ -31,6 +31,7 @@ import {
formatLatency
,
formatLatency
,
formatThroughput
,
formatThroughput
,
formatUptimePct
,
formatUptimePct
,
getSuccessRateTextClass
,
}
from
'@/features/performance-metrics/lib/format'
}
from
'@/features/performance-metrics/lib/format'
import
type
{
PerformanceGroup
}
from
'@/features/performance-metrics/types'
import
type
{
PerformanceGroup
}
from
'@/features/performance-metrics/types'
import
{
type
UptimeDayPoint
}
from
'../lib/mock-stats'
import
{
type
UptimeDayPoint
}
from
'../lib/mock-stats'
...
@@ -43,10 +44,9 @@ function StatCard(props: {
...
@@ -43,10 +44,9 @@ function StatCard(props: {
label
:
string
label
:
string
value
:
React
.
ReactNode
value
:
React
.
ReactNode
hint
?:
string
hint
?:
string
intent
?:
'default'
|
'warning'
|
'success'
valueClassName
?:
string
})
{
})
{
const
Icon
=
props
.
icon
const
Icon
=
props
.
icon
const
intent
=
props
.
intent
??
'default'
return
(
return
(
<
div
className=
'bg-background flex flex-col gap-1 rounded-lg border p-3'
>
<
div
className=
'bg-background flex flex-col gap-1 rounded-lg border p-3'
>
<
span
className=
'text-muted-foreground inline-flex items-center gap-1.5 text-[10px] font-medium tracking-wider uppercase'
>
<
span
className=
'text-muted-foreground inline-flex items-center gap-1.5 text-[10px] font-medium tracking-wider uppercase'
>
...
@@ -56,8 +56,7 @@ function StatCard(props: {
...
@@ -56,8 +56,7 @@ function StatCard(props: {
<
span
<
span
className=
{
cn
(
className=
{
cn
(
'text-foreground font-mono text-lg font-semibold tabular-nums'
,
'text-foreground font-mono text-lg font-semibold tabular-nums'
,
intent
===
'warning'
&&
'text-amber-600 dark:text-amber-400'
,
props
.
valueClassName
intent
===
'success'
&&
'text-emerald-600 dark:text-emerald-400'
)
}
)
}
>
>
{
props
.
value
}
{
props
.
value
}
...
@@ -217,12 +216,6 @@ export function ModelDetailsPerformance(props: { model: PricingModel }) {
...
@@ -217,12 +216,6 @@ export function ModelDetailsPerformance(props: { model: PricingModel }) {
successRates
.
length
successRates
.
length
:
0
:
0
const
incidentCount
=
uptimeSeries
.
reduce
((
s
,
p
)
=>
s
+
p
.
incidents
,
0
)
const
incidentCount
=
uptimeSeries
.
reduce
((
s
,
p
)
=>
s
+
p
.
incidents
,
0
)
let
intent
:
'default'
|
'warning'
|
'success'
=
'warning'
if
(
successRate
>=
99.9
)
{
intent
=
'success'
}
else
if
(
successRate
>=
99
)
{
intent
=
'default'
}
return
(
return
(
<
div
className=
'flex flex-col gap-4'
>
<
div
className=
'flex flex-col gap-4'
>
...
@@ -249,7 +242,7 @@ export function ModelDetailsPerformance(props: { model: PricingModel }) {
...
@@ -249,7 +242,7 @@ export function ModelDetailsPerformance(props: { model: PricingModel }) {
})
})
:
t
(
'No incidents in the last 24 hours'
)
:
t
(
'No incidents in the last 24 hours'
)
}
}
intent=
{
intent
}
valueClassName=
{
getSuccessRateTextClass
(
successRate
)
}
/>
/>
</
div
>
</
div
>
...
...
web/default/src/features/pricing/components/model-details-quick-stats.tsx
deleted
100644 → 0
View file @
3cc2b1be
/*
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 <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import
{
CalendarClock
,
FileText
,
Layers
,
Maximize2
,
Sparkles
,
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
cn
}
from
'@/lib/utils'
import
{
formatTokenCount
,
formatYearMonth
,
type
ModelMetadata
,
}
from
'../lib/model-metadata'
import
type
{
Modality
}
from
'../types'
import
{
ModalityIcons
}
from
'./model-details-modalities'
type
QuickStatsProps
=
{
metadata
:
ModelMetadata
}
type
Stat
=
{
key
:
string
icon
:
React
.
ComponentType
<
{
className
?:
string
}
>
label
:
string
value
:
React
.
ReactNode
hint
?:
string
}
function
buildStats
(
metadata
:
ModelMetadata
,
t
:
(
key
:
string
)
=>
string
):
Stat
[]
{
const
stats
:
Stat
[]
=
[
{
key
:
'context'
,
icon
:
Layers
,
label
:
t
(
'Context'
),
value
:
formatTokenCount
(
metadata
.
context_length
),
hint
:
t
(
'Maximum input window'
),
},
]
if
(
metadata
.
max_output_tokens
>
0
)
{
stats
.
push
({
key
:
'max-output'
,
icon
:
Maximize2
,
label
:
t
(
'Max output'
),
value
:
formatTokenCount
(
metadata
.
max_output_tokens
),
hint
:
t
(
'Maximum tokens per response'
),
})
}
stats
.
push
({
key
:
'modalities'
,
icon
:
FileText
,
label
:
t
(
'Modalities'
),
value
:
(
<
ModalityFlow
input=
{
metadata
.
input_modalities
}
output=
{
metadata
.
output_modalities
}
/>
),
})
if
(
metadata
.
knowledge_cutoff
)
{
stats
.
push
({
key
:
'knowledge'
,
icon
:
Sparkles
,
label
:
t
(
'Knowledge cutoff'
),
value
:
formatYearMonth
(
metadata
.
knowledge_cutoff
),
})
}
if
(
metadata
.
release_date
)
{
stats
.
push
({
key
:
'release'
,
icon
:
CalendarClock
,
label
:
t
(
'Released'
),
value
:
formatYearMonth
(
metadata
.
release_date
),
})
}
return
stats
}
function
ModalityFlow
(
props
:
{
input
:
Modality
[];
output
:
Modality
[]
})
{
return
(
<
span
className=
'inline-flex items-center gap-1 align-middle'
>
<
ModalityIcons
modalities=
{
props
.
input
}
className=
'size-3.5'
/>
<
span
className=
'text-muted-foreground/40'
>
→
</
span
>
<
ModalityIcons
modalities=
{
props
.
output
}
className=
'size-3.5'
/>
</
span
>
)
}
export
function
ModelDetailsQuickStats
(
props
:
QuickStatsProps
)
{
const
{
t
}
=
useTranslation
()
const
stats
=
buildStats
(
props
.
metadata
,
t
)
return
(
<
div
className=
'bg-muted/20 grid grid-cols-2 gap-px overflow-hidden rounded-lg border @md/details:grid-cols-3 @2xl/details:grid-cols-5'
>
{
stats
.
map
((
stat
)
=>
{
const
Icon
=
stat
.
icon
return
(
<
div
key=
{
stat
.
key
}
className=
{
cn
(
'bg-background flex min-w-0 flex-col gap-0.5 px-3 py-2.5'
)
}
>
<
span
className=
'text-muted-foreground inline-flex min-w-0 items-center gap-1 text-[10px] font-medium tracking-wider uppercase'
>
<
Icon
className=
'size-3 shrink-0'
/>
<
span
className=
'truncate'
>
{
stat
.
label
}
</
span
>
</
span
>
<
span
className=
'text-foreground truncate text-sm font-semibold tabular-nums'
>
{
stat
.
value
}
</
span
>
{
stat
.
hint
&&
(
<
span
className=
'text-muted-foreground/60 truncate text-[10px]'
>
{
stat
.
hint
}
</
span
>
)
}
</
div
>
)
})
}
</
div
>
)
}
web/default/src/features/pricing/components/model-details-uptime-sparkline.tsx
View file @
a95655a2
...
@@ -25,7 +25,11 @@ import {
...
@@ -25,7 +25,11 @@ import {
TooltipContent
,
TooltipContent
,
TooltipTrigger
,
TooltipTrigger
,
}
from
'@/components/ui/tooltip'
}
from
'@/components/ui/tooltip'
import
{
formatUptimePct
}
from
'@/features/performance-metrics/lib/format'
import
{
formatUptimePct
,
getSuccessRateDotClass
,
getSuccessRateTextClass
,
}
from
'@/features/performance-metrics/lib/format'
import
{
aggregateUptime
,
type
UptimeDayPoint
}
from
'../lib/mock-stats'
import
{
aggregateUptime
,
type
UptimeDayPoint
}
from
'../lib/mock-stats'
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
...
@@ -50,14 +54,6 @@ type UptimeSparklineProps = {
...
@@ -50,14 +54,6 @@ type UptimeSparklineProps = {
className
?:
string
className
?:
string
}
}
function
colourFor
(
uptime
:
number
):
string
{
if
(
uptime
>=
99.9
)
return
'bg-emerald-500'
if
(
uptime
>=
99.0
)
return
'bg-emerald-400'
if
(
uptime
>=
95.0
)
return
'bg-amber-500'
if
(
uptime
>=
90.0
)
return
'bg-amber-600'
return
'bg-rose-500'
}
function
heightFor
(
uptime
:
number
):
string
{
function
heightFor
(
uptime
:
number
):
string
{
if
(
uptime
>=
99.9
)
return
'h-full'
if
(
uptime
>=
99.9
)
return
'h-full'
if
(
uptime
>=
99.0
)
return
'h-[88%]'
if
(
uptime
>=
99.0
)
return
'h-[88%]'
...
@@ -66,13 +62,6 @@ function heightFor(uptime: number): string {
...
@@ -66,13 +62,6 @@ function heightFor(uptime: number): string {
return
'h-[40%]'
return
'h-[40%]'
}
}
function
overallTextColour
(
pct
:
number
):
string
{
if
(
pct
>=
99.9
)
return
'text-emerald-600 dark:text-emerald-400'
if
(
pct
>=
99.0
)
return
'text-emerald-600 dark:text-emerald-400'
if
(
pct
>=
95.0
)
return
'text-amber-600 dark:text-amber-400'
return
'text-rose-600 dark:text-rose-400'
}
export
function
UptimeSparkline
(
props
:
UptimeSparklineProps
)
{
export
function
UptimeSparkline
(
props
:
UptimeSparklineProps
)
{
const
size
=
props
.
size
??
'md'
const
size
=
props
.
size
??
'md'
const
showOverall
=
props
.
showOverall
??
true
const
showOverall
=
props
.
showOverall
??
true
...
@@ -116,7 +105,7 @@ export function UptimeSparkline(props: UptimeSparklineProps) {
...
@@ -116,7 +105,7 @@ export function UptimeSparkline(props: UptimeSparklineProps) {
<
div
<
div
className=
{
cn
(
className=
{
cn
(
'w-full rounded-sm'
,
'w-full rounded-sm'
,
colourFor
(
day
.
uptime_pct
),
getSuccessRateDotClass
(
day
.
uptime_pct
),
heightFor
(
day
.
uptime_pct
)
heightFor
(
day
.
uptime_pct
)
)
}
)
}
aria
-
hidden
aria
-
hidden
...
@@ -138,7 +127,7 @@ export function UptimeSparkline(props: UptimeSparklineProps) {
...
@@ -138,7 +127,7 @@ export function UptimeSparkline(props: UptimeSparklineProps) {
<
span
<
span
className=
{
cn
(
className=
{
cn
(
'font-mono text-sm font-semibold tabular-nums'
,
'font-mono text-sm font-semibold tabular-nums'
,
overallTextColour
(
overall
)
getSuccessRateTextClass
(
overall
)
)
}
)
}
>
>
{
overall
.
toFixed
(
1
)
}
%
{
overall
.
toFixed
(
1
)
}
%
...
...
web/default/src/features/pricing/components/model-details.tsx
View file @
a95655a2
This diff is collapsed.
Click to expand it.
web/default/src/features/pricing/components/model-perf-badge.tsx
View file @
a95655a2
...
@@ -22,6 +22,7 @@ import { cn } from '@/lib/utils'
...
@@ -22,6 +22,7 @@ import { cn } from '@/lib/utils'
import
{
import
{
formatLatency
,
formatLatency
,
formatThroughput
,
formatThroughput
,
getSuccessRateDotClass
,
}
from
'@/features/performance-metrics/lib/format'
}
from
'@/features/performance-metrics/lib/format'
export
type
ModelPerfBadgeData
=
{
export
type
ModelPerfBadgeData
=
{
...
@@ -49,12 +50,7 @@ export const ModelPerfBadge = memo(function ModelPerfBadge(
...
@@ -49,12 +50,7 @@ export const ModelPerfBadge = memo(function ModelPerfBadge(
const
{
avg_latency_ms
,
avg_tps
,
success_rate
}
=
props
.
perf
const
{
avg_latency_ms
,
avg_tps
,
success_rate
}
=
props
.
perf
let
statusColor
=
'bg-emerald-500'
const
statusColor
=
getSuccessRateDotClass
(
success_rate
)
if
(
success_rate
<
99
)
{
statusColor
=
'bg-red-500'
}
else
if
(
success_rate
<
99.9
)
{
statusColor
=
'bg-amber-500'
}
return
(
return
(
<
div
<
div
...
...
web/default/src/features/pricing/lib/index.ts
View file @
a95655a2
...
@@ -25,6 +25,5 @@ export * from './price'
...
@@ -25,6 +25,5 @@ export * from './price'
export
*
from
'./model-helpers'
export
*
from
'./model-helpers'
export
*
from
'./billing-expr'
export
*
from
'./billing-expr'
export
*
from
'./tier-expr'
export
*
from
'./tier-expr'
export
*
from
'./model-metadata'
export
*
from
'./mock-stats'
export
*
from
'./mock-stats'
export
*
from
'./seed'
export
*
from
'./seed'
web/default/src/features/pricing/lib/model-metadata.ts
deleted
100644 → 0
View file @
3cc2b1be
This diff is collapsed.
Click to expand it.
web/default/src/features/pricing/types.ts
View file @
a95655a2
...
@@ -57,10 +57,8 @@ export type PricingModel = {
...
@@ -57,10 +57,8 @@ export type PricingModel = {
/** Pricing version returned by backend, useful for cache busting */
/** Pricing version returned by backend, useful for cache busting */
pricing_version
?:
string
pricing_version
?:
string
/**
/**
* Optional model metadata fields. These are not yet returned by the backend
* Optional model metadata fields reserved for backend-provided catalog data.
* and are populated client-side from {@link inferModelMetadata}.
* Keep them data-driven; do not synthesize display values on the client.
* When the backend ships these fields, the inference layer becomes a
* fallback rather than the source of truth.
*/
*/
context_length
?:
number
context_length
?:
number
max_output_tokens
?:
number
max_output_tokens
?:
number
...
...
web/default/src/features/redemption-codes/components/redemptions-mutate-drawer.tsx
View file @
a95655a2
...
@@ -16,12 +16,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
...
@@ -16,12 +16,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
For commercial licensing, please contact support@quantumnous.com
*/
*/
import
{
useEffect
,
useState
}
from
'react'
import
{
type
FormEvent
,
useEffect
,
useState
}
from
'react'
import
{
useForm
}
from
'react-hook-form'
import
{
useForm
}
from
'react-hook-form'
import
{
zodResolver
}
from
'@hookform/resolvers/zod'
import
{
zodResolver
}
from
'@hookform/resolvers/zod'
import
{
useTranslation
}
from
'react-i18next'
import
{
useTranslation
}
from
'react-i18next'
import
{
toast
}
from
'sonner'
import
{
toast
}
from
'sonner'
import
{
getCurrencyDisplay
,
getCurrencyLabel
}
from
'@/lib/currency'
import
{
getCurrencyDisplay
,
getCurrencyLabel
}
from
'@/lib/currency'
import
{
formatQuota
,
parseQuotaFromDollars
}
from
'@/lib/format'
import
{
addTimeToDate
}
from
'@/lib/time'
import
{
addTimeToDate
}
from
'@/lib/time'
import
{
Button
}
from
'@/components/ui/button'
import
{
Button
}
from
'@/components/ui/button'
import
{
import
{
...
@@ -135,6 +136,18 @@ export function RedemptionsMutateDrawer({
...
@@ -135,6 +136,18 @@ export function RedemptionsMutateDrawer({
}
}
}
}
const
handleSubmit
=
(
event
:
FormEvent
<
HTMLFormElement
>
)
=>
{
if
(
!
isUpdate
)
{
const
name
=
form
.
getValues
(
'name'
)
if
(
!
name
?.
trim
())
{
const
quota
=
parseQuotaFromDollars
(
form
.
getValues
(
'quota_dollars'
))
form
.
setValue
(
'name'
,
formatQuota
(
quota
),
{
shouldValidate
:
true
})
}
}
void
form
.
handleSubmit
(
onSubmit
)(
event
)
}
const
handleSetExpiry
=
(
months
:
number
,
days
:
number
,
hours
:
number
)
=>
{
const
handleSetExpiry
=
(
months
:
number
,
days
:
number
,
hours
:
number
)
=>
{
const
newDate
=
addTimeToDate
(
months
,
days
,
hours
)
const
newDate
=
addTimeToDate
(
months
,
days
,
hours
)
form
.
setValue
(
'expired_time'
,
newDate
)
form
.
setValue
(
'expired_time'
,
newDate
)
...
@@ -177,7 +190,7 @@ export function RedemptionsMutateDrawer({
...
@@ -177,7 +190,7 @@ export function RedemptionsMutateDrawer({
<
Form
{
...
form
}
>
<
Form
{
...
form
}
>
<
form
<
form
id=
'redemption-form'
id=
'redemption-form'
onSubmit=
{
form
.
handleSubmit
(
onSubmit
)
}
onSubmit=
{
handleSubmit
}
className=
{
sideDrawerFormClassName
()
}
className=
{
sideDrawerFormClassName
()
}
>
>
<
SideDrawerSection
>
<
SideDrawerSection
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment