diff --git a/model/channel.go b/model/channel.go index dce78c51..76b0554d 100644 --- a/model/channel.go +++ b/model/channel.go @@ -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 != "" { diff --git a/web/default/src/components/ui/combobox-input.tsx b/web/default/src/components/ui/combobox-input.tsx index 1cd80874..7e45a2d9 100644 --- a/web/default/src/components/ui/combobox-input.tsx +++ b/web/default/src/components/ui/combobox-input.tsx @@ -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(null) const inputRef = React.useRef(null) const listRef = React.useRef(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 (
@@ -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({ ) : (
- {emptyText} - {value.trim() && ( + {t(emptyText)} + {allowCustomValue && searchValue.trim() && (
- {t('Press Enter to use "{{value}}"', { value: value.trim() })} + {t('Press Enter to use "{{value}}"', { + value: searchValue.trim(), + })}
)}
diff --git a/web/default/src/components/ui/combobox.tsx b/web/default/src/components/ui/combobox.tsx index 729d3ada..638efc9f 100644 --- a/web/default/src/components/ui/combobox.tsx +++ b/web/default/src/components/ui/combobox.tsx @@ -68,6 +68,7 @@ function Combobox( placeholder={props.searchPlaceholder ?? props.placeholder} emptyText={props.emptyText} className={props.className} + allowCustomValue={props.allowCustomValue} /> ) } diff --git a/web/default/src/features/channels/lib/channel-form.ts b/web/default/src/features/channels/lib/channel-form.ts index e05da96d..709a7ee9 100644 --- a/web/default/src/features/channels/lib/channel-form.ts +++ b/web/default/src/features/channels/lib/channel-form.ts @@ -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 } diff --git a/web/default/src/features/chat/lib/chat-links.ts b/web/default/src/features/chat/lib/chat-links.ts index 6d061967..729a59fe 100644 --- a/web/default/src/features/chat/lib/chat-links.ts +++ b/web/default/src/features/chat/lib/chat-links.ts @@ -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 = { diff --git a/web/default/src/features/dashboard/components/models/consumption-distribution-chart.tsx b/web/default/src/features/dashboard/components/models/consumption-distribution-chart.tsx index 268da7ec..6d09b5e3 100644 --- a/web/default/src/features/dashboard/components/models/consumption-distribution-chart.tsx +++ b/web/default/src/features/dashboard/components/models/consumption-distribution-chart.tsx @@ -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 (
@@ -152,7 +161,7 @@ export function ConsumptionDistributionChart(
{themeReady && spec && ( @@ -149,7 +158,7 @@ export function ModelCharts(props: ModelChartsProps) {
{themeReady && spec && ( & { minimal?: boolean + error?: unknown +} + +function getHttpStatus(error: unknown): number | undefined { + if (typeof error !== 'object' || error === null) return undefined + const response = (error as Record).response + if (typeof response !== 'object' || response === null) return undefined + const status = (response as Record).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 (
{!minimal && ( -

500

+

+ {status ?? 500} +

)} - - {t('Oops! Something went wrong')} {`:')`} - + {title}

- {t('We apologize for the inconvenience.')}
{' '} - {t('Please try again later.')} + {t('We apologize for the inconvenience.')}
{description}

{!minimal && (

diff --git a/web/default/src/features/keys/components/api-key-group-combobox.tsx b/web/default/src/features/keys/components/api-key-group-combobox.tsx index 48882875..c0a59cdc 100644 --- a/web/default/src/features/keys/components/api-key-group-combobox.tsx +++ b/web/default/src/features/keys/components/api-key-group-combobox.tsx @@ -145,7 +145,7 @@ export function ApiKeyGroupCombobox({ - {selectedOption?.value || placeholder || t('Select a group')} + {selectedOption?.label || placeholder || t('Select a group')} {selectedOption?.desc && ( @@ -178,7 +178,7 @@ export function ApiKeyGroupCombobox({ handleSelect(option.value)} className='data-[selected=true]:bg-muted items-start gap-3 rounded-lg px-3 py-3 transition-colors' > - {option.value} + {option.label} {option.desc && ( diff --git a/web/default/src/features/system-settings/request-limits/rate-limit-section.tsx b/web/default/src/features/system-settings/request-limits/rate-limit-section.tsx index 3b876b2f..04bc8b1f 100644 --- a/web/default/src/features/system-settings/request-limits/rate-limit-section.tsx +++ b/web/default/src/features/system-settings/request-limits/rate-limit-section.tsx @@ -126,7 +126,7 @@ export function RateLimitSection({ defaultValues }: RateLimitSectionProps) { {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.' )}

diff --git a/web/default/src/features/usage-logs/components/usage-logs-table.tsx b/web/default/src/features/usage-logs/components/usage-logs-table.tsx index 69a24210..333bfb08 100644 --- a/web/default/src/features/usage-logs/components/usage-logs-table.tsx +++ b/web/default/src/features/usage-logs/components/usage-logs-table.tsx @@ -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 ? ( diff --git a/web/default/src/features/wallet/components/dialogs/billing-history-dialog.tsx b/web/default/src/features/wallet/components/dialogs/billing-history-dialog.tsx index 6ef4e7dd..ac58574b 100644 --- a/web/default/src/features/wallet/components/dialogs/billing-history-dialog.tsx +++ b/web/default/src/features/wallet/components/dialogs/billing-history-dialog.tsx @@ -176,8 +176,8 @@ export function BillingHistoryDialog({

{keyword - ? 'Try adjusting your search' - : 'Your transaction history will appear here'} + ? t('Try adjusting your search') + : t('Your transaction history will appear here')}

) : ( @@ -233,15 +233,15 @@ export function BillingHistoryDialog({
- {getPaymentMethodName(record.payment_method)} + {getPaymentMethodName(record.payment_method, t)}
{formatCurrencyFromUSD(record.amount, { @@ -253,7 +253,7 @@ export function BillingHistoryDialog({
{formatNumber(record.money)} @@ -270,7 +270,7 @@ export function BillingHistoryDialog({ onClick={() => setConfirmTradeNo(record.trade_no)} disabled={completing} > - Complete Order + {t('Complete Order')}
)} @@ -341,7 +341,7 @@ export function BillingHistoryDialog({ onClick={handleConfirmComplete} disabled={completing} > - {completing ? 'Processing...' : 'Confirm'} + {completing ? t('Processing...') : t('Confirm')} diff --git a/web/default/src/features/wallet/lib/billing.ts b/web/default/src/features/wallet/lib/billing.ts index 41849004..77bf98c6 100644 --- a/web/default/src/features/wallet/lib/billing.ts +++ b/web/default/src/features/wallet/lib/billing.ts @@ -67,8 +67,12 @@ export const PAYMENT_METHOD_NAMES: Record = { /** * 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 } /** diff --git a/web/default/src/i18n/locales/_reports/_sync-report.json b/web/default/src/i18n/locales/_reports/_sync-report.json index 9f4616fa..c93ef1f4 100644 --- a/web/default/src/i18n/locales/_reports/_sync-report.json +++ b/web/default/src/i18n/locales/_reports/_sync-report.json @@ -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", diff --git a/web/default/src/i18n/locales/_reports/ja.untranslated.json b/web/default/src/i18n/locales/_reports/ja.untranslated.json index dc73160b..2e6d3ca8 100644 --- a/web/default/src/i18n/locales/_reports/ja.untranslated.json +++ b/web/default/src/i18n/locales/_reports/ja.untranslated.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" } diff --git a/web/default/src/i18n/locales/_reports/ru.untranslated.json b/web/default/src/i18n/locales/_reports/ru.untranslated.json index 2e417dff..0727e081 100644 --- a/web/default/src/i18n/locales/_reports/ru.untranslated.json +++ b/web/default/src/i18n/locales/_reports/ru.untranslated.json @@ -103,5 +103,7 @@ "whsec_xxx": "whsec_xxx", "Xinference": "Xinference", "Xunfei": "Xunfei", + "Alipay": "Alipay", + "WeChat Pay": "WeChat Pay", "Zhipu V4": "Zhipu V4" } diff --git a/web/default/src/i18n/locales/en.json b/web/default/src/i18n/locales/en.json index 05e8c5bd..ed77ba19 100644 --- a/web/default/src/i18n/locales/en.json +++ b/web/default/src/i18n/locales/en.json @@ -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", diff --git a/web/default/src/i18n/locales/fr.json b/web/default/src/i18n/locales/fr.json index f13d1b8b..67ab1178 100644 --- a/web/default/src/i18n/locales/fr.json +++ b/web/default/src/i18n/locales/fr.json @@ -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", diff --git a/web/default/src/i18n/locales/ja.json b/web/default/src/i18n/locales/ja.json index 7a17e1cd..6184b912 100644 --- a/web/default/src/i18n/locales/ja.json +++ b/web/default/src/i18n/locales/ja.json @@ -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", diff --git a/web/default/src/i18n/locales/ru.json b/web/default/src/i18n/locales/ru.json index 91f8030e..5cf0e0aa 100644 --- a/web/default/src/i18n/locales/ru.json +++ b/web/default/src/i18n/locales/ru.json @@ -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", diff --git a/web/default/src/i18n/locales/vi.json b/web/default/src/i18n/locales/vi.json index 1e5fede3..4047a683 100644 --- a/web/default/src/i18n/locales/vi.json +++ b/web/default/src/i18n/locales/vi.json @@ -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", diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json index 5d3014ce..949944d9 100644 --- a/web/default/src/i18n/locales/zh.json +++ b/web/default/src/i18n/locales/zh.json @@ -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", diff --git a/web/default/src/routeTree.gen.ts b/web/default/src/routeTree.gen.ts index 8503f671..8202cdc2 100644 --- a/web/default/src/routeTree.gen.ts +++ b/web/default/src/routeTree.gen.ts @@ -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, diff --git a/web/default/src/routes/__root.tsx b/web/default/src/routes/__root.tsx index c7754217..1af569a1 100644 --- a/web/default/src/routes/__root.tsx +++ b/web/default/src/routes/__root.tsx @@ -16,6 +16,7 @@ along with this program. If not, see . 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 ( diff --git a/web/default/src/routes/console/log.tsx b/web/default/src/routes/console/log.tsx new file mode 100644 index 00000000..4af6ba98 --- /dev/null +++ b/web/default/src/routes/console/log.tsx @@ -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 . + +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' }) + }, +}) diff --git a/web/default/src/routes/console/topup.tsx b/web/default/src/routes/console/topup.tsx new file mode 100644 index 00000000..fce5e772 --- /dev/null +++ b/web/default/src/routes/console/topup.tsx @@ -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 . + +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 }, + }) + }, +})