feat: 官方等价对比改为轮播动效,展示所有匹配模型
- 新增 RotatingEquiv 组件,3秒间隔淡入淡出切换 - calcOfficialEquivList 返回所有匹配模型(含 Gemini 系列) - Packages 页面和 7 个 Home 模板全部适配轮播 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
b34e81bb58
commit
a6b962084d
49 files changed
+134
-82
No files matched your search
@@ -0,0 +1,34 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* Rotating display of official price equivalents.
|
||||
* Cycles through items with a fade up/down transition.
|
||||
*
|
||||
* @param {{ items: {label:string, equivDollars:number}[], text: (item) => string, className?: string, interval?: number }}
|
||||
*/
|
||||
export default function RotatingEquiv({ items, text, className = '', interval = 3000 }) {
|
||||
const [idx, setIdx] = useState(0);
|
||||
const [visible, setVisible] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (items.length <= 1) return;
|
||||
const timer = setInterval(() => {
|
||||
setVisible(false);
|
||||
setTimeout(() => {
|
||||
setIdx((i) => (i + 1) % items.length);
|
||||
setVisible(true);
|
||||
}, 300);
|
||||
}, interval);
|
||||
return () => clearInterval(timer);
|
||||
}, [items.length, interval]);
|
||||
|
||||
if (!items || items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`inline-block transition-all duration-300 ${visible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-1'} ${className}`}
|
||||
>
|
||||
{text(items[idx])}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,8 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { getSitePackages, getSiteModels, subscribePackage, getActiveSubscriptions, Q } from '../api';
|
||||
import SpotlightCard from '../components/bits/SpotlightCard';
|
||||
import { calcOfficialEquiv } from '../utils/officialEquiv';
|
||||
import { calcOfficialEquivList } from '../utils/officialEquiv';
|
||||
import RotatingEquiv from '../components/bits/RotatingEquiv';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const resetLabelKeys = {
|
||||
@@ -166,7 +167,7 @@ export default function Packages() {
|
||||
const resetPeriod = pkg.quota_reset_period || 'never';
|
||||
const isSubscription = resetPeriod !== 'never';
|
||||
const quotaDollars = pkg.quota_amount > 0 ? pkg.quota_amount / Q : 0;
|
||||
const equiv = calcOfficialEquiv(enabledModels, quotaDollars);
|
||||
const equivList = calcOfficialEquivList(enabledModels, quotaDollars);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -203,13 +204,13 @@ export default function Packages() {
|
||||
</div>
|
||||
|
||||
{/* Official Equiv Banner */}
|
||||
{equiv && equiv.equivDollars > quotaDollars && (
|
||||
{equivList.length > 0 && (
|
||||
<div className="bg-gradient-to-r from-amber-500/10 to-orange-500/10 border border-amber-500/20 rounded-xl p-3 mb-4">
|
||||
<p className="text-xs text-page-warning font-medium">
|
||||
🔥 {t('packages.officialEquiv', {
|
||||
model: equiv.label,
|
||||
amount: Math.round(equiv.equivDollars),
|
||||
})}
|
||||
🔥 <RotatingEquiv
|
||||
items={equivList}
|
||||
text={(item) => t('packages.officialEquiv', { model: item.label, amount: item.equivDollars })}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { useSite } from '../../context/SiteContext';
|
||||
import { getSiteModels, getSitePackages, Q } from '../../api';
|
||||
import { calcOfficialEquiv } from '../../utils/officialEquiv';
|
||||
import { calcOfficialEquivList } from '../../utils/officialEquiv';
|
||||
import RotatingEquiv from '../../components/bits/RotatingEquiv';
|
||||
import CountUp from '../../components/bits/CountUp';
|
||||
import FadeContent from '../../components/bits/FadeContent';
|
||||
|
||||
@@ -142,7 +143,7 @@ export default function ClaudeHome() {
|
||||
<div className="grid md:grid-cols-3 gap-4 max-w-4xl">
|
||||
{packages.filter(p => p.enabled).slice(0, 3).map((pkg, i) => {
|
||||
const quotaDollars = pkg.quota_amount > 0 ? pkg.quota_amount / Q : 0;
|
||||
const equiv = calcOfficialEquiv(enabledModels, quotaDollars);
|
||||
const equiv = calcOfficialEquivList(enabledModels, quotaDollars);
|
||||
return (
|
||||
<div key={pkg.id} className={`rounded-xl p-6 flex flex-col border transition-all ${
|
||||
i === 1 ? 'border-[#D97757]/30 bg-[#D97757]/[0.04]' : 'border-[#E8DDD0] hover:border-[#D9C5B2]'
|
||||
@@ -157,8 +158,8 @@ export default function ClaudeHome() {
|
||||
)}
|
||||
{pkg.duration > 0 && <p className="text-xs text-[#8B7D6E] mt-1">{t('home.days', { count: pkg.duration })}</p>}
|
||||
</div>
|
||||
{equiv && equiv.equivDollars > quotaDollars && (
|
||||
<p className="text-xs text-[#D97757] mt-2">🔥 {t('packages.officialEquiv', { model: equiv.label, amount: Math.round(equiv.equivDollars) })}</p>
|
||||
{equiv.length > 0 && (
|
||||
<p className="text-xs text-[#D97757] mt-2">🔥 <RotatingEquiv items={equiv} text={(item) => t('packages.officialEquiv', { model: item.label, amount: item.equivDollars })} /></p>
|
||||
)}
|
||||
<Link to={user ? '/packages' : '/register'} className={`mt-4 py-2.5 rounded-full font-medium text-sm text-center transition-colors ${
|
||||
i === 1
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { useSite } from '../../context/SiteContext';
|
||||
import { getSiteModels, getSitePackages, Q } from '../../api';
|
||||
import { calcOfficialEquiv } from '../../utils/officialEquiv';
|
||||
import { calcOfficialEquivList } from '../../utils/officialEquiv';
|
||||
import RotatingEquiv from '../../components/bits/RotatingEquiv';
|
||||
import CountUp from '../../components/bits/CountUp';
|
||||
import FadeContent from '../../components/bits/FadeContent';
|
||||
|
||||
@@ -155,7 +156,7 @@ export default function CleanHome() {
|
||||
<div className="grid md:grid-cols-3 gap-4 max-w-4xl">
|
||||
{packages.filter(p => p.enabled).slice(0, 3).map((pkg, i) => {
|
||||
const quotaDollars = pkg.quota_amount > 0 ? pkg.quota_amount / Q : 0;
|
||||
const equiv = calcOfficialEquiv(enabledModels, quotaDollars);
|
||||
const equiv = calcOfficialEquivList(enabledModels, quotaDollars);
|
||||
return (
|
||||
<div key={pkg.id} className={`rounded-xl p-6 flex flex-col border transition-all ${
|
||||
i === 1 ? 'border-blue-200 bg-blue-50/50 shadow-sm' : 'border-gray-100 hover:border-gray-200'
|
||||
@@ -170,8 +171,8 @@ export default function CleanHome() {
|
||||
)}
|
||||
{pkg.duration > 0 && <p className="text-xs text-gray-500 mt-1">{t('home.days', { count: pkg.duration })}</p>}
|
||||
</div>
|
||||
{equiv && equiv.equivDollars > quotaDollars && (
|
||||
<p className="text-xs text-amber-600 mt-2">🔥 {t('packages.officialEquiv', { model: equiv.label, amount: Math.round(equiv.equivDollars) })}</p>
|
||||
{equiv.length > 0 && (
|
||||
<p className="text-xs text-amber-600 mt-2">🔥 <RotatingEquiv items={equiv} text={(item) => t('packages.officialEquiv', { model: item.label, amount: item.equivDollars })} /></p>
|
||||
)}
|
||||
<Link to={user ? '/packages' : '/register'} className={`mt-4 py-2.5 rounded-lg font-medium text-sm text-center transition-colors ${
|
||||
i === 1
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { useSite } from '../../context/SiteContext';
|
||||
import { getSiteModels, getSitePackages, Q } from '../../api';
|
||||
import { calcOfficialEquiv } from '../../utils/officialEquiv';
|
||||
import { calcOfficialEquivList } from '../../utils/officialEquiv';
|
||||
import RotatingEquiv from '../../components/bits/RotatingEquiv';
|
||||
import CountUp from '../../components/bits/CountUp';
|
||||
import FadeContent from '../../components/bits/FadeContent';
|
||||
|
||||
@@ -154,7 +155,7 @@ export default function CorporateHome() {
|
||||
<div className="grid md:grid-cols-3 gap-5 max-w-4xl">
|
||||
{packages.filter(p => p.enabled).slice(0, 3).map((pkg, i) => {
|
||||
const quotaDollars = pkg.quota_amount > 0 ? pkg.quota_amount / Q : 0;
|
||||
const equiv = calcOfficialEquiv(enabledModels, quotaDollars);
|
||||
const equiv = calcOfficialEquivList(enabledModels, quotaDollars);
|
||||
return (
|
||||
<div key={pkg.id} className={`rounded-xl p-6 flex flex-col border transition-all ${
|
||||
i === 1 ? 'border-slate-900 shadow-sm' : 'border-slate-200 hover:border-slate-300'
|
||||
@@ -169,8 +170,8 @@ export default function CorporateHome() {
|
||||
)}
|
||||
{pkg.duration > 0 && <p className="text-xs text-slate-500 mt-1">{t('home.days', { count: pkg.duration })}</p>}
|
||||
</div>
|
||||
{equiv && equiv.equivDollars > quotaDollars && (
|
||||
<p className="text-xs text-amber-600 mt-2">🔥 {t('packages.officialEquiv', { model: equiv.label, amount: Math.round(equiv.equivDollars) })}</p>
|
||||
{equiv.length > 0 && (
|
||||
<p className="text-xs text-amber-600 mt-2">🔥 <RotatingEquiv items={equiv} text={(item) => t('packages.officialEquiv', { model: item.label, amount: item.equivDollars })} /></p>
|
||||
)}
|
||||
<Link to={user ? '/packages' : '/register'} className={`mt-4 py-2.5 rounded-lg font-medium text-sm text-center transition-colors ${
|
||||
i === 1
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { useSite } from '../../context/SiteContext';
|
||||
import { getSiteModels, getSitePackages, Q } from '../../api';
|
||||
import { calcOfficialEquiv } from '../../utils/officialEquiv';
|
||||
import { calcOfficialEquivList } from '../../utils/officialEquiv';
|
||||
import RotatingEquiv from '../../components/bits/RotatingEquiv';
|
||||
import CountUp from '../../components/bits/CountUp';
|
||||
import FadeContent from '../../components/bits/FadeContent';
|
||||
|
||||
@@ -145,7 +146,7 @@ export default function DarkHome() {
|
||||
<div className="grid md:grid-cols-3 gap-4 max-w-4xl">
|
||||
{packages.filter(p => p.enabled).slice(0, 3).map((pkg, i) => {
|
||||
const quotaDollars = pkg.quota_amount > 0 ? pkg.quota_amount / Q : 0;
|
||||
const equiv = calcOfficialEquiv(enabledModels, quotaDollars);
|
||||
const equiv = calcOfficialEquivList(enabledModels, quotaDollars);
|
||||
return (
|
||||
<div key={pkg.id} className={`rounded-xl p-6 flex flex-col border transition-colors ${
|
||||
i === 1 ? 'border-emerald-500/25 bg-emerald-500/[0.04]' : 'border-neutral-800/60 hover:border-neutral-700'
|
||||
@@ -160,8 +161,8 @@ export default function DarkHome() {
|
||||
)}
|
||||
{pkg.duration > 0 && <p className="text-xs text-neutral-600 mt-1">{t('home.days', { count: pkg.duration })}</p>}
|
||||
</div>
|
||||
{equiv && equiv.equivDollars > quotaDollars && (
|
||||
<p className="text-xs text-amber-300/80 mt-2">🔥 {t('packages.officialEquiv', { model: equiv.label, amount: Math.round(equiv.equivDollars) })}</p>
|
||||
{equiv.length > 0 && (
|
||||
<p className="text-xs text-amber-300/80 mt-2">🔥 <RotatingEquiv items={equiv} text={(item) => t('packages.officialEquiv', { model: item.label, amount: item.equivDollars })} /></p>
|
||||
)}
|
||||
<Link to={user ? '/packages' : '/register'} className={`mt-4 py-2.5 rounded-lg font-medium text-sm text-center transition-colors ${
|
||||
i === 1
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { useSite } from '../../context/SiteContext';
|
||||
import { getSiteModels, getSitePackages, Q } from '../../api';
|
||||
import { calcOfficialEquiv } from '../../utils/officialEquiv';
|
||||
import { calcOfficialEquivList } from '../../utils/officialEquiv';
|
||||
import RotatingEquiv from '../../components/bits/RotatingEquiv';
|
||||
import CountUp from '../../components/bits/CountUp';
|
||||
import FadeContent from '../../components/bits/FadeContent';
|
||||
|
||||
@@ -154,7 +155,7 @@ export default function DefaultHome() {
|
||||
<div className="grid md:grid-cols-3 gap-4 max-w-4xl">
|
||||
{packages.filter(p => p.enabled).slice(0, 3).map((pkg, i) => {
|
||||
const quotaDollars = pkg.quota_amount > 0 ? pkg.quota_amount / Q : 0;
|
||||
const equiv = calcOfficialEquiv(enabledModels, quotaDollars);
|
||||
const equiv = calcOfficialEquivList(enabledModels, quotaDollars);
|
||||
return (
|
||||
<div key={pkg.id} className={`rounded-2xl p-6 flex flex-col border transition-colors ${
|
||||
i === 1 ? 'bg-white/[0.04] border-white/[0.12]' : 'bg-white/[0.02] border-white/[0.06] hover:border-white/[0.1]'
|
||||
@@ -169,8 +170,8 @@ export default function DefaultHome() {
|
||||
)}
|
||||
{pkg.duration > 0 && <p className="text-xs text-neutral-600 mt-1">{t('home.days', { count: pkg.duration })}</p>}
|
||||
</div>
|
||||
{equiv && equiv.equivDollars > quotaDollars && (
|
||||
<p className="text-xs text-amber-300/80 mt-2">🔥 {t('packages.officialEquiv', { model: equiv.label, amount: Math.round(equiv.equivDollars) })}</p>
|
||||
{equiv.length > 0 && (
|
||||
<p className="text-xs text-amber-300/80 mt-2">🔥 <RotatingEquiv items={equiv} text={(item) => t('packages.officialEquiv', { model: item.label, amount: item.equivDollars })} /></p>
|
||||
)}
|
||||
<Link to={user ? '/packages' : '/register'} className={`mt-4 py-2.5 rounded-xl font-medium text-sm text-center transition-colors ${
|
||||
i === 1
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { useSite } from '../../context/SiteContext';
|
||||
import { getSiteModels, getSitePackages, Q } from '../../api';
|
||||
import { calcOfficialEquiv } from '../../utils/officialEquiv';
|
||||
import { calcOfficialEquivList } from '../../utils/officialEquiv';
|
||||
import RotatingEquiv from '../../components/bits/RotatingEquiv';
|
||||
import CountUp from '../../components/bits/CountUp';
|
||||
import FadeContent from '../../components/bits/FadeContent';
|
||||
|
||||
@@ -140,7 +141,7 @@ export default function MinimalHome() {
|
||||
<div className="grid md:grid-cols-3 gap-4 max-w-4xl">
|
||||
{packages.filter(p => p.enabled).slice(0, 3).map((pkg) => {
|
||||
const quotaDollars = pkg.quota_amount > 0 ? pkg.quota_amount / Q : 0;
|
||||
const equiv = calcOfficialEquiv(enabledModels, quotaDollars);
|
||||
const equiv = calcOfficialEquivList(enabledModels, quotaDollars);
|
||||
return (
|
||||
<div key={pkg.id} className="rounded-xl p-6 flex flex-col border border-neutral-800/60 hover:border-neutral-700 transition-colors">
|
||||
<h3 className="text-base font-semibold text-white">{pkg.name}</h3>
|
||||
@@ -152,8 +153,8 @@ export default function MinimalHome() {
|
||||
)}
|
||||
{pkg.duration > 0 && <p className="text-xs text-neutral-600 mt-1">{t('home.days', { count: pkg.duration })}</p>}
|
||||
</div>
|
||||
{equiv && equiv.equivDollars > quotaDollars && (
|
||||
<p className="text-xs text-amber-300/80 mt-2">🔥 {t('packages.officialEquiv', { model: equiv.label, amount: Math.round(equiv.equivDollars) })}</p>
|
||||
{equiv.length > 0 && (
|
||||
<p className="text-xs text-amber-300/80 mt-2">🔥 <RotatingEquiv items={equiv} text={(item) => t('packages.officialEquiv', { model: item.label, amount: item.equivDollars })} /></p>
|
||||
)}
|
||||
<Link to={user ? '/packages' : '/register'} className="mt-4 py-2.5 rounded-lg bg-neutral-800 text-white font-medium text-sm text-center hover:bg-neutral-700 transition-colors">
|
||||
{user ? t('home.subscribe') : t('home.getStarted')}
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { useSite } from '../../context/SiteContext';
|
||||
import { getSiteModels, getSitePackages, Q } from '../../api';
|
||||
import { calcOfficialEquiv } from '../../utils/officialEquiv';
|
||||
import { calcOfficialEquivList } from '../../utils/officialEquiv';
|
||||
import RotatingEquiv from '../../components/bits/RotatingEquiv';
|
||||
import CountUp from '../../components/bits/CountUp';
|
||||
import FadeContent from '../../components/bits/FadeContent';
|
||||
|
||||
@@ -160,7 +161,7 @@ export default function StarterHome() {
|
||||
<div className="grid md:grid-cols-3 gap-4 max-w-4xl">
|
||||
{packages.filter(p => p.enabled).slice(0, 3).map((pkg, i) => {
|
||||
const quotaDollars = pkg.quota_amount > 0 ? pkg.quota_amount / Q : 0;
|
||||
const equiv = calcOfficialEquiv(enabledModels, quotaDollars);
|
||||
const equiv = calcOfficialEquivList(enabledModels, quotaDollars);
|
||||
return (
|
||||
<div key={pkg.id} className={`rounded-xl p-6 flex flex-col border transition-colors ${
|
||||
i === 1 ? 'border-brand-500/30 bg-brand-500/[0.04]' : 'border-neutral-800/60 hover:border-neutral-700'
|
||||
@@ -175,8 +176,8 @@ export default function StarterHome() {
|
||||
)}
|
||||
{pkg.duration > 0 && <p className="text-xs text-neutral-600 mt-1">{t('home.days', { count: pkg.duration })}</p>}
|
||||
</div>
|
||||
{equiv && equiv.equivDollars > quotaDollars && (
|
||||
<p className="text-xs text-amber-300/80 mt-2">🔥 {t('packages.officialEquiv', { model: equiv.label, amount: Math.round(equiv.equivDollars) })}</p>
|
||||
{equiv.length > 0 && (
|
||||
<p className="text-xs text-amber-300/80 mt-2">🔥 <RotatingEquiv items={equiv} text={(item) => t('packages.officialEquiv', { model: item.label, amount: item.equivDollars })} /></p>
|
||||
)}
|
||||
<Link to={user ? '/packages' : '/register'} className={`mt-4 py-2.5 rounded-lg font-medium text-sm text-center transition-colors ${
|
||||
i === 1
|
||||
|
||||
+26
-16
@@ -2,6 +2,7 @@ import { Q } from '../api';
|
||||
|
||||
// Official prices (input $/1M tokens) for reference models
|
||||
const OFFICIAL_PRICES = [
|
||||
// Claude
|
||||
{ pattern: 'claude-4-sonnet', label: 'Claude Sonnet 4', inputPerMtok: 3 },
|
||||
{ pattern: 'claude-sonnet-4', label: 'Claude Sonnet 4', inputPerMtok: 3 },
|
||||
{ pattern: 'claude-4.5-sonnet', label: 'Claude 4.5 Sonnet', inputPerMtok: 3 },
|
||||
@@ -13,26 +14,31 @@ const OFFICIAL_PRICES = [
|
||||
{ pattern: 'claude-4.6-opus', label: 'Claude Opus 4.6', inputPerMtok: 5 },
|
||||
{ pattern: 'claude-3.5-haiku', label: 'Claude 3.5 Haiku', inputPerMtok: 0.8 },
|
||||
{ pattern: 'claude-3-5-haiku', label: 'Claude 3.5 Haiku', inputPerMtok: 0.8 },
|
||||
// OpenAI
|
||||
{ pattern: 'gpt-5.1', label: 'GPT-5.1', inputPerMtok: 1.25 },
|
||||
{ pattern: 'gpt-5.2', label: 'GPT-5.2', inputPerMtok: 1.75 },
|
||||
{ pattern: 'gpt-5.3', label: 'GPT-5.3', inputPerMtok: 1.75 },
|
||||
{ pattern: 'gpt-5.4', label: 'GPT-5.4', inputPerMtok: 2.5 },
|
||||
{ pattern: 'gpt-4o', label: 'GPT-4o', inputPerMtok: 2.5 },
|
||||
{ pattern: 'gpt-4.1', label: 'GPT-4.1', inputPerMtok: 2 },
|
||||
// Gemini
|
||||
{ pattern: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro', inputPerMtok: 1.25 },
|
||||
{ pattern: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash', inputPerMtok: 0.15 },
|
||||
{ pattern: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash', inputPerMtok: 0.1 },
|
||||
{ pattern: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro', inputPerMtok: 1.25 },
|
||||
];
|
||||
|
||||
/**
|
||||
* Find the best official equivalent for a given quota.
|
||||
* Picks the model with the highest ratio (biggest discount advantage).
|
||||
*
|
||||
* @param {Array} models - site models with input_price ($/1K tokens)
|
||||
* @param {number} quotaDollars - package quota in USD
|
||||
* @returns {{ label: string, equivDollars: number } | null}
|
||||
* Find ALL official equivalents for a given quota.
|
||||
* Returns array of { label, equivDollars } sorted by equivDollars desc.
|
||||
* Only includes results where equivDollars > quotaDollars.
|
||||
* Deduplicates by label (keeps best ratio per model label).
|
||||
*/
|
||||
export function calcOfficialEquiv(models, quotaDollars) {
|
||||
if (!models || models.length === 0 || quotaDollars <= 0) return null;
|
||||
export function calcOfficialEquivList(models, quotaDollars) {
|
||||
if (!models || models.length === 0 || quotaDollars <= 0) return [];
|
||||
|
||||
let bestResult = null;
|
||||
// label -> best equivDollars
|
||||
const bestByLabel = {};
|
||||
|
||||
for (const m of models) {
|
||||
const siteInputPrice = Number(m.input_price);
|
||||
@@ -46,21 +52,25 @@ export function calcOfficialEquiv(models, quotaDollars) {
|
||||
const ratio = official.inputPerMtok / sitePricePerMtok;
|
||||
const equivDollars = quotaDollars * ratio;
|
||||
|
||||
if (!bestResult || equivDollars > bestResult.equivDollars) {
|
||||
bestResult = { label: official.label, equivDollars, ratio };
|
||||
if (equivDollars > quotaDollars) {
|
||||
if (!bestByLabel[official.label] || equivDollars > bestByLabel[official.label]) {
|
||||
bestByLabel[official.label] = equivDollars;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestResult;
|
||||
return Object.entries(bestByLabel)
|
||||
.map(([label, equivDollars]) => ({ label, equivDollars: Math.round(equivDollars) }))
|
||||
.sort((a, b) => b.equivDollars - a.equivDollars);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience: compute equiv from raw quota units + models.
|
||||
* Legacy single-result API (kept for compatibility).
|
||||
*/
|
||||
export function calcPkgEquiv(models, quotaAmount) {
|
||||
const quotaDollars = quotaAmount > 0 ? quotaAmount / Q : 0;
|
||||
return calcOfficialEquiv(models, quotaDollars);
|
||||
export function calcOfficialEquiv(models, quotaDollars) {
|
||||
const list = calcOfficialEquivList(models, quotaDollars);
|
||||
return list.length > 0 ? list[0] : null;
|
||||
}
|
||||
Reference in new issue
Block a user