From b1eea1cac8474e24e63ceeb24ebe44efe2643c0b Mon Sep 17 00:00:00 2001 From: Zhang Minghan Date: Sat, 3 Feb 2024 18:20:52 +0800 Subject: [PATCH] feat: provide external display of model prices --- app/src/admin/charge.ts | 9 ++-- app/src/api/types.ts | 3 ++ app/src/assets/pages/home.less | 4 ++ app/src/components/app/AppProvider.tsx | 11 +++-- app/src/components/home/ModelMarket.tsx | 55 ++++++++++++++++++++++++- app/src/conf/storage.ts | 1 + app/src/resources/i18n/cn.json | 7 +++- app/src/resources/i18n/en.json | 7 +++- app/src/resources/i18n/ja.json | 7 +++- app/src/resources/i18n/ru.json | 7 +++- app/src/routes/admin/Market.tsx | 11 +++-- 11 files changed, 106 insertions(+), 16 deletions(-) diff --git a/app/src/admin/charge.ts b/app/src/admin/charge.ts index e3803b0..1697525 100644 --- a/app/src/admin/charge.ts +++ b/app/src/admin/charge.ts @@ -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[]; +}; diff --git a/app/src/api/types.ts b/app/src/api/types.ts index 4ee473f..0417d26 100644 --- a/app/src/api/types.ts +++ b/app/src/api/types.ts @@ -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; diff --git a/app/src/assets/pages/home.less b/app/src/assets/pages/home.less index 0eaea17..7d34846 100644 --- a/app/src/assets/pages/home.less +++ b/app/src/assets/pages/home.less @@ -247,6 +247,10 @@ font-size: 12px; margin-bottom: 0.25rem; + &.pro { + color: hsl(var(--gold)) !important; + } + &:last-child { margin-right: 0; } diff --git a/app/src/components/app/AppProvider.tsx b/app/src/components/app/AppProvider.tsx index 2c504b4..3ab9d1e 100644 --- a/app/src/components/app/AppProvider.tsx +++ b/app/src/components/app/AppProvider.tsx @@ -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)); diff --git a/app/src/components/home/ModelMarket.tsx b/app/src/components/home/ModelMarket.tsx index 46de7fa..5b9e931 100644 --- a/app/src/components/home/ModelMarket.tsx +++ b/app/src/components/home/ModelMarket.tsx @@ -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; }; +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 ( + + + {t("tag.badges.non-billing")} + + ); + case timesBilling: + return ( + + + {t("tag.badges.times-billing", { price: output })} + + ); + case tokenBilling: + return ( + <> + + + {input.toFixed(2)} / 1k tokens + + + + {output.toFixed(2)} / 1k tokens + + + ); + } +} + 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 (
); })} + {model.price && }
diff --git a/app/src/conf/storage.ts b/app/src/conf/storage.ts index 03f66f1..dfb25cd 100644 --- a/app/src/conf/storage.ts +++ b/app/src/conf/storage.ts @@ -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); diff --git a/app/src/resources/i18n/cn.json b/app/src/resources/i18n/cn.json index 4854e16..7a9d73a 100644 --- a/app/src/resources/i18n/cn.json +++ b/app/src/resources/i18n/cn.json @@ -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": "模型市场", diff --git a/app/src/resources/i18n/en.json b/app/src/resources/i18n/en.json index c8e23ca..31fe865 100644 --- a/app/src/resources/i18n/en.json +++ b/app/src/resources/i18n/en.json @@ -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", diff --git a/app/src/resources/i18n/ja.json b/app/src/resources/i18n/ja.json index fc7c81d..b76aead 100644 --- a/app/src/resources/i18n/ja.json +++ b/app/src/resources/i18n/ja.json @@ -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": "モデルマーケット", diff --git a/app/src/resources/i18n/ru.json b/app/src/resources/i18n/ru.json index 720b074..8791967 100644 --- a/app/src/resources/i18n/ru.json +++ b/app/src/resources/i18n/ru.json @@ -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": "Рынок моделей", diff --git a/app/src/routes/admin/Market.tsx b/app/src/routes/admin/Market.tsx index ae3cab2..b7d0fe3 100644 --- a/app/src/routes/admin/Market.tsx +++ b/app/src/routes/admin/Market.tsx @@ -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));