Fix dist-site pricing topup and review UX
This commit is contained in:
1 parent
58a18a2991
commit
d6b0d65ce2
5 files changed
+92
-30
No files matched your search
Vendored
+2
-2
@@ -7,8 +7,8 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Noto+Sans+SC:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||
<title>AI Platform</title>
|
||||
<script type="module" crossorigin src="/assets/index-VPjF2gPe.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DZDvBjih.css">
|
||||
<script type="module" crossorigin src="/assets/index-CgGPBQ4H.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-B3G1pDJs.css">
|
||||
</head>
|
||||
<body class="antialiased">
|
||||
<script>
|
||||
|
||||
@@ -197,6 +197,8 @@
|
||||
"pricing.model": "Model",
|
||||
"pricing.inputPrice": "Input / 1M tokens",
|
||||
"pricing.outputPrice": "Output / 1M tokens",
|
||||
"pricing.perCall": "Per call",
|
||||
"pricing.perCallUnit": "call",
|
||||
"pricing.status": "Status",
|
||||
"pricing.online": "Online",
|
||||
"pricing.unknown": "Unknown",
|
||||
@@ -246,7 +248,9 @@
|
||||
"topup.onlineTopup": "Online Top Up",
|
||||
"topup.selectAmount": "Select Amount",
|
||||
"topup.customAmount": "Custom Amount",
|
||||
"topup.amountPlaceholder": "Enter amount (min ${{min}})",
|
||||
"topup.customAmountHint": "Custom amounts are automatically aligned to payable tiers so the entered and charged amounts stay consistent.",
|
||||
"topup.rechargeAmountLabel": "Recharge Amount",
|
||||
"topup.amountPlaceholder": "Enter amount (min {{min}})",
|
||||
"topup.paymentMethod": "Payment Method",
|
||||
"topup.processing": "Processing...",
|
||||
"topup.calculating": "Calculating...",
|
||||
|
||||
@@ -197,6 +197,8 @@
|
||||
"pricing.model": "模型",
|
||||
"pricing.inputPrice": "输入 / 百万 token",
|
||||
"pricing.outputPrice": "输出 / 百万 token",
|
||||
"pricing.perCall": "按次计费",
|
||||
"pricing.perCallUnit": "次",
|
||||
"pricing.status": "状态",
|
||||
"pricing.online": "在线",
|
||||
"pricing.unknown": "未知",
|
||||
@@ -246,7 +248,9 @@
|
||||
"topup.onlineTopup": "在线充值",
|
||||
"topup.selectAmount": "选择金额",
|
||||
"topup.customAmount": "自定义金额",
|
||||
"topup.amountPlaceholder": "输入金额(最低 ${{min}})",
|
||||
"topup.customAmountHint": "自定义金额会自动对齐到可支付档位,避免输入金额和实付金额不一致。",
|
||||
"topup.rechargeAmountLabel": "充值金额",
|
||||
"topup.amountPlaceholder": "输入金额(最低 {{min}})",
|
||||
"topup.paymentMethod": "支付方式",
|
||||
"topup.processing": "处理中...",
|
||||
"topup.calculating": "计算中...",
|
||||
|
||||
+19
-4
@@ -45,11 +45,26 @@ export default function Pricing() {
|
||||
(m.display_name || m.model_name || '').toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
// Sort by input_price ascending
|
||||
list = [...list].sort((a, b) => (Number(a.input_price) || 0) - (Number(b.input_price) || 0));
|
||||
list = [...list].sort((a, b) => {
|
||||
if (!!a.is_per_call !== !!b.is_per_call) {
|
||||
return a.is_per_call ? 1 : -1;
|
||||
}
|
||||
if (a.is_per_call) {
|
||||
return (Number(a.fixed_price) || 0) - (Number(b.fixed_price) || 0);
|
||||
}
|
||||
return (Number(a.input_price) || 0) - (Number(b.input_price) || 0);
|
||||
});
|
||||
return list;
|
||||
}, [enabledModels, vendor, search]);
|
||||
|
||||
const formatTokenPrice = (price) =>
|
||||
price != null ? `${symbol}${(Number(price) * 1000 * rate).toFixed(4)}` : '-';
|
||||
|
||||
const formatPerCallPrice = (price) =>
|
||||
price != null
|
||||
? `${symbol}${(Number(price) * rate).toFixed(4)}/${t('pricing.perCallUnit')}`
|
||||
: '-';
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[50vh]">
|
||||
@@ -134,10 +149,10 @@ export default function Pricing() {
|
||||
<span className="font-mono text-page">{m.display_name || m.model_name}</span>
|
||||
</td>
|
||||
<td className="px-5 py-3.5 text-right font-mono text-page-label">
|
||||
{m.input_price != null ? `${symbol}${(Number(m.input_price) * 1000 * rate).toFixed(4)}` : '-'}
|
||||
{m.is_per_call ? t('pricing.perCall') : formatTokenPrice(m.input_price)}
|
||||
</td>
|
||||
<td className="px-5 py-3.5 text-right font-mono text-page-label">
|
||||
{m.output_price != null ? `${symbol}${(Number(m.output_price) * 1000 * rate).toFixed(4)}` : '-'}
|
||||
{m.is_per_call ? formatPerCallPrice(m.fixed_price) : formatTokenPrice(m.output_price)}
|
||||
</td>
|
||||
<td className="px-5 py-3.5 text-center">
|
||||
<span className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs border ${
|
||||
|
||||
+61
-22
@@ -28,6 +28,7 @@ export default function Topup() {
|
||||
|
||||
// Online topup
|
||||
const [amount, setAmount] = useState('');
|
||||
const [displayAmount, setDisplayAmount] = useState('');
|
||||
const [selectedPreset, setSelectedPreset] = useState(null);
|
||||
const [payAmount, setPayAmount] = useState(null);
|
||||
const [amountLoading, setAmountLoading] = useState(false);
|
||||
@@ -50,6 +51,14 @@ export default function Topup() {
|
||||
|
||||
const enableTopup = site?.enable_topup && topupInfo;
|
||||
const topupConfig = site?.topup_config;
|
||||
const presetAmounts = topupInfo?.amount_options || [1, 5, 10, 20, 50, 100];
|
||||
const minTopup = topupInfo?.min_topup || 1;
|
||||
const payMethods = topupInfo?.pay_methods || [];
|
||||
const enableOnline = topupInfo?.enable_online_topup;
|
||||
const enableStripe = topupInfo?.enable_stripe_topup;
|
||||
const enableCreem = topupInfo?.enable_creem_topup;
|
||||
const enableCrypto = topupInfo?.enable_crypto_topup;
|
||||
const hasAnyPayment = enableOnline || enableStripe || enableCreem || enableCrypto;
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -71,6 +80,22 @@ export default function Topup() {
|
||||
const requestCount = usage?.request_count ?? user?.request_count ?? 0;
|
||||
const balanceDollars = quota / Q * rate;
|
||||
|
||||
const formatCurrencyAmount = useCallback((value) => {
|
||||
if (value === '' || value == null || Number.isNaN(Number(value))) return '';
|
||||
return Number(value).toFixed(2).replace(/\.?0+$/, '');
|
||||
}, []);
|
||||
|
||||
const toDisplayAmount = useCallback((quotaAmount) => {
|
||||
if (quotaAmount === '' || quotaAmount == null) return '';
|
||||
return formatCurrencyAmount(Number(quotaAmount) * rate);
|
||||
}, [formatCurrencyAmount, rate]);
|
||||
|
||||
const toQuotaAmount = useCallback((currencyAmount) => {
|
||||
const numeric = Number.parseFloat(currencyAmount);
|
||||
if (!Number.isFinite(numeric) || numeric <= 0) return '';
|
||||
return Math.max(minTopup, Math.round(numeric / rate));
|
||||
}, [minTopup, rate]);
|
||||
|
||||
// Redeem
|
||||
const handleRedeem = async (e) => {
|
||||
e.preventDefault();
|
||||
@@ -103,7 +128,8 @@ export default function Topup() {
|
||||
// Select preset
|
||||
const handlePreset = (val) => {
|
||||
setSelectedPreset(val);
|
||||
setAmount(val);
|
||||
setAmount(String(val));
|
||||
setDisplayAmount(toDisplayAmount(val));
|
||||
calcAmount(val);
|
||||
};
|
||||
|
||||
@@ -291,15 +317,6 @@ export default function Topup() {
|
||||
setHistoryLoading(false);
|
||||
};
|
||||
|
||||
const presetAmounts = topupInfo?.amount_options || [1, 5, 10, 20, 50, 100];
|
||||
const minTopup = topupInfo?.min_topup || 1;
|
||||
const payMethods = topupInfo?.pay_methods || [];
|
||||
const enableOnline = topupInfo?.enable_online_topup;
|
||||
const enableStripe = topupInfo?.enable_stripe_topup;
|
||||
const enableCreem = topupInfo?.enable_creem_topup;
|
||||
const enableCrypto = topupInfo?.enable_crypto_topup;
|
||||
const hasAnyPayment = enableOnline || enableStripe || enableCreem || enableCrypto;
|
||||
|
||||
// Filter pay methods: EPay methods + Stripe-based methods go in the main payment buttons
|
||||
// Creem and Crypto get their own sections
|
||||
const epayAndStripeMethods = payMethods.filter(
|
||||
@@ -373,7 +390,7 @@ export default function Topup() {
|
||||
: 'glass-sm text-page-label hover:text-page hover:bg-page-surface-hover'
|
||||
}`}
|
||||
>
|
||||
{symbol}{Math.round(val * rate)}
|
||||
{symbol}{formatCurrencyAmount(val * rate)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -385,28 +402,50 @@ export default function Topup() {
|
||||
<div className="flex gap-3">
|
||||
<input
|
||||
type="number"
|
||||
value={amount ? Math.round(amount * rate) : ''}
|
||||
value={displayAmount}
|
||||
onChange={(e) => {
|
||||
const cnyVal = e.target.value;
|
||||
const usdVal = cnyVal ? Math.max(1, Math.ceil(cnyVal / rate)) : '';
|
||||
setAmount(usdVal);
|
||||
const currentValue = e.target.value;
|
||||
setDisplayAmount(currentValue);
|
||||
setSelectedPreset(null);
|
||||
const quotaAmount = toQuotaAmount(currentValue);
|
||||
setAmount(quotaAmount === '' ? '' : String(quotaAmount));
|
||||
calcAmount(quotaAmount);
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const cnyVal = e.target.value;
|
||||
const usdVal = cnyVal ? Math.max(1, Math.ceil(cnyVal / rate)) : '';
|
||||
calcAmount(usdVal);
|
||||
const quotaAmount = toQuotaAmount(e.target.value);
|
||||
if (quotaAmount === '') {
|
||||
setDisplayAmount('');
|
||||
setAmount('');
|
||||
setPayAmount(null);
|
||||
return;
|
||||
}
|
||||
setAmount(String(quotaAmount));
|
||||
setDisplayAmount(toDisplayAmount(quotaAmount));
|
||||
calcAmount(quotaAmount);
|
||||
}}
|
||||
min={Math.round(minTopup * rate)}
|
||||
placeholder={t('topup.amountPlaceholder', { min: Math.round(minTopup * rate) })}
|
||||
min={minTopup * rate}
|
||||
step="0.01"
|
||||
placeholder={t('topup.amountPlaceholder', { min: formatCurrencyAmount(minTopup * rate) })}
|
||||
className="input flex-1"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-page-muted mt-2">
|
||||
{t('topup.customAmountHint')}
|
||||
</p>
|
||||
{amountLoading ? (
|
||||
<p className="text-xs text-page-muted mt-2">{t('topup.calculating')}</p>
|
||||
) : payAmount ? (
|
||||
<p className="text-xs text-page-secondary mt-2">
|
||||
{t('topup.payAmountLabel')}: <span className="text-page-success font-medium">¥{payAmount}</span>
|
||||
<div className="mt-2 space-y-1 text-xs text-page-secondary">
|
||||
<p>
|
||||
{t('topup.rechargeAmountLabel')}: <span className="text-page font-medium">{symbol}{displayAmount || toDisplayAmount(amount)}</span>
|
||||
</p>
|
||||
<p>
|
||||
{t('topup.payAmountLabel')}: <span className="text-page-success font-medium">{symbol}{payAmount}</span>
|
||||
</p>
|
||||
</div>
|
||||
) : amount ? (
|
||||
<p className="text-xs text-page-muted mt-2">
|
||||
{t('topup.rechargeAmountLabel')}: {symbol}{displayAmount || toDisplayAmount(amount)}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user