feat: provide external display of model prices

This commit is contained in:
Zhang Minghan 2024-02-03 18:20:52 +08:00
parent d0b5c90df9
commit b1eea1cac8
11 changed files with 106 additions and 16 deletions

View File

@ -5,11 +5,14 @@ export const nonBilling = "non-billing";
export const defaultChargeType = tokenBilling; export const defaultChargeType = tokenBilling;
export const chargeTypes = [nonBilling, timesBilling, tokenBilling]; export const chargeTypes = [nonBilling, timesBilling, tokenBilling];
export type ChargeProps = { export type ChargeBaseProps = {
id: number;
models: string[];
type: string; type: string;
anonymous: boolean; anonymous: boolean;
input: number; input: number;
output: number; output: number;
}; };
export type ChargeProps = ChargeBaseProps & {
id: number;
models: string[];
};

View File

@ -1,4 +1,5 @@
import { Conversation } from "./conversation.ts"; import { Conversation } from "./conversation.ts";
import { ChargeBaseProps } from "@/admin/charge.ts";
export type Message = { export type Message = {
role: string; role: string;
@ -19,6 +20,8 @@ export type Model = {
high_context: boolean; high_context: boolean;
avatar: string; avatar: string;
tag?: string[]; tag?: string[];
price?: ChargeBaseProps;
}; };
export type Id = number; export type Id = number;

View File

@ -247,6 +247,10 @@
font-size: 12px; font-size: 12px;
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
&.pro {
color: hsl(var(--gold)) !important;
}
&:last-child { &:last-child {
margin-right: 0; margin-right: 0;
} }

View File

@ -37,11 +37,14 @@ function AppProvider() {
const charge = await getApiCharge(); const charge = await getApiCharge();
market.forEach((item: Model) => { market.forEach((item: Model) => {
const obj = charge.find((i: ChargeProps) => i.models.includes(item.id)); const instance = charge.find((i: ChargeProps) =>
if (!obj) return; i.models.includes(item.id),
);
if (!instance) return;
item.free = obj.type === nonBilling; item.free = instance.type === nonBilling;
item.auth = !item.free || !obj.anonymous; item.auth = !item.free || !instance.anonymous;
item.price = { ...instance };
}); });
resetJsArray(supportModels, loadPreferenceModels(market)); resetJsArray(supportModels, loadPreferenceModels(market));

View File

@ -3,11 +3,14 @@ import { Input } from "@/components/ui/input.tsx";
import { import {
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
Cloud,
DownloadCloud,
GripVertical, GripVertical,
Link, Link,
Plus, Plus,
Search, Search,
Trash2, Trash2,
UploadCloud,
X, X,
} from "lucide-react"; } from "lucide-react";
import React, { useMemo, useState } from "react"; import React, { useMemo, useState } from "react";
@ -50,6 +53,12 @@ import { useMobile } from "@/utils/device.ts";
import Tips from "@/components/Tips.tsx"; import Tips from "@/components/Tips.tsx";
import { includingModelFromPlan } from "@/conf/subscription.tsx"; import { includingModelFromPlan } from "@/conf/subscription.tsx";
import { subscriptionDataSelector } from "@/store/globals.ts"; import { subscriptionDataSelector } from "@/store/globals.ts";
import {
ChargeBaseProps,
nonBilling,
timesBilling,
tokenBilling,
} from "@/admin/charge.ts";
type SearchBarProps = { type SearchBarProps = {
value: string; value: string;
@ -97,6 +106,46 @@ type ModelProps = React.DetailedHTMLProps<
forwardRef?: React.Ref<HTMLDivElement>; forwardRef?: React.Ref<HTMLDivElement>;
}; };
type PriceTagProps = ChargeBaseProps & {
pro: boolean;
};
function PriceTag({ type, input, output, pro }: PriceTagProps) {
const { t } = useTranslation();
const className = cn("flex flex-row tag-item", pro && "pro");
switch (type) {
case nonBilling:
return (
<span className={className}>
<Cloud className={`h-4 w-4 mr-1 translate-y-[1px]`} />
{t("tag.badges.non-billing")}
</span>
);
case timesBilling:
return (
<span className={className}>
<Cloud className={`h-4 w-4 mr-1 translate-y-[1px]`} />
{t("tag.badges.times-billing", { price: output })}
</span>
);
case tokenBilling:
return (
<>
<span className={className}>
<UploadCloud className={`h-4 w-4 mr-1 translate-y-[1px]`} />
{input.toFixed(2)} / 1k tokens
</span>
<span className={className}>
<DownloadCloud className={`h-4 w-4 mr-1 translate-y-[1px]`} />
{output.toFixed(2)} / 1k tokens
</span>
</>
);
}
}
function ModelItem({ function ModelItem({
model, model,
className, className,
@ -132,7 +181,10 @@ function ModelItem({
return isUrl(model.avatar) ? model.avatar : `/icons/${model.avatar}`; return isUrl(model.avatar) ? model.avatar : `/icons/${model.avatar}`;
}, [model]); }, [model]);
const tags = useMemo((): string[] => getTags(model), [model]); const tags = useMemo(
(): string[] => getTags(model).filter((tag) => tag !== "free"),
[model],
);
return ( return (
<div <div
@ -199,6 +251,7 @@ function ModelItem({
</span> </span>
); );
})} })}
{model.price && <PriceTag {...model.price} pro={pro} />}
</div> </div>
</div> </div>
<div className={`grow`} /> <div className={`grow`} />

