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 chargeTypes = [nonBilling, timesBilling, tokenBilling];
export type ChargeProps = {
id: number;
models: string[];
export type ChargeBaseProps = {
type: string;
anonymous: boolean;
input: number;
output: number;
};
export type ChargeProps = ChargeBaseProps & {
id: number;
models: string[];
};

View File

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

View File

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

View File

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

View File

@ -3,11 +3,14 @@ import { Input } from "@/components/ui/input.tsx";
import {
ChevronLeft,
ChevronRight,
Cloud,
DownloadCloud,
GripVertical,
Link,
Plus,
Search,
Trash2,
UploadCloud,
X,
} from "lucide-react";
import React, { useMemo, useState } from "react";
@ -50,6 +53,12 @@ import { useMobile } from "@/utils/device.ts";
import Tips from "@/components/Tips.tsx";
import { includingModelFromPlan } from "@/conf/subscription.tsx";
import { subscriptionDataSelector } from "@/store/globals.ts";
import {
ChargeBaseProps,
nonBilling,
timesBilling,
tokenBilling,
} from "@/admin/charge.ts";
type SearchBarProps = {
value: string;
@ -97,6 +106,46 @@ type ModelProps = React.DetailedHTMLProps<
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({
model,
className,
@ -132,7 +181,10 @@ function ModelItem({
return isUrl(model.avatar) ? model.avatar : `/icons/${model.avatar}`;
}, [model]);
const tags = useMemo((): string[] => getTags(model), [model]);
const tags = useMemo(
(): string[] => getTags(model).filter((tag) => tag !== "free"),
[model],
);
return (
<div
@ -199,6 +251,7 @@ function ModelItem({
</span>
);
})}
{model.price && <PriceTag {...model.price} pro={pro} />}
</div>
</div>
<div className={`grow`} />

View File

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

View File

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

View File

@ -46,7 +46,12 @@
"image-generation": "Image Generation",
"multi-modal": "Multi Modal",
"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": {
"title": "Model Market",

View File

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

View File

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

View File

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