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 {
|
||||
return nil, err
|
||||
}
|
||||
if channel == nil {
|
||||
return nil, errors.New("channel not found")
|
||||
}
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
@ -758,7 +755,7 @@ func EditChannelByTag(tag string, newTag *string, modelMapping *string, models *
|
||||
updateData.Tag = newTag
|
||||
updatedTag = *newTag
|
||||
}
|
||||
if modelMapping != nil && *modelMapping != "" {
|
||||
if modelMapping != nil {
|
||||
updateData.ModelMapping = modelMapping
|
||||
}
|
||||
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
|
||||
className?: string
|
||||
id?: string
|
||||
allowCustomValue?: boolean
|
||||
}
|
||||
|
||||
export function ComboboxInput({
|
||||
@ -46,23 +47,30 @@ export function ComboboxInput({
|
||||
emptyText = 'No option found.',
|
||||
className,
|
||||
id,
|
||||
allowCustomValue = false,
|
||||
}: ComboboxInputProps) {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [searchValue, setSearchValue] = React.useState('')
|
||||
const [highlightedIndex, setHighlightedIndex] = React.useState(-1)
|
||||
const containerRef = React.useRef<HTMLDivElement>(null)
|
||||
const inputRef = React.useRef<HTMLInputElement>(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(() => {
|
||||
if (!value.trim()) return options
|
||||
const search = value.toLowerCase().trim()
|
||||
if (!searchValue.trim()) return options
|
||||
const search = searchValue.toLowerCase().trim()
|
||||
return options.filter(
|
||||
(option) =>
|
||||
option.label.toLowerCase().includes(search) ||
|
||||
option.value.toLowerCase().includes(search)
|
||||
)
|
||||
}, [options, value])
|
||||
}, [options, searchValue])
|
||||
|
||||
// Reset highlight when filtered options change
|
||||
React.useEffect(() => {
|
||||
@ -79,6 +87,7 @@ export function ComboboxInput({
|
||||
!containerRef.current.contains(e.target as Node)
|
||||
) {
|
||||
setOpen(false)
|
||||
setSearchValue('')
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,6 +98,7 @@ export function ComboboxInput({
|
||||
const handleSelect = (selectedValue: string) => {
|
||||
onValueChange(selectedValue)
|
||||
setOpen(false)
|
||||
setSearchValue('')
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
|
||||
@ -117,14 +127,18 @@ export function ComboboxInput({
|
||||
e.preventDefault()
|
||||
if (highlightedIndex >= 0 && filteredOptions[highlightedIndex]) {
|
||||
handleSelect(filteredOptions[highlightedIndex].value)
|
||||
} else if (allowCustomValue && searchValue.trim()) {
|
||||
handleSelect(searchValue.trim())
|
||||
} else {
|
||||
// No highlighted option, just close the dropdown and keep current value
|
||||
setOpen(false)
|
||||
setSearchValue('')
|
||||
}
|
||||
break
|
||||
case 'Escape':
|
||||
e.preventDefault()
|
||||
setOpen(false)
|
||||
setSearchValue('')
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -136,7 +150,9 @@ export function ComboboxInput({
|
||||
item?.scrollIntoView({ block: 'nearest' })
|
||||
}, [highlightedIndex])
|
||||
|
||||
const showDropdown = open && (filteredOptions.length > 0 || value.trim())
|
||||
const showDropdown =
|
||||
open &&
|
||||
(filteredOptions.length > 0 || (allowCustomValue && searchValue.trim()))
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className='relative'>
|
||||
@ -150,12 +166,19 @@ export function ComboboxInput({
|
||||
aria-autocomplete='list'
|
||||
autoComplete='off'
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
value={displayValue}
|
||||
onChange={(e) => {
|
||||
onValueChange(e.target.value)
|
||||
const nextValue = e.target.value
|
||||
setSearchValue(nextValue)
|
||||
if (allowCustomValue) {
|
||||
onValueChange(nextValue)
|
||||
}
|
||||
if (!open) setOpen(true)
|
||||
}}
|
||||
onFocus={() => setOpen(true)}
|
||||
onFocus={() => {
|
||||
setSearchValue(allowCustomValue ? value : '')
|
||||
setOpen(true)
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={cn('pr-9', className)}
|
||||
/>
|
||||
@ -200,10 +223,12 @@ export function ComboboxInput({
|
||||
</ul>
|
||||
) : (
|
||||
<div className='px-2 py-6 text-center text-sm'>
|
||||
{emptyText}
|
||||
{value.trim() && (
|
||||
{t(emptyText)}
|
||||
{allowCustomValue && searchValue.trim() && (
|
||||
<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>
|
||||
|
||||
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}
|
||||
emptyText={props.emptyText}
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@ -143,6 +143,12 @@ function replaceToken(source: string, token: string, value: string) {
|
||||
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({
|
||||
template,
|
||||
apiKey,
|
||||
@ -151,7 +157,7 @@ export function resolveChatUrl({
|
||||
let url = template
|
||||
const safeServerAddress = serverAddress || ''
|
||||
|
||||
const safeApiKey = apiKey || ''
|
||||
const safeApiKey = normalizeApiKey(apiKey || '')
|
||||
|
||||
if (url.includes('{cherryConfig}')) {
|
||||
const payload = {
|
||||
|
||||
@ -115,6 +115,15 @@ export function ConsumptionDistributionChart(
|
||||
]
|
||||
)
|
||||
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 (
|
||||
<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'>
|
||||
{themeReady && spec && (
|
||||
<VChart
|
||||
key={`${chartType}-${resolvedTheme}-${customization.preset}`}
|
||||
key={chartKey}
|
||||
spec={{
|
||||
...spec,
|
||||
theme: resolvedTheme === 'dark' ? 'dark' : 'light',
|
||||
|
||||
@ -114,6 +114,15 @@ export function ModelCharts(props: ModelChartsProps) {
|
||||
)
|
||||
|
||||
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 (
|
||||
<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'>
|
||||
{themeReady && spec && (
|
||||
<VChart
|
||||
key={`${activeTab}-${resolvedTheme}-${customization.preset}`}
|
||||
key={chartKey}
|
||||
spec={{
|
||||
...spec,
|
||||
theme: resolvedTheme === 'dark' ? 'dark' : 'light',
|
||||
|
||||
@ -212,7 +212,7 @@ export function processChartData(
|
||||
legends: { visible: true, selectMode: 'single' },
|
||||
},
|
||||
spec_model_line: {
|
||||
type: 'line',
|
||||
type: 'area',
|
||||
data: [{ id: 'lineData', values: [] }],
|
||||
xField: 'Time',
|
||||
yField: 'Count',
|
||||
|
||||
@ -25,27 +25,45 @@ const FEEDBACK_URL = 'https://github.com/QuantumNous/new-api/issues'
|
||||
|
||||
type GeneralErrorProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
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({
|
||||
className,
|
||||
minimal = false,
|
||||
error,
|
||||
}: GeneralErrorProps) {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
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 (
|
||||
<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'>
|
||||
{!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'>
|
||||
{t('Oops! Something went wrong')} {`:')`}
|
||||
</span>
|
||||
<span className='font-medium'>{title}</span>
|
||||
<p className='text-muted-foreground text-center'>
|
||||
{t('We apologize for the inconvenience.')} <br />{' '}
|
||||
{t('Please try again later.')}
|
||||
{t('We apologize for the inconvenience.')} <br /> {description}
|
||||
</p>
|
||||
{!minimal && (
|
||||
<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='min-w-0'>
|
||||
<span className='block truncate font-medium'>
|
||||
{selectedOption?.value || placeholder || t('Select a group')}
|
||||
{selectedOption?.label || placeholder || t('Select a group')}
|
||||
</span>
|
||||
{selectedOption?.desc && (
|
||||
<span className='text-muted-foreground block truncate text-[11px] sm:text-xs'>
|
||||
@ -178,7 +178,7 @@ export function ApiKeyGroupCombobox({
|
||||
<CommandItem
|
||||
key={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'
|
||||
>
|
||||
<Check
|
||||
@ -189,7 +189,7 @@ export function ApiKeyGroupCombobox({
|
||||
/>
|
||||
<span className='min-w-0 flex-1'>
|
||||
<span className='block truncate font-medium'>
|
||||
{option.value}
|
||||
{option.label}
|
||||
</span>
|
||||
{option.desc && (
|
||||
<span className='text-muted-foreground block truncate text-xs'>
|
||||
|
||||
@ -126,7 +126,7 @@ export function RateLimitSection({ defaultValues }: RateLimitSectionProps) {
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
{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>
|
||||
</div>
|
||||
|
||||
@ -171,6 +171,7 @@ export function UsageLogsTable({ logCategory }: UsageLogsTableProps) {
|
||||
'No usage logs available. Logs will appear here once API calls are made.'
|
||||
)}
|
||||
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'
|
||||
toolbar={
|
||||
isCommon ? (
|
||||
|
||||
@ -176,8 +176,8 @@ export function BillingHistoryDialog({
|
||||
</p>
|
||||
<p className='mt-1 text-xs'>
|
||||
{keyword
|
||||
? 'Try adjusting your search'
|
||||
: 'Your transaction history will appear here'}
|
||||
? t('Try adjusting your search')
|
||||
: t('Your transaction history will appear here')}
|
||||
</p>
|
||||
</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='space-y-1'>
|
||||
<Label className='text-muted-foreground text-xs'>
|
||||
Payment Method
|
||||
{t('Payment Method')}
|
||||
</Label>
|
||||
<div className='text-sm font-medium'>
|
||||
{getPaymentMethodName(record.payment_method)}
|
||||
{getPaymentMethodName(record.payment_method, t)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='space-y-1'>
|
||||
<Label className='text-muted-foreground text-xs'>
|
||||
Amount
|
||||
{t('Amount')}
|
||||
</Label>
|
||||
<div className='text-sm font-semibold'>
|
||||
{formatCurrencyFromUSD(record.amount, {
|
||||
@ -253,7 +253,7 @@ export function BillingHistoryDialog({
|
||||
</div>
|
||||
<div className='space-y-1'>
|
||||
<Label className='text-muted-foreground text-xs'>
|
||||
Payment
|
||||
{t('Payment')}
|
||||
</Label>
|
||||
<div className='text-sm font-semibold text-red-600'>
|
||||
{formatNumber(record.money)}
|
||||
@ -270,7 +270,7 @@ export function BillingHistoryDialog({
|
||||
onClick={() => setConfirmTradeNo(record.trade_no)}
|
||||
disabled={completing}
|
||||
>
|
||||
Complete Order
|
||||
{t('Complete Order')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@ -341,7 +341,7 @@ export function BillingHistoryDialog({
|
||||
onClick={handleConfirmComplete}
|
||||
disabled={completing}
|
||||
>
|
||||
{completing ? 'Processing...' : 'Confirm'}
|
||||
{completing ? t('Processing...') : t('Confirm')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
@ -67,8 +67,12 @@ export const PAYMENT_METHOD_NAMES: Record<string, string> = {
|
||||
/**
|
||||
* Get payment method display name
|
||||
*/
|
||||
export function getPaymentMethodName(method: string): string {
|
||||
return PAYMENT_METHOD_NAMES[method] || method
|
||||
export function getPaymentMethodName(
|
||||
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",
|
||||
"missingCount": 0,
|
||||
"extrasCount": 0,
|
||||
"untranslatedCount": 90
|
||||
"untranslatedCount": 92
|
||||
},
|
||||
"ru": {
|
||||
"file": "ru.json",
|
||||
"missingCount": 0,
|
||||
"extrasCount": 0,
|
||||
"untranslatedCount": 105
|
||||
"untranslatedCount": 107
|
||||
},
|
||||
"vi": {
|
||||
"file": "vi.json",
|
||||
|
||||
@ -88,5 +88,7 @@
|
||||
"Webhook URL:": "Webhook URL:",
|
||||
"whsec_xxx": "whsec_xxx",
|
||||
"Xinference": "Xinference",
|
||||
"Xunfei": "Xunfei"
|
||||
"Xunfei": "Xunfei",
|
||||
"Alipay": "Alipay",
|
||||
"WeChat Pay": "WeChat Pay"
|
||||
}
|
||||
|
||||
@ -103,5 +103,7 @@
|
||||
"whsec_xxx": "whsec_xxx",
|
||||
"Xinference": "Xinference",
|
||||
"Xunfei": "Xunfei",
|
||||
"Alipay": "Alipay",
|
||||
"WeChat Pay": "WeChat Pay",
|
||||
"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 Turnstile secret key": "Your Turnstile secret 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",
|
||||
"Zhipu": "Zhipu",
|
||||
"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 Turnstile secret key": "Votre clé secrète 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",
|
||||
"Zhipu": "Zhipu",
|
||||
"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 Turnstile secret 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": "データ保持なし",
|
||||
"Zhipu": "Zhipu",
|
||||
"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 Turnstile secret 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": "Без хранения данных",
|
||||
"Zhipu": "Zhipu",
|
||||
"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 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",
|
||||
"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",
|
||||
"Zhipu": "Zhipu",
|
||||
"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 Turnstile secret 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": "零数据保留",
|
||||
"Zhipu": "智谱",
|
||||
"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 AboutIndexRouteImport } from './routes/about/index'
|
||||
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 errors503RouteImport } from './routes/(errors)/503'
|
||||
import { Route as errors500RouteImport } from './routes/(errors)/500'
|
||||
@ -114,6 +116,16 @@ const OauthProviderRoute = OauthProviderRouteImport.update({
|
||||
path: '/oauth/$provider',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} 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({
|
||||
id: '/chat2link',
|
||||
path: '/chat2link',
|
||||
@ -391,6 +403,8 @@ export interface FileRoutesByFullPath {
|
||||
'/500': typeof errors500Route
|
||||
'/503': typeof errors503Route
|
||||
'/chat2link': typeof AuthenticatedChat2linkRoute
|
||||
'/console/log': typeof ConsoleLogRoute
|
||||
'/console/topup': typeof ConsoleTopupRoute
|
||||
'/oauth/$provider': typeof OauthProviderRoute
|
||||
'/about/': typeof AboutIndexRoute
|
||||
'/pricing/': typeof PricingIndexRoute
|
||||
@ -446,6 +460,8 @@ export interface FileRoutesByTo {
|
||||
'/500': typeof errors500Route
|
||||
'/503': typeof errors503Route
|
||||
'/chat2link': typeof AuthenticatedChat2linkRoute
|
||||
'/console/log': typeof ConsoleLogRoute
|
||||
'/console/topup': typeof ConsoleTopupRoute
|
||||
'/oauth/$provider': typeof OauthProviderRoute
|
||||
'/about': typeof AboutIndexRoute
|
||||
'/pricing': typeof PricingIndexRoute
|
||||
@ -505,6 +521,8 @@ export interface FileRoutesById {
|
||||
'/(errors)/500': typeof errors500Route
|
||||
'/(errors)/503': typeof errors503Route
|
||||
'/_authenticated/chat2link': typeof AuthenticatedChat2linkRoute
|
||||
'/console/log': typeof ConsoleLogRoute
|
||||
'/console/topup': typeof ConsoleTopupRoute
|
||||
'/oauth/$provider': typeof OauthProviderRoute
|
||||
'/about/': typeof AboutIndexRoute
|
||||
'/pricing/': typeof PricingIndexRoute
|
||||
@ -563,6 +581,8 @@ export interface FileRouteTypes {
|
||||
| '/500'
|
||||
| '/503'
|
||||
| '/chat2link'
|
||||
| '/console/log'
|
||||
| '/console/topup'
|
||||
| '/oauth/$provider'
|
||||
| '/about/'
|
||||
| '/pricing/'
|
||||
@ -618,6 +638,8 @@ export interface FileRouteTypes {
|
||||
| '/500'
|
||||
| '/503'
|
||||
| '/chat2link'
|
||||
| '/console/log'
|
||||
| '/console/topup'
|
||||
| '/oauth/$provider'
|
||||
| '/about'
|
||||
| '/pricing'
|
||||
@ -676,6 +698,8 @@ export interface FileRouteTypes {
|
||||
| '/(errors)/500'
|
||||
| '/(errors)/503'
|
||||
| '/_authenticated/chat2link'
|
||||
| '/console/log'
|
||||
| '/console/topup'
|
||||
| '/oauth/$provider'
|
||||
| '/about/'
|
||||
| '/pricing/'
|
||||
@ -727,6 +751,8 @@ export interface RootRouteChildren {
|
||||
errors404Route: typeof errors404Route
|
||||
errors500Route: typeof errors500Route
|
||||
errors503Route: typeof errors503Route
|
||||
ConsoleLogRoute: typeof ConsoleLogRoute
|
||||
ConsoleTopupRoute: typeof ConsoleTopupRoute
|
||||
OauthProviderRoute: typeof OauthProviderRoute
|
||||
AboutIndexRoute: typeof AboutIndexRoute
|
||||
PricingIndexRoute: typeof PricingIndexRoute
|
||||
@ -807,6 +833,20 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof OauthProviderRouteImport
|
||||
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': {
|
||||
id: '/_authenticated/chat2link'
|
||||
path: '/chat2link'
|
||||
@ -1271,6 +1311,8 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
errors404Route: errors404Route,
|
||||
errors500Route: errors500Route,
|
||||
errors503Route: errors503Route,
|
||||
ConsoleLogRoute: ConsoleLogRoute,
|
||||
ConsoleTopupRoute: ConsoleTopupRoute,
|
||||
OauthProviderRoute: OauthProviderRoute,
|
||||
AboutIndexRoute: AboutIndexRoute,
|
||||
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
|
||||
*/
|
||||
import { useEffect } from 'react'
|
||||
import { type QueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
createRootRouteWithContext,
|
||||
@ -31,11 +32,19 @@ import { NavigationProgress } from '@/components/navigation-progress'
|
||||
import { GeneralError } from '@/features/errors/general-error'
|
||||
import { NotFoundError } from '@/features/errors/not-found-error'
|
||||
import { getSetupStatus } from '@/features/setup/api'
|
||||
import { saveAffiliateCode } from '@/features/auth/lib/storage'
|
||||
|
||||
function RootComponent() {
|
||||
// Load system configuration (logo, system name, etc.) from backend
|
||||
useSystemConfig({ autoLoad: true })
|
||||
|
||||
useEffect(() => {
|
||||
const aff = new URLSearchParams(window.location.search).get('aff')?.trim()
|
||||
if (aff) {
|
||||
saveAffiliateCode(aff)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ThemeCustomizationProvider>
|
||||
<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