|
import * as React from "react"; |
|
import { |
|
Table, |
|
TableHeader, |
|
TableBody, |
|
TableRow, |
|
TableHead, |
|
TableCell, |
|
} from "@/components/ui/table"; |
|
import { MultiSelect } from "@/components/ui/multi-select"; |
|
import { FlattenedModel, Provider } from "@/App"; |
|
|
|
interface PricingTableProps { |
|
data: FlattenedModel[]; |
|
providers: Provider[]; |
|
selectedProviders: string[]; |
|
selectedModels: string[]; |
|
onProviderChange: (values: string[]) => void; |
|
onModelChange: (values: string[]) => void; |
|
comparisonModels: string[]; |
|
inputTokens: number; |
|
outputTokens: number; |
|
tokenCalculation: string; |
|
requestSort: (key: keyof FlattenedModel) => void; |
|
sortConfig: { |
|
key: keyof FlattenedModel; |
|
direction: string; |
|
} | null; |
|
|
|
} |
|
|
|
export const PricingTable: React.FC<PricingTableProps> = ({ |
|
data, |
|
providers, |
|
selectedProviders, |
|
selectedModels, |
|
onProviderChange, |
|
onModelChange, |
|
comparisonModels, |
|
inputTokens, |
|
outputTokens, |
|
tokenCalculation, |
|
requestSort, |
|
sortConfig, |
|
|
|
}) => { |
|
const calculatePrice = (price: number, tokens: number): number => { |
|
let multiplier = 1; |
|
if (tokenCalculation === "thousand") multiplier = 1e-3; |
|
else if (tokenCalculation === "unit") multiplier = 1e-6; |
|
else if (tokenCalculation === "billion") multiplier = 1e3; |
|
return price * tokens * multiplier; |
|
}; |
|
|
|
const calculateComparison = (modelPrice: number, comparisonPrice: number): string => { |
|
if (comparisonPrice === 0) return "∞"; |
|
return (((modelPrice - comparisonPrice) / comparisonPrice) * 100).toFixed(2); |
|
}; |
|
|
|
const getModelsForSelectedProviders = () => { |
|
const flatModels = providers.flatMap((provider) => |
|
provider.models.map((model) => ({ |
|
label: model.name, |
|
value: model.name, |
|
provider: provider.provider, |
|
})) |
|
); |
|
|
|
const filtered = flatModels.filter((m) => |
|
!selectedProviders.length || selectedProviders.includes(m.provider) |
|
); |
|
|
|
|
|
|
|
return Array.from(new Map(filtered.map((m) => [m.value, m])).values()); |
|
}; |
|
|
|
return ( |
|
<Table> |
|
<TableHeader> |
|
<TableRow> |
|
<TableHead> |
|
<button onClick={() => requestSort("provider")}> |
|
Provider {sortConfig?.key === "provider" ? (sortConfig.direction === "ascending" ? "▲" : "▼") : null} |
|
</button> |
|
</TableHead> |
|
<TableHead> |
|
<button onClick={() => requestSort("name")}> |
|
Model {sortConfig?.key === "name" ? (sortConfig.direction === "ascending" ? "▲" : "▼") : null} |
|
</button> |
|
</TableHead> |
|
<TableHead> |
|
<button onClick={() => requestSort("inputPrice")}> |
|
Input Price (million tokens) {sortConfig?.key === "inputPrice" ? (sortConfig.direction === "ascending" ? "▲" : "▼") : null} |
|
</button> |
|
</TableHead> |
|
<TableHead> |
|
<button onClick={() => requestSort("outputPrice")}> |
|
Output Price (million tokens) {sortConfig?.key === "outputPrice" ? (sortConfig.direction === "ascending" ? "▲" : "▼") : null} |
|
</button> |
|
</TableHead> |
|
<TableHead>Total Price (per {tokenCalculation} tokens)</TableHead> |
|
{comparisonModels.map((model) => ( |
|
<TableHead key={model} colSpan={2}> |
|
Compared to {model} |
|
</TableHead> |
|
))} |
|
</TableRow> |
|
<TableRow> |
|
<TableHead> |
|
<MultiSelect |
|
options={providers.map((p) => ({ label: p.provider, value: p.provider }))} |
|
defaultValue={selectedProviders} |
|
onValueChange={onProviderChange} |
|
/> |
|
</TableHead> |
|
<TableHead> |
|
<MultiSelect |
|
options={getModelsForSelectedProviders()} |
|
defaultValue={selectedModels} |
|
onValueChange={onModelChange} |
|
/> |
|
</TableHead> |
|
<TableHead /> |
|
<TableHead /> |
|
<TableHead /> |
|
{comparisonModels.flatMap((model) => [ |
|
<TableHead key={`${model}-input`}>Input</TableHead>, |
|
<TableHead key={`${model}-output`}>Output</TableHead>, |
|
])} |
|
</TableRow> |
|
</TableHeader> |
|
<TableBody> |
|
{data.map((item) => ( |
|
<TableRow key={`${item.provider}-${item.name}`}> |
|
<TableCell> |
|
<a href={item.uri} className="underline"> |
|
{item.provider} |
|
</a> |
|
</TableCell> |
|
<TableCell>{item.name}</TableCell> |
|
<TableCell>{item.inputPrice.toFixed(2)}</TableCell> |
|
<TableCell>{item.outputPrice.toFixed(2)}</TableCell> |
|
<TableCell className="font-bold"> |
|
${( |
|
calculatePrice(item.inputPrice, inputTokens) + |
|
calculatePrice(item.outputPrice, outputTokens) |
|
).toFixed(2)} |
|
</TableCell> |
|
{comparisonModels.flatMap((model) => { |
|
const [comparisonProvider, comparisonModelName] = model.split(":"); |
|
const comparisonModel = providers |
|
.find((p) => p.provider === comparisonProvider) |
|
?.models.find((m) => m.name === comparisonModelName); |
|
|
|
if (!comparisonModel) return [<TableCell key="missing">–</TableCell>, <TableCell key="missing2">–</TableCell>]; |
|
|
|
const inputDelta = calculateComparison(item.inputPrice, comparisonModel.inputPrice); |
|
const outputDelta = calculateComparison(item.outputPrice, comparisonModel.outputPrice); |
|
|
|
return [ |
|
<TableCell key={`${model}-input`} className={parseFloat(inputDelta) < 0 ? "bg-green-100" : parseFloat(inputDelta) > 0 ? "bg-red-100" : ""}> |
|
{`${item.provider}:${item.name}` === model ? "0.00%" : `${inputDelta}%`} |
|
</TableCell>, |
|
<TableCell key={`${model}-output`} className={parseFloat(outputDelta) < 0 ? "bg-green-100" : parseFloat(outputDelta) > 0 ? "bg-red-100" : ""}> |
|
{`${item.provider}:${item.name}` === model ? "0.00%" : `${outputDelta}%`} |
|
</TableCell>, |
|
]; |
|
})} |
|
</TableRow> |
|
))} |
|
</TableBody> |
|
</Table> |
|
); |
|
}; |
|
|