fix(default): resolve v1 frontend issue regressions
Fix v1 frontend regressions across channel forms, dashboard charts, wallet history, payment callbacks, invite links, API key groups, rate-limit errors, and usage-log scrolling. Fixes #4715 Fixes #4618 Fixes #4699 Fixes #4651 Fixes #4637 Fixes #4682 Fixes #4691 Fixes #4565 Fixes #4334
This commit is contained in:
parent
5fa103fa5b
commit
ba474393fb
@ -401,9 +401,6 @@ func GetChannelById(id int, selectAll bool) (*Channel, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if channel == nil {
|
|
||||||
return nil, errors.New("channel not found")
|
|
||||||
}
|
|
||||||
return channel, nil
|
return channel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -758,7 +755,7 @@ func EditChannelByTag(tag string, newTag *string, modelMapping *string, models *
|
|||||||
updateData.Tag = newTag
|
updateData.Tag = newTag
|
||||||
updatedTag = *newTag
|
updatedTag = *newTag
|
||||||
}
|
}
|
||||||
if modelMapping != nil && *modelMapping != "" {
|
if modelMapping != nil {
|
||||||
updateData.ModelMapping = modelMapping
|
updateData.ModelMapping = modelMapping
|
||||||
}
|
}
|
||||||
if models != nil && *models != "" {
|
if models != nil && *models != "" {
|
||||||
|
|||||||
45
web/default/src/components/ui/combobox-input.tsx
vendored
45
web/default/src/components/ui/combobox-input.tsx
vendored
@ -36,6 +36,7 @@ interface ComboboxInputProps {
|
|||||||
emptyText?: string
|
emptyText?: string
|
||||||
className?: string
|
className?: string
|
||||||
id?: string
|
id?: string
|
||||||
|
allowCustomValue?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ComboboxInput({
|
export function ComboboxInput({
|
||||||
@ -46,23 +47,30 @@ export function ComboboxInput({
|
|||||||
emptyText = 'No option found.',
|
emptyText = 'No option found.',
|
||||||
className,
|
className,
|
||||||
id,
|
id,
|
||||||
|
allowCustomValue = false,
|
||||||
}: ComboboxInputProps) {
|
}: ComboboxInputProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [open, setOpen] = React.useState(false)
|
const [open, setOpen] = React.useState(false)
|
||||||
|
const [searchValue, setSearchValue] = React.useState('')
|
||||||
const [highlightedIndex, setHighlightedIndex] = React.useState(-1)
|
const [highlightedIndex, setHighlightedIndex] = React.useState(-1)
|
||||||
const containerRef = React.useRef<HTMLDivElement>(null)
|
const containerRef = React.useRef<HTMLDivElement>(null)
|
||||||
const inputRef = React.useRef<HTMLInputElement>(null)
|
const inputRef = React.useRef<HTMLInputElement>(null)
|
||||||
const listRef = React.useRef<HTMLUListElement>(null)
|
const listRef = React.useRef<HTMLUListElement>(null)
|
||||||
|
const selectedOption = React.useMemo(
|
||||||
|
() => options.find((option) => option.value === value),
|
||||||
|
[options, value]
|
||||||
|
)
|
||||||
|
const displayValue = open ? searchValue : (selectedOption?.label ?? value)
|
||||||
|
|
||||||
const filteredOptions = React.useMemo(() => {
|
const filteredOptions = React.useMemo(() => {
|
||||||
if (!value.trim()) return options
|
if (!searchValue.trim()) return options
|
||||||
const search = value.toLowerCase().trim()
|
const search = searchValue.toLowerCase().trim()
|
||||||
return options.filter(
|
return options.filter(
|
||||||
(option) =>
|
(option) =>
|
||||||
option.label.toLowerCase().includes(search) ||
|
option.label.toLowerCase().includes(search) ||
|
||||||
option.value.toLowerCase().includes(search)
|
option.value.toLowerCase().includes(search)
|
||||||
)
|
)
|
||||||
}, [options, value])
|
}, [options, searchValue])
|
||||||
|
|
||||||
// Reset highlight when filtered options change
|
// Reset highlight when filtered options change
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -79,6 +87,7 @@ export function ComboboxInput({
|
|||||||
!containerRef.current.contains(e.target as Node)
|
!containerRef.current.contains(e.target as Node)
|
||||||
) {
|
) {
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
|
setSearchValue('')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,6 +98,7 @@ export function ComboboxInput({
|
|||||||
const handleSelect = (selectedValue: string) => {
|
const handleSelect = (selectedValue: string) => {
|
||||||
onValueChange(selectedValue)
|
onValueChange(selectedValue)
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
|
setSearchValue('')
|
||||||
inputRef.current?.focus()
|
inputRef.current?.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,14 +127,18 @@ export function ComboboxInput({
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (highlightedIndex >= 0 && filteredOptions[highlightedIndex]) {
|
if (highlightedIndex >= 0 && filteredOptions[highlightedIndex]) {
|
||||||
handleSelect(filteredOptions[highlightedIndex].value)
|
handleSelect(filteredOptions[highlightedIndex].value)
|
||||||
|
} else if (allowCustomValue && searchValue.trim()) {
|
||||||
|
handleSelect(searchValue.trim())
|
||||||
} else {
|
} else {
|
||||||
// No highlighted option, just close the dropdown and keep current value
|
// No highlighted option, just close the dropdown and keep current value
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
|
setSearchValue('')
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 'Escape':
|
case 'Escape':
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
|
setSearchValue('')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -136,7 +150,9 @@ export function ComboboxInput({
|
|||||||
item?.scrollIntoView({ block: 'nearest' })
|
item?.scrollIntoView({ block: 'nearest' })
|
||||||
}, [highlightedIndex])
|
}, [highlightedIndex])
|
||||||
|
|
||||||
const showDropdown = open && (filteredOptions.length > 0 || value.trim())
|
const showDropdown =
|
||||||
|
open &&
|
||||||
|
(filteredOptions.length > 0 || (allowCustomValue && searchValue.trim()))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className='relative'>
|
<div ref={containerRef} className='relative'>
|
||||||
@ -150,12 +166,19 @@ export function ComboboxInput({
|
|||||||
aria-autocomplete='list'
|
aria-autocomplete='list'
|
||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={value}
|
value={displayValue}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
onValueChange(e.target.value)
|
const nextValue = e.target.value
|
||||||
|
setSearchValue(nextValue)
|
||||||
|
if (allowCustomValue) {
|
||||||
|
onValueChange(nextValue)
|
||||||
|
}
|
||||||
if (!open) setOpen(true)
|
if (!open) setOpen(true)
|
||||||
}}
|
}}
|
||||||
onFocus={() => setOpen(true)}
|
onFocus={() => {
|
||||||
|
setSearchValue(allowCustomValue ? value : '')
|
||||||
|
setOpen(true)
|
||||||
|
}}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
className={cn('pr-9', className)}
|
className={cn('pr-9', className)}
|
||||||
/>
|
/>
|
||||||
@ -200,10 +223,12 @@ export function ComboboxInput({
|
|||||||
</ul>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
<div className='px-2 py-6 text-center text-sm'>
|
<div className='px-2 py-6 text-center text-sm'>
|
||||||
{emptyText}
|
{t(emptyText)}
|
||||||
{value.trim() && (
|
{allowCustomValue && searchValue.trim() && (
|
||||||
<div className='text-muted-foreground mt-1 text-xs'>
|
<div className='text-muted-foreground mt-1 text-xs'>
|
||||||
{t('Press Enter to use "{{value}}"', { value: value.trim() })}
|
{t('Press Enter to use "{{value}}"', {
|
||||||
|
value: searchValue.trim(),
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
1
web/default/src/components/ui/combobox.tsx
vendored
1
web/default/src/components/ui/combobox.tsx
vendored
@ -68,6 +68,7 @@ function Combobox(
|
|||||||
placeholder={props.searchPlaceholder ?? props.placeholder}
|
placeholder={props.searchPlaceholder ?? props.placeholder}
|
||||||
emptyText={props.emptyText}
|
emptyText={props.emptyText}
|
||||||
className={props.className}
|
className={props.className}
|
||||||
|
allowCustomValue={props.allowCustomValue}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -484,6 +484,12 @@ export function transformFormDataToUpdatePayload(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Send explicit empty strings for nullable JSON/text fields so GORM updates can clear them.
|
||||||
|
payload.model_mapping = formData.model_mapping || ''
|
||||||
|
payload.status_code_mapping = formData.status_code_mapping || ''
|
||||||
|
payload.param_override = formData.param_override || ''
|
||||||
|
payload.header_override = formData.header_override || ''
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -143,6 +143,12 @@ function replaceToken(source: string, token: string, value: string) {
|
|||||||
return source.split(token).join(value)
|
return source.split(token).join(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeApiKey(apiKey: string): string {
|
||||||
|
const trimmed = apiKey.trim()
|
||||||
|
if (!trimmed) return ''
|
||||||
|
return trimmed.startsWith('sk-') ? trimmed : `sk-${trimmed}`
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveChatUrl({
|
export function resolveChatUrl({
|
||||||
template,
|
template,
|
||||||
apiKey,
|
apiKey,
|
||||||
@ -151,7 +157,7 @@ export function resolveChatUrl({
|
|||||||
let url = template
|
let url = template
|
||||||
const safeServerAddress = serverAddress || ''
|
const safeServerAddress = serverAddress || ''
|
||||||
|
|
||||||
const safeApiKey = apiKey || ''
|
const safeApiKey = normalizeApiKey(apiKey || '')
|
||||||
|
|
||||||
if (url.includes('{cherryConfig}')) {
|
if (url.includes('{cherryConfig}')) {
|
||||||
const payload = {
|
const payload = {
|
||||||
|
|||||||
@ -115,6 +115,15 @@ export function ConsumptionDistributionChart(
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
const spec = chartType === 'bar' ? chartData.spec_line : chartData.spec_area
|
const spec = chartType === 'bar' ? chartData.spec_line : chartData.spec_area
|
||||||
|
const specType = typeof spec?.type === 'string' ? spec.type : chartType
|
||||||
|
const chartKey = [
|
||||||
|
chartType,
|
||||||
|
specType,
|
||||||
|
props.loading ? 'loading' : 'ready',
|
||||||
|
props.data.length,
|
||||||
|
resolvedTheme,
|
||||||
|
customization.preset,
|
||||||
|
].join('-')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='overflow-hidden rounded-lg border'>
|
<div className='overflow-hidden rounded-lg border'>
|
||||||
@ -152,7 +161,7 @@ export function ConsumptionDistributionChart(
|
|||||||
<div className='h-[300px] p-1.5 sm:h-96 sm:p-2'>
|
<div className='h-[300px] p-1.5 sm:h-96 sm:p-2'>
|
||||||
{themeReady && spec && (
|
{themeReady && spec && (
|
||||||
<VChart
|
<VChart
|
||||||
key={`${chartType}-${resolvedTheme}-${customization.preset}`}
|
key={chartKey}
|
||||||
spec={{
|
spec={{
|
||||||
...spec,
|
...spec,
|
||||||
theme: resolvedTheme === 'dark' ? 'dark' : 'light',
|
theme: resolvedTheme === 'dark' ? 'dark' : 'light',
|
||||||
|
|||||||
@ -114,6 +114,15 @@ export function ModelCharts(props: ModelChartsProps) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const spec = chartData[CHART_SPEC_KEYS[activeTab]]
|
const spec = chartData[CHART_SPEC_KEYS[activeTab]]
|
||||||
|
const specType = typeof spec?.type === 'string' ? spec.type : activeTab
|
||||||
|
const chartKey = [
|
||||||
|
activeTab,
|
||||||
|
specType,
|
||||||
|
props.loading ? 'loading' : 'ready',
|
||||||
|
props.data.length,
|
||||||
|
resolvedTheme,
|
||||||
|
customization.preset,
|
||||||
|
].join('-')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='overflow-hidden rounded-lg border'>
|
<div className='overflow-hidden rounded-lg border'>
|
||||||
@ -149,7 +158,7 @@ export function ModelCharts(props: ModelChartsProps) {
|
|||||||
<div className='h-[300px] p-1.5 sm:h-96 sm:p-2'>
|
<div className='h-[300px] p-1.5 sm:h-96 sm:p-2'>
|
||||||
{themeReady && spec && (
|
{themeReady && spec && (
|
||||||
<VChart
|
<VChart
|
||||||
key={`${activeTab}-${resolvedTheme}-${customization.preset}`}
|
key={chartKey}
|
||||||
spec={{
|
spec={{
|
||||||
...spec,
|
...spec,
|
||||||
theme: resolvedTheme === 'dark' ? 'dark' : 'light',
|
theme: resolvedTheme === 'dark' ? 'dark' : 'light',
|
||||||
|
|||||||
@ -212,7 +212,7 @@ export function processChartData(
|
|||||||
legends: { visible: true, selectMode: 'single' },
|
legends: { visible: true, selectMode: 'single' },
|
||||||
},
|
},
|
||||||
spec_model_line: {
|
spec_model_line: {
|
||||||
type: 'line',
|
type: 'area',
|
||||||
data: [{ id: 'lineData', values: [] }],
|
data: [{ id: 'lineData', values: [] }],
|
||||||
xField: 'Time',
|
xField: 'Time',
|
||||||
yField: 'Count',
|
yField: 'Count',
|
||||||
|
|||||||
@ -25,27 +25,45 @@ const FEEDBACK_URL = 'https://github.com/QuantumNous/new-api/issues'
|
|||||||
|
|
||||||
type GeneralErrorProps = React.HTMLAttributes<HTMLDivElement> & {
|
type GeneralErrorProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||||
minimal?: boolean
|
minimal?: boolean
|
||||||
|
error?: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHttpStatus(error: unknown): number | undefined {
|
||||||
|
if (typeof error !== 'object' || error === null) return undefined
|
||||||
|
const response = (error as Record<string, unknown>).response
|
||||||
|
if (typeof response !== 'object' || response === null) return undefined
|
||||||
|
const status = (response as Record<string, unknown>).status
|
||||||
|
return typeof status === 'number' ? status : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GeneralError({
|
export function GeneralError({
|
||||||
className,
|
className,
|
||||||
minimal = false,
|
minimal = false,
|
||||||
|
error,
|
||||||
}: GeneralErrorProps) {
|
}: GeneralErrorProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { history } = useRouter()
|
const { history } = useRouter()
|
||||||
|
const status = getHttpStatus(error)
|
||||||
|
const isRateLimited = status === 429
|
||||||
|
const title = isRateLimited
|
||||||
|
? t('Too many requests')
|
||||||
|
: `${t('Oops! Something went wrong')} ${`:')`}`
|
||||||
|
const description = isRateLimited
|
||||||
|
? t('Please wait a moment before trying again.')
|
||||||
|
: t('Please try again later.')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('h-svh w-full', className)}>
|
<div className={cn('h-svh w-full', className)}>
|
||||||
<div className='m-auto flex h-full w-full flex-col items-center justify-center gap-2'>
|
<div className='m-auto flex h-full w-full flex-col items-center justify-center gap-2'>
|
||||||
{!minimal && (
|
{!minimal && (
|
||||||
<h1 className='text-[7rem] leading-tight font-bold'>500</h1>
|
<h1 className='text-[7rem] leading-tight font-bold'>
|
||||||
|
{status ?? 500}
|
||||||
|
</h1>
|
||||||
)}
|
)}
|
||||||
<span className='font-medium'>
|
<span className='font-medium'>{title}</span>
|
||||||
{t('Oops! Something went wrong')} {`:')`}
|
|
||||||
</span>
|
|
||||||
<p className='text-muted-foreground text-center'>
|
<p className='text-muted-foreground text-center'>
|
||||||
{t('We apologize for the inconvenience.')} <br />{' '}
|
{t('We apologize for the inconvenience.')} <br /> {description}
|
||||||
{t('Please try again later.')}
|
|
||||||
</p>
|
</p>
|
||||||
{!minimal && (
|
{!minimal && (
|
||||||
<p className='text-muted-foreground text-center text-sm'>
|
<p className='text-muted-foreground text-center text-sm'>
|
||||||
|
|||||||
@ -145,7 +145,7 @@ export function ApiKeyGroupCombobox({
|
|||||||
<span className='flex min-w-0 flex-1 items-center justify-between gap-2 sm:gap-3'>
|
<span className='flex min-w-0 flex-1 items-center justify-between gap-2 sm:gap-3'>
|
||||||
<span className='min-w-0'>
|
<span className='min-w-0'>
|
||||||
<span className='block truncate font-medium'>
|
<span className='block truncate font-medium'>
|
||||||
{selectedOption?.value || placeholder || t('Select a group')}
|
{selectedOption?.label || placeholder || t('Select a group')}
|
||||||
</span>
|
</span>
|
||||||
{selectedOption?.desc && (
|
{selectedOption?.desc && (
|
||||||
<span className='text-muted-foreground block truncate text-[11px] sm:text-xs'>
|
<span className='text-muted-foreground block truncate text-[11px] sm:text-xs'>
|
||||||
@ -178,7 +178,7 @@ export function ApiKeyGroupCombobox({
|
|||||||
<CommandItem
|
<CommandItem
|
||||||
key={option.value}
|
key={option.value}
|
||||||
value={option.value}
|
value={option.value}
|
||||||
onSelect={handleSelect}
|
onSelect={() => handleSelect(option.value)}
|
||||||
className='data-[selected=true]:bg-muted items-start gap-3 rounded-lg px-3 py-3 transition-colors'
|
className='data-[selected=true]:bg-muted items-start gap-3 rounded-lg px-3 py-3 transition-colors'
|
||||||
>
|
>
|
||||||
<Check
|
<Check
|
||||||
@ -189,7 +189,7 @@ export function ApiKeyGroupCombobox({
|
|||||||
/>
|
/>
|
||||||
<span className='min-w-0 flex-1'>
|
<span className='min-w-0 flex-1'>
|
||||||
<span className='block truncate font-medium'>
|
<span className='block truncate font-medium'>
|
||||||
{option.value}
|
{option.label}
|
||||||
</span>
|
</span>
|
||||||
{option.desc && (
|
{option.desc && (
|
||||||
<span className='text-muted-foreground block truncate text-xs'>
|
<span className='text-muted-foreground block truncate text-xs'>
|
||||||
|
|||||||
@ -126,7 +126,7 @@ export function RateLimitSection({ defaultValues }: RateLimitSectionProps) {
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t(
|
{t(
|
||||||
'Restrict user model request frequency (may impact high concurrency performance)'
|
'This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.'
|
||||||
)}
|
)}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -171,6 +171,7 @@ export function UsageLogsTable({ logCategory }: UsageLogsTableProps) {
|
|||||||
'No usage logs available. Logs will appear here once API calls are made.'
|
'No usage logs available. Logs will appear here once API calls are made.'
|
||||||
)}
|
)}
|
||||||
skeletonKeyPrefix='usage-log-skeleton'
|
skeletonKeyPrefix='usage-log-skeleton'
|
||||||
|
tableClassName='max-h-[calc(100dvh-13rem)] overflow-auto sm:max-h-[calc(100dvh-14rem)]'
|
||||||
tableHeaderClassName='bg-muted/30 sticky top-0 z-10'
|
tableHeaderClassName='bg-muted/30 sticky top-0 z-10'
|
||||||
toolbar={
|
toolbar={
|
||||||
isCommon ? (
|
isCommon ? (
|
||||||
|
|||||||
@ -176,8 +176,8 @@ export function BillingHistoryDialog({
|
|||||||
</p>
|
</p>
|
||||||
<p className='mt-1 text-xs'>
|
<p className='mt-1 text-xs'>
|
||||||
{keyword
|
{keyword
|
||||||
? 'Try adjusting your search'
|
? t('Try adjusting your search')
|
||||||
: 'Your transaction history will appear here'}
|
: t('Your transaction history will appear here')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -233,15 +233,15 @@ export function BillingHistoryDialog({
|
|||||||
<div className='mt-3 grid grid-cols-2 gap-3 sm:mt-4 sm:grid-cols-3 sm:gap-4'>
|
<div className='mt-3 grid grid-cols-2 gap-3 sm:mt-4 sm:grid-cols-3 sm:gap-4'>
|
||||||
<div className='space-y-1'>
|
<div className='space-y-1'>
|
||||||
<Label className='text-muted-foreground text-xs'>
|
<Label className='text-muted-foreground text-xs'>
|
||||||
Payment Method
|
{t('Payment Method')}
|
||||||
</Label>
|
</Label>
|
||||||
<div className='text-sm font-medium'>
|
<div className='text-sm font-medium'>
|
||||||
{getPaymentMethodName(record.payment_method)}
|
{getPaymentMethodName(record.payment_method, t)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='space-y-1'>
|
<div className='space-y-1'>
|
||||||
<Label className='text-muted-foreground text-xs'>
|
<Label className='text-muted-foreground text-xs'>
|
||||||
Amount
|
{t('Amount')}
|
||||||
</Label>
|
</Label>
|
||||||
<div className='text-sm font-semibold'>
|
<div className='text-sm font-semibold'>
|
||||||
{formatCurrencyFromUSD(record.amount, {
|
{formatCurrencyFromUSD(record.amount, {
|
||||||
@ -253,7 +253,7 @@ export function BillingHistoryDialog({
|
|||||||
</div>
|
</div>
|
||||||
<div className='space-y-1'>
|
<div className='space-y-1'>
|
||||||
<Label className='text-muted-foreground text-xs'>
|
<Label className='text-muted-foreground text-xs'>
|
||||||
Payment
|
{t('Payment')}
|
||||||
</Label>
|
</Label>
|
||||||
<div className='text-sm font-semibold text-red-600'>
|
<div className='text-sm font-semibold text-red-600'>
|
||||||
{formatNumber(record.money)}
|
{formatNumber(record.money)}
|
||||||
@ -270,7 +270,7 @@ export function BillingHistoryDialog({
|
|||||||
onClick={() => setConfirmTradeNo(record.trade_no)}
|
onClick={() => setConfirmTradeNo(record.trade_no)}
|
||||||
disabled={completing}
|
disabled={completing}
|
||||||
>
|
>
|
||||||
Complete Order
|
{t('Complete Order')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -341,7 +341,7 @@ export function BillingHistoryDialog({
|
|||||||
onClick={handleConfirmComplete}
|
onClick={handleConfirmComplete}
|
||||||
disabled={completing}
|
disabled={completing}
|
||||||
>
|
>
|
||||||
{completing ? 'Processing...' : 'Confirm'}
|
{completing ? t('Processing...') : t('Confirm')}
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
|
|||||||
@ -67,8 +67,12 @@ export const PAYMENT_METHOD_NAMES: Record<string, string> = {
|
|||||||
/**
|
/**
|
||||||
* Get payment method display name
|
* Get payment method display name
|
||||||
*/
|
*/
|
||||||
export function getPaymentMethodName(method: string): string {
|
export function getPaymentMethodName(
|
||||||
return PAYMENT_METHOD_NAMES[method] || method
|
method: string,
|
||||||
|
t?: (key: string) => string
|
||||||
|
): string {
|
||||||
|
const name = PAYMENT_METHOD_NAMES[method] || method
|
||||||
|
return t ? t(name) : name
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -17,13 +17,13 @@
|
|||||||
"file": "ja.json",
|
"file": "ja.json",
|
||||||
"missingCount": 0,
|
"missingCount": 0,
|
||||||
"extrasCount": 0,
|
"extrasCount": 0,
|
||||||
"untranslatedCount": 90
|
"untranslatedCount": 92
|
||||||
},
|
},
|
||||||
"ru": {
|
"ru": {
|
||||||
"file": "ru.json",
|
"file": "ru.json",
|
||||||
"missingCount": 0,
|
"missingCount": 0,
|
||||||
"extrasCount": 0,
|
"extrasCount": 0,
|
||||||
"untranslatedCount": 105
|
"untranslatedCount": 107
|
||||||
},
|
},
|
||||||
"vi": {
|
"vi": {
|
||||||
"file": "vi.json",
|
"file": "vi.json",
|
||||||
|
|||||||
@ -88,5 +88,7 @@
|
|||||||
"Webhook URL:": "Webhook URL:",
|
"Webhook URL:": "Webhook URL:",
|
||||||
"whsec_xxx": "whsec_xxx",
|
"whsec_xxx": "whsec_xxx",
|
||||||
"Xinference": "Xinference",
|
"Xinference": "Xinference",
|
||||||
"Xunfei": "Xunfei"
|
"Xunfei": "Xunfei",
|
||||||
|
"Alipay": "Alipay",
|
||||||
|
"WeChat Pay": "WeChat Pay"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -103,5 +103,7 @@
|
|||||||
"whsec_xxx": "whsec_xxx",
|
"whsec_xxx": "whsec_xxx",
|
||||||
"Xinference": "Xinference",
|
"Xinference": "Xinference",
|
||||||
"Xunfei": "Xunfei",
|
"Xunfei": "Xunfei",
|
||||||
|
"Alipay": "Alipay",
|
||||||
|
"WeChat Pay": "WeChat Pay",
|
||||||
"Zhipu V4": "Zhipu V4"
|
"Zhipu V4": "Zhipu V4"
|
||||||
}
|
}
|
||||||
|
|||||||
7
web/default/src/i18n/locales/en.json
vendored
7
web/default/src/i18n/locales/en.json
vendored
@ -4400,6 +4400,13 @@
|
|||||||
"Your Telegram Bot Token": "Your Telegram Bot Token",
|
"Your Telegram Bot Token": "Your Telegram Bot Token",
|
||||||
"Your Turnstile secret key": "Your Turnstile secret key",
|
"Your Turnstile secret key": "Your Turnstile secret key",
|
||||||
"Your Turnstile site key": "Your Turnstile site key",
|
"Your Turnstile site key": "Your Turnstile site key",
|
||||||
|
"Alipay": "Alipay",
|
||||||
|
"Please wait a moment before trying again.": "Please wait a moment before trying again.",
|
||||||
|
"This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.": "This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.",
|
||||||
|
"Too many requests": "Too many requests",
|
||||||
|
"Try adjusting your search": "Try adjusting your search",
|
||||||
|
"WeChat Pay": "WeChat Pay",
|
||||||
|
"Your transaction history will appear here": "Your transaction history will appear here",
|
||||||
"Zero retention": "Zero retention",
|
"Zero retention": "Zero retention",
|
||||||
"Zhipu": "Zhipu",
|
"Zhipu": "Zhipu",
|
||||||
"Zhipu V4": "Zhipu V4",
|
"Zhipu V4": "Zhipu V4",
|
||||||
|
|||||||
7
web/default/src/i18n/locales/fr.json
vendored
7
web/default/src/i18n/locales/fr.json
vendored
@ -4400,6 +4400,13 @@
|
|||||||
"Your Telegram Bot Token": "Votre Jeton de Bot Telegram",
|
"Your Telegram Bot Token": "Votre Jeton de Bot Telegram",
|
||||||
"Your Turnstile secret key": "Votre clé secrète Turnstile",
|
"Your Turnstile secret key": "Votre clé secrète Turnstile",
|
||||||
"Your Turnstile site key": "Votre clé de site Turnstile",
|
"Your Turnstile site key": "Votre clé de site Turnstile",
|
||||||
|
"Alipay": "Alipay",
|
||||||
|
"Please wait a moment before trying again.": "Veuillez patienter un instant avant de réessayer.",
|
||||||
|
"This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.": "Ce réglage contrôle la limitation des requêtes de modèles. La limitation des routes Web/API se configure via les variables d'environnement et peut encore renvoyer 429.",
|
||||||
|
"Too many requests": "Trop de requêtes",
|
||||||
|
"Try adjusting your search": "Essayez d'ajuster votre recherche",
|
||||||
|
"WeChat Pay": "WeChat Pay",
|
||||||
|
"Your transaction history will appear here": "Votre historique de transactions apparaîtra ici",
|
||||||
"Zero retention": "Aucune rétention",
|
"Zero retention": "Aucune rétention",
|
||||||
"Zhipu": "Zhipu",
|
"Zhipu": "Zhipu",
|
||||||
"Zhipu V4": "Zhipu V4",
|
"Zhipu V4": "Zhipu V4",
|
||||||
|
|||||||
7
web/default/src/i18n/locales/ja.json
vendored
7
web/default/src/i18n/locales/ja.json
vendored
@ -4400,6 +4400,13 @@
|
|||||||
"Your Telegram Bot Token": "あなたのTelegramボットトークン",
|
"Your Telegram Bot Token": "あなたのTelegramボットトークン",
|
||||||
"Your Turnstile secret key": "あなたのTurnstileシークレットキー",
|
"Your Turnstile secret key": "あなたのTurnstileシークレットキー",
|
||||||
"Your Turnstile site key": "あなたのTurnstileサイトキー",
|
"Your Turnstile site key": "あなたのTurnstileサイトキー",
|
||||||
|
"Alipay": "Alipay",
|
||||||
|
"Please wait a moment before trying again.": "しばらく待ってからもう一度お試しください。",
|
||||||
|
"This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.": "これはモデルリクエストのレート制限を制御します。Web/API ルートのスロットリングは環境変数で設定され、引き続き 429 を返す場合があります。",
|
||||||
|
"Too many requests": "リクエストが多すぎます",
|
||||||
|
"Try adjusting your search": "検索条件を調整してみてください",
|
||||||
|
"WeChat Pay": "WeChat Pay",
|
||||||
|
"Your transaction history will appear here": "取引履歴はここに表示されます",
|
||||||
"Zero retention": "データ保持なし",
|
"Zero retention": "データ保持なし",
|
||||||
"Zhipu": "Zhipu",
|
"Zhipu": "Zhipu",
|
||||||
"Zhipu V4": "Zhipu V 4",
|
"Zhipu V4": "Zhipu V 4",
|
||||||
|
|||||||
7
web/default/src/i18n/locales/ru.json
vendored
7
web/default/src/i18n/locales/ru.json
vendored
@ -4400,6 +4400,13 @@
|
|||||||
"Your Telegram Bot Token": "Ваш токен Telegram-бота",
|
"Your Telegram Bot Token": "Ваш токен Telegram-бота",
|
||||||
"Your Turnstile secret key": "Секретный ключ Turnstile",
|
"Your Turnstile secret key": "Секретный ключ Turnstile",
|
||||||
"Your Turnstile site key": "Ключ сайта Turnstile",
|
"Your Turnstile site key": "Ключ сайта Turnstile",
|
||||||
|
"Alipay": "Alipay",
|
||||||
|
"Please wait a moment before trying again.": "Пожалуйста, подождите немного и попробуйте снова.",
|
||||||
|
"This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.": "Этот параметр управляет ограничением частоты запросов к моделям. Ограничение маршрутов Web/API настраивается переменными окружения и всё ещё может возвращать 429.",
|
||||||
|
"Too many requests": "Слишком много запросов",
|
||||||
|
"Try adjusting your search": "Попробуйте изменить условия поиска",
|
||||||
|
"WeChat Pay": "WeChat Pay",
|
||||||
|
"Your transaction history will appear here": "Ваша история транзакций появится здесь",
|
||||||
"Zero retention": "Без хранения данных",
|
"Zero retention": "Без хранения данных",
|
||||||
"Zhipu": "Zhipu",
|
"Zhipu": "Zhipu",
|
||||||
"Zhipu V4": "Zhipu V4",
|
"Zhipu V4": "Zhipu V4",
|
||||||
|
|||||||
7
web/default/src/i18n/locales/vi.json
vendored
7
web/default/src/i18n/locales/vi.json
vendored
@ -4400,6 +4400,13 @@
|
|||||||
"Your Telegram Bot Token": "Mã thông báo bot Telegram của bạn",
|
"Your Telegram Bot Token": "Mã thông báo bot Telegram của bạn",
|
||||||
"Your Turnstile secret key": "Khóa bí mật Turnstile của bạn",
|
"Your Turnstile secret key": "Khóa bí mật Turnstile của bạn",
|
||||||
"Your Turnstile site key": "Khóa site Turnstile của bạn",
|
"Your Turnstile site key": "Khóa site Turnstile của bạn",
|
||||||
|
"Alipay": "Alipay",
|
||||||
|
"Please wait a moment before trying again.": "Vui lòng chờ một lát rồi thử lại.",
|
||||||
|
"This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.": "Thiết lập này kiểm soát giới hạn tốc độ yêu cầu mô hình. Giới hạn tuyến Web/API được cấu hình bằng biến môi trường và vẫn có thể trả về 429.",
|
||||||
|
"Too many requests": "Quá nhiều yêu cầu",
|
||||||
|
"Try adjusting your search": "Hãy thử điều chỉnh tìm kiếm",
|
||||||
|
"WeChat Pay": "WeChat Pay",
|
||||||
|
"Your transaction history will appear here": "Lịch sử giao dịch của bạn sẽ xuất hiện ở đây",
|
||||||
"Zero retention": "Không lưu dữ liệu",
|
"Zero retention": "Không lưu dữ liệu",
|
||||||
"Zhipu": "Zhipu",
|
"Zhipu": "Zhipu",
|
||||||
"Zhipu V4": "Zhipu V4",
|
"Zhipu V4": "Zhipu V4",
|
||||||
|
|||||||
7
web/default/src/i18n/locales/zh.json
vendored
7
web/default/src/i18n/locales/zh.json
vendored
@ -4400,6 +4400,13 @@
|
|||||||
"Your Telegram Bot Token": "您的 Telegram 机器人令牌",
|
"Your Telegram Bot Token": "您的 Telegram 机器人令牌",
|
||||||
"Your Turnstile secret key": "您的 Turnstile 密钥",
|
"Your Turnstile secret key": "您的 Turnstile 密钥",
|
||||||
"Your Turnstile site key": "您的 Turnstile 站点密钥",
|
"Your Turnstile site key": "您的 Turnstile 站点密钥",
|
||||||
|
"Alipay": "支付宝",
|
||||||
|
"Please wait a moment before trying again.": "请稍候再试。",
|
||||||
|
"This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.": "此处仅控制模型请求速率限制。Web/API 路由限流由环境变量配置,仍可能返回 429。",
|
||||||
|
"Too many requests": "请求过于频繁",
|
||||||
|
"Try adjusting your search": "请尝试调整搜索条件",
|
||||||
|
"WeChat Pay": "微信支付",
|
||||||
|
"Your transaction history will appear here": "您的交易历史会显示在这里",
|
||||||
"Zero retention": "零数据保留",
|
"Zero retention": "零数据保留",
|
||||||
"Zhipu": "智谱",
|
"Zhipu": "智谱",
|
||||||
"Zhipu V4": "智谱 V4",
|
"Zhipu V4": "智谱 V4",
|
||||||
|
|||||||
42
web/default/src/routeTree.gen.ts
vendored
42
web/default/src/routeTree.gen.ts
vendored
@ -19,6 +19,8 @@ import { Route as RankingsIndexRouteImport } from './routes/rankings/index'
|
|||||||
import { Route as PricingIndexRouteImport } from './routes/pricing/index'
|
import { Route as PricingIndexRouteImport } from './routes/pricing/index'
|
||||||
import { Route as AboutIndexRouteImport } from './routes/about/index'
|
import { Route as AboutIndexRouteImport } from './routes/about/index'
|
||||||
import { Route as OauthProviderRouteImport } from './routes/oauth/$provider'
|
import { Route as OauthProviderRouteImport } from './routes/oauth/$provider'
|
||||||
|
import { Route as ConsoleTopupRouteImport } from './routes/console/topup'
|
||||||
|
import { Route as ConsoleLogRouteImport } from './routes/console/log'
|
||||||
import { Route as AuthenticatedChat2linkRouteImport } from './routes/_authenticated/chat2link'
|
import { Route as AuthenticatedChat2linkRouteImport } from './routes/_authenticated/chat2link'
|
||||||
import { Route as errors503RouteImport } from './routes/(errors)/503'
|
import { Route as errors503RouteImport } from './routes/(errors)/503'
|
||||||
import { Route as errors500RouteImport } from './routes/(errors)/500'
|
import { Route as errors500RouteImport } from './routes/(errors)/500'
|
||||||
@ -114,6 +116,16 @@ const OauthProviderRoute = OauthProviderRouteImport.update({
|
|||||||
path: '/oauth/$provider',
|
path: '/oauth/$provider',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const ConsoleTopupRoute = ConsoleTopupRouteImport.update({
|
||||||
|
id: '/console/topup',
|
||||||
|
path: '/console/topup',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const ConsoleLogRoute = ConsoleLogRouteImport.update({
|
||||||
|
id: '/console/log',
|
||||||
|
path: '/console/log',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const AuthenticatedChat2linkRoute = AuthenticatedChat2linkRouteImport.update({
|
const AuthenticatedChat2linkRoute = AuthenticatedChat2linkRouteImport.update({
|
||||||
id: '/chat2link',
|
id: '/chat2link',
|
||||||
path: '/chat2link',
|
path: '/chat2link',
|
||||||
@ -391,6 +403,8 @@ export interface FileRoutesByFullPath {
|
|||||||
'/500': typeof errors500Route
|
'/500': typeof errors500Route
|
||||||
'/503': typeof errors503Route
|
'/503': typeof errors503Route
|
||||||
'/chat2link': typeof AuthenticatedChat2linkRoute
|
'/chat2link': typeof AuthenticatedChat2linkRoute
|
||||||
|
'/console/log': typeof ConsoleLogRoute
|
||||||
|
'/console/topup': typeof ConsoleTopupRoute
|
||||||
'/oauth/$provider': typeof OauthProviderRoute
|
'/oauth/$provider': typeof OauthProviderRoute
|
||||||
'/about/': typeof AboutIndexRoute
|
'/about/': typeof AboutIndexRoute
|
||||||
'/pricing/': typeof PricingIndexRoute
|
'/pricing/': typeof PricingIndexRoute
|
||||||
@ -446,6 +460,8 @@ export interface FileRoutesByTo {
|
|||||||
'/500': typeof errors500Route
|
'/500': typeof errors500Route
|
||||||
'/503': typeof errors503Route
|
'/503': typeof errors503Route
|
||||||
'/chat2link': typeof AuthenticatedChat2linkRoute
|
'/chat2link': typeof AuthenticatedChat2linkRoute
|
||||||
|
'/console/log': typeof ConsoleLogRoute
|
||||||
|
'/console/topup': typeof ConsoleTopupRoute
|
||||||
'/oauth/$provider': typeof OauthProviderRoute
|
'/oauth/$provider': typeof OauthProviderRoute
|
||||||
'/about': typeof AboutIndexRoute
|
'/about': typeof AboutIndexRoute
|
||||||
'/pricing': typeof PricingIndexRoute
|
'/pricing': typeof PricingIndexRoute
|
||||||
@ -505,6 +521,8 @@ export interface FileRoutesById {
|
|||||||
'/(errors)/500': typeof errors500Route
|
'/(errors)/500': typeof errors500Route
|
||||||
'/(errors)/503': typeof errors503Route
|
'/(errors)/503': typeof errors503Route
|
||||||
'/_authenticated/chat2link': typeof AuthenticatedChat2linkRoute
|
'/_authenticated/chat2link': typeof AuthenticatedChat2linkRoute
|
||||||
|
'/console/log': typeof ConsoleLogRoute
|
||||||
|
'/console/topup': typeof ConsoleTopupRoute
|
||||||
'/oauth/$provider': typeof OauthProviderRoute
|
'/oauth/$provider': typeof OauthProviderRoute
|
||||||
'/about/': typeof AboutIndexRoute
|
'/about/': typeof AboutIndexRoute
|
||||||
'/pricing/': typeof PricingIndexRoute
|
'/pricing/': typeof PricingIndexRoute
|
||||||
@ -563,6 +581,8 @@ export interface FileRouteTypes {
|
|||||||
| '/500'
|
| '/500'
|
||||||
| '/503'
|
| '/503'
|
||||||
| '/chat2link'
|
| '/chat2link'
|
||||||
|
| '/console/log'
|
||||||
|
| '/console/topup'
|
||||||
| '/oauth/$provider'
|
| '/oauth/$provider'
|
||||||
| '/about/'
|
| '/about/'
|
||||||
| '/pricing/'
|
| '/pricing/'
|
||||||
@ -618,6 +638,8 @@ export interface FileRouteTypes {
|
|||||||
| '/500'
|
| '/500'
|
||||||
| '/503'
|
| '/503'
|
||||||
| '/chat2link'
|
| '/chat2link'
|
||||||
|
| '/console/log'
|
||||||
|
| '/console/topup'
|
||||||
| '/oauth/$provider'
|
| '/oauth/$provider'
|
||||||
| '/about'
|
| '/about'
|
||||||
| '/pricing'
|
| '/pricing'
|
||||||
@ -676,6 +698,8 @@ export interface FileRouteTypes {
|
|||||||
| '/(errors)/500'
|
| '/(errors)/500'
|
||||||
| '/(errors)/503'
|
| '/(errors)/503'
|
||||||
| '/_authenticated/chat2link'
|
| '/_authenticated/chat2link'
|
||||||
|
| '/console/log'
|
||||||
|
| '/console/topup'
|
||||||
| '/oauth/$provider'
|
| '/oauth/$provider'
|
||||||
| '/about/'
|
| '/about/'
|
||||||
| '/pricing/'
|
| '/pricing/'
|
||||||
@ -727,6 +751,8 @@ export interface RootRouteChildren {
|
|||||||
errors404Route: typeof errors404Route
|
errors404Route: typeof errors404Route
|
||||||
errors500Route: typeof errors500Route
|
errors500Route: typeof errors500Route
|
||||||
errors503Route: typeof errors503Route
|
errors503Route: typeof errors503Route
|
||||||
|
ConsoleLogRoute: typeof ConsoleLogRoute
|
||||||
|
ConsoleTopupRoute: typeof ConsoleTopupRoute
|
||||||
OauthProviderRoute: typeof OauthProviderRoute
|
OauthProviderRoute: typeof OauthProviderRoute
|
||||||
AboutIndexRoute: typeof AboutIndexRoute
|
AboutIndexRoute: typeof AboutIndexRoute
|
||||||
PricingIndexRoute: typeof PricingIndexRoute
|
PricingIndexRoute: typeof PricingIndexRoute
|
||||||
@ -807,6 +833,20 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof OauthProviderRouteImport
|
preLoaderRoute: typeof OauthProviderRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/console/topup': {
|
||||||
|
id: '/console/topup'
|
||||||
|
path: '/console/topup'
|
||||||
|
fullPath: '/console/topup'
|
||||||
|
preLoaderRoute: typeof ConsoleTopupRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/console/log': {
|
||||||
|
id: '/console/log'
|
||||||
|
path: '/console/log'
|
||||||
|
fullPath: '/console/log'
|
||||||
|
preLoaderRoute: typeof ConsoleLogRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/_authenticated/chat2link': {
|
'/_authenticated/chat2link': {
|
||||||
id: '/_authenticated/chat2link'
|
id: '/_authenticated/chat2link'
|
||||||
path: '/chat2link'
|
path: '/chat2link'
|
||||||
@ -1271,6 +1311,8 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
errors404Route: errors404Route,
|
errors404Route: errors404Route,
|
||||||
errors500Route: errors500Route,
|
errors500Route: errors500Route,
|
||||||
errors503Route: errors503Route,
|
errors503Route: errors503Route,
|
||||||
|
ConsoleLogRoute: ConsoleLogRoute,
|
||||||
|
ConsoleTopupRoute: ConsoleTopupRoute,
|
||||||
OauthProviderRoute: OauthProviderRoute,
|
OauthProviderRoute: OauthProviderRoute,
|
||||||
AboutIndexRoute: AboutIndexRoute,
|
AboutIndexRoute: AboutIndexRoute,
|
||||||
PricingIndexRoute: PricingIndexRoute,
|
PricingIndexRoute: PricingIndexRoute,
|
||||||
|
|||||||
9
web/default/src/routes/__root.tsx
vendored
9
web/default/src/routes/__root.tsx
vendored
@ -16,6 +16,7 @@ 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 } from 'react'
|
||||||
import { type QueryClient } from '@tanstack/react-query'
|
import { type QueryClient } from '@tanstack/react-query'
|
||||||
import {
|
import {
|
||||||
createRootRouteWithContext,
|
createRootRouteWithContext,
|
||||||
@ -31,11 +32,19 @@ import { NavigationProgress } from '@/components/navigation-progress'
|
|||||||
import { GeneralError } from '@/features/errors/general-error'
|
import { GeneralError } from '@/features/errors/general-error'
|
||||||
import { NotFoundError } from '@/features/errors/not-found-error'
|
import { NotFoundError } from '@/features/errors/not-found-error'
|
||||||
import { getSetupStatus } from '@/features/setup/api'
|
import { getSetupStatus } from '@/features/setup/api'
|
||||||
|
import { saveAffiliateCode } from '@/features/auth/lib/storage'
|
||||||
|
|
||||||
function RootComponent() {
|
function RootComponent() {
|
||||||
// Load system configuration (logo, system name, etc.) from backend
|
// Load system configuration (logo, system name, etc.) from backend
|
||||||
useSystemConfig({ autoLoad: true })
|
useSystemConfig({ autoLoad: true })
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const aff = new URLSearchParams(window.location.search).get('aff')?.trim()
|
||||||
|
if (aff) {
|
||||||
|
saveAffiliateCode(aff)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeCustomizationProvider>
|
<ThemeCustomizationProvider>
|
||||||
<NavigationProgress />
|
<NavigationProgress />
|
||||||
|
|||||||
25
web/default/src/routes/console/log.tsx
vendored
Normal file
25
web/default/src/routes/console/log.tsx
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
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 { createFileRoute, redirect } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/console/log')({
|
||||||
|
beforeLoad: () => {
|
||||||
|
throw redirect({ to: '/usage-logs' })
|
||||||
|
},
|
||||||
|
})
|
||||||
28
web/default/src/routes/console/topup.tsx
vendored
Normal file
28
web/default/src/routes/console/topup.tsx
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
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 { createFileRoute, redirect } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/console/topup')({
|
||||||
|
beforeLoad: () => {
|
||||||
|
throw redirect({
|
||||||
|
to: '/wallet',
|
||||||
|
search: { show_history: true },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user