mirror of
https://github.com/coaidev/coai.git
synced 2025-05-21 14:00:13 +09:00
feat: provide external display of model prices
This commit is contained in:
parent
d0b5c90df9
commit
b1eea1cac8
@ -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[];
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -247,6 +247,10 @@
|
||||
font-size: 12px;
|
||||
margin-bottom: 0.25rem;
|
||||
|
||||
&.pro {
|
||||
color: hsl(var(--gold)) !important;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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`} />
|
||||
|
@ -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);
|
||||
|
@ -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": "模型市场",
|
||||
|
@ -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",
|
||||
|
@ -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": "モデルマーケット",
|
||||
|
@ -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": "Рынок моделей",
|
||||
|
@ -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));
|
||||
|
Loading…
Reference in New Issue
Block a user