View File

@ -56,6 +56,7 @@ export function parseOfflineModels(models: string): Model[] {
high_context: item.high_context || false, high_context: item.high_context || false,
avatar: item.avatar || "", avatar: item.avatar || "",
tag: item.tag || [], tag: item.tag || [],
price: item.price,
} as Model; } as Model;
}) })
.filter((item): item is Model => item !== null); .filter((item): item is Model => item !== null);

View File

@ -98,7 +98,12 @@
"image-generation": "绘图", "image-generation": "绘图",
"multi-modal": "多模态", "multi-modal": "多模态",
"fast": "快速", "fast": "快速",
"english-model": "英文模型" "english-model": "英文模型",
"badges": {
"non-billing": "免费",
"times-billing": "{{price}} / 次",
"token-billing": "输入 {{input}} / 1k tokens 输出 {{output}} / 1k tokens"
}
}, },
"market": { "market": {
"title": "模型市场", "title": "模型市场",

View File

@ -46,7 +46,12 @@
"image-generation": "Image Generation", "image-generation": "Image Generation",
"multi-modal": "Multi Modal", "multi-modal": "Multi Modal",
"fast": "Fast", "fast": "Fast",
"english-model": "English Model" "english-model": "English Model",
"badges": {
"non-billing": "FREE PASS",
"times-billing": "{{price}}/time",
"token-billing": "Input {{input}}/1k tokens Output {{output}}/1k tokens"
}
}, },
"market": { "market": {
"title": "Model Market", "title": "Model Market",

View File

@ -46,7 +46,12 @@
"image-generation": "画像生成", "image-generation": "画像生成",
"multi-modal": "マルチモーダル", "multi-modal": "マルチモーダル",
"fast": "高速", "fast": "高速",
"english-model": "英語モデル" "english-model": "英語モデル",
"badges": {
"non-billing": "無料",
"times-billing": "{{price }}/回",
"token-billing": "入力{{input }}/ 1 kトークン出力{{output }}/ 1 kトークン"
}
}, },
"market": { "market": {
"title": "モデルマーケット", "title": "モデルマーケット",

View File

@ -46,7 +46,12 @@
"image-generation": "Генерация изображений", "image-generation": "Генерация изображений",
"multi-modal": "Мульти Модальный", "multi-modal": "Мульти Модальный",
"fast": "Быстрый", "fast": "Быстрый",
"english-model": "Английская модель" "english-model": "Английская модель",
"badges": {
"non-billing": "Бесплатно",
"times-billing": "{{price}}/время",
"token-billing": "Ввод {{input}}/1k токенов Вывод {{output}}/1k токенов"
}
}, },
"market": { "market": {
"title": "Рынок моделей", "title": "Рынок моделей",

View File

@ -679,11 +679,14 @@ function Market() {
const charge = await getApiCharge(); const charge = await getApiCharge();
market.forEach((item: Model) => { market.forEach((item: Model) => {
const obj = charge.find((i: ChargeProps) => i.models.includes(item.id)); const instance = charge.find((i: ChargeProps) =>
if (!obj) return; i.models.includes(item.id),
);
if (!instance) return;
item.free = obj.type === nonBilling; item.free = instance.type === nonBilling;
item.auth = !item.free || !obj.anonymous; item.auth = !item.free || !instance.anonymous;
item.price = { ...instance };
}); });
resetJsArray(supportModels, loadPreferenceModels(market)); resetJsArray(supportModels, loadPreferenceModels(market));