mirror of
https://github.com/coaidev/coai.git
synced 2025-05-20 21:40:15 +09:00
feat: charge alpha
This commit is contained in:
parent
fa9f7da55e
commit
035e71b2f2
29
README.md
29
README.md
@ -9,7 +9,7 @@
|
|||||||
🚀 Powerful and beautiful **AI Aggregation** chat platform
|
🚀 Powerful and beautiful **AI Aggregation** chat platform
|
||||||
|
|
||||||
|
|
||||||
[官网](https://chatnio.net) | [开放文档](https://docs.chatnio.net) | [SDKs](https://docs.chatnio.net/kuai-su-kai-shi) | [QQ 群](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=YKcvGGlM03LYWlPk-iosqAqL4qHwOtBx&authKey=6cjCqRNKNuOUJltyo%2FNgmKm%2BS%2FUCtAyVHCnirHyxNuxGExUHsJRtlSaW1EeDxhNx&noverify=0&group_code=565902327)
|
[官网](https://chatnio.net) | [开放文档](https://docs.chatnio.net) | [SDKs](https://docs.chatnio.net/kuai-su-kai-shi) | [QQ 群](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=1mv1Y8SyxnQVvQCoqhmIgVTbwQmkNmvQ&authKey=5KUA9nJPR29nQwjbsYNknN2Fj6cKePkRes%2B1QZy84Dr4GHYVzcvb0yklxiMMNVJN&noverify=0&group_code=749482576)
|
||||||
|
|
||||||
[](https://stats.deeptrain.net)
|
[](https://stats.deeptrain.net)
|
||||||
|
|
||||||
@ -38,24 +38,26 @@
|
|||||||
- 🎁 Image generation
|
- 🎁 Image generation
|
||||||
11. 🔔 PWA 应用
|
11. 🔔 PWA 应用
|
||||||
- 🔔 PWA application
|
- 🔔 PWA application
|
||||||
12. ⚡ Token 计费系统
|
12. 📚 逆向工程模型支持
|
||||||
- ⚡ Token billing system
|
|
||||||
13. 📚 逆向工程模型支持
|
|
||||||
- 📚 Reverse engineering model support
|
- 📚 Reverse engineering model support
|
||||||
14. 🌏 国际化支持
|
13. 🌏 国际化支持
|
||||||
- 🌏 Internationalization support
|
- 🌏 Internationalization support
|
||||||
- 🇨🇳 简体中文
|
- 🇨🇳 简体中文
|
||||||
- 🇺🇸 English
|
- 🇺🇸 English
|
||||||
- 🇷🇺 Русский
|
- 🇷🇺 Русский
|
||||||
15. 🍎 主题切换
|
14. 🍎 主题切换
|
||||||
- 🍎 Theme switching
|
- 🍎 Theme switching
|
||||||
16. 🥪 Key 中转服务
|
15. 🥪 Key 中转服务
|
||||||
- 🥪 Key relay service
|
- 🥪 Key relay service
|
||||||
17. 🔨 多模型支持
|
16. 🔨 多模型支持
|
||||||
- 🔨 Multi-model support
|
- 🔨 Multi-model support
|
||||||
18. ⚙ 后台管理系统
|
17. ⚙ 后台管理系统 (仪表盘,用户管理,公告管理等)
|
||||||
- ⚙ Admin system
|
- ⚙ Admin system (dashboard, user management, announcement management, etc.)
|
||||||
19. 📂 文件上传功能 (支持 pdf, docx, pptx, xlsx, 音频, 图片等)
|
18. ⚒ 渠道管理 (多账号均衡负载,优先级调配,权重负载,模型映射,渠道状态管理)
|
||||||
|
- ⚒ Channel management (multi-account load balancing, priority allocation, weight load, model mapping, channel status management)
|
||||||
|
19. ⚡ 计费系统 (支持匿名计费,按次数计费,Token 弹性计费等方式)
|
||||||
|
- ⚡ Billing system (support anonymous billing, billing by number of times, Token billing, etc.)
|
||||||
|
20. 📂 文件上传功能 (支持 pdf, docx, pptx, xlsx, 音频, 图片等)
|
||||||
- 📂 File upload function (support pdf, docx, pptx, xlsx, audio, images, etc.)
|
- 📂 File upload function (support pdf, docx, pptx, xlsx, audio, images, etc.)
|
||||||
|
|
||||||
|
|
||||||
@ -185,11 +187,6 @@ Replace `https://api.openai.com` with `https://api.chatnio.net` and fill in the
|
|||||||
- 应用技术: PWA + HTTP2 + WebSocket + Stream Buffer
|
- 应用技术: PWA + HTTP2 + WebSocket + Stream Buffer
|
||||||
|
|
||||||
|
|
||||||
## 🎈 感谢 | Thanks
|
|
||||||
感谢这些开源项目提供的思路:
|
|
||||||
- ChatGPT 逆向工程: [go-chatgpt-api](https://github.com/linweiyuan/go-chatgpt-api)
|
|
||||||
- New Bing 逆向工程: [EdgeGPT](https://github.com/acheong08/EdgeGPT)
|
|
||||||
|
|
||||||
## 🎃 开发团队 | Team
|
## 🎃 开发团队 | Team
|
||||||
- [@ProgramZmh](https://github.com/zmh-program) (全栈开发)
|
- [@ProgramZmh](https://github.com/zmh-program) (全栈开发)
|
||||||
- [@Sh1n3zz](https://github.com/sh1n3zz) (全栈开发)
|
- [@Sh1n3zz](https://github.com/sh1n3zz) (全栈开发)
|
||||||
|
@ -54,7 +54,7 @@ func createChatRequest(conf globals.ChannelConfig, props *ChatProps, hook global
|
|||||||
Message: props.Message,
|
Message: props.Message,
|
||||||
Token: utils.Multi(
|
Token: utils.Multi(
|
||||||
props.Token == 0,
|
props.Token == 0,
|
||||||
utils.Multi(globals.IsFreeModel(model) && !props.Plan, utils.ToPtr(2500), nil),
|
utils.Multi(props.Infinity || props.Plan, nil, utils.ToPtr(2500)),
|
||||||
&props.Token,
|
&props.Token,
|
||||||
),
|
),
|
||||||
PresencePenalty: props.PresencePenalty,
|
PresencePenalty: props.PresencePenalty,
|
||||||
|
@ -10,14 +10,11 @@ import (
|
|||||||
|
|
||||||
type Hook func(message []globals.Message, token int) (string, error)
|
type Hook func(message []globals.Message, token int) (string, error)
|
||||||
|
|
||||||
func ChatWithWeb(message []globals.Message, long bool) []globals.Message {
|
func ChatWithWeb(message []globals.Message) []globals.Message {
|
||||||
data := SearchWebResult(GetPointByLatestMessage(message))
|
data := utils.GetSegmentString(
|
||||||
|
SearchWebResult(GetPointByLatestMessage(message)), 2048,
|
||||||
|
)
|
||||||
|
|
||||||
if long {
|
|
||||||
data = utils.GetSegmentString(data, 6000)
|
|
||||||
} else {
|
|
||||||
data = utils.GetSegmentString(data, 3000)
|
|
||||||
}
|
|
||||||
return utils.Insert(message, 0, globals.Message{
|
return utils.Insert(message, 0, globals.Message{
|
||||||
Role: globals.System,
|
Role: globals.System,
|
||||||
Content: fmt.Sprintf("You will play the role of an AI Q&A assistant, where your knowledge base is not offline, but can be networked in real time, and you can provide real-time networked information with links to networked search sources."+
|
Content: fmt.Sprintf("You will play the role of an AI Q&A assistant, where your knowledge base is not offline, but can be networked in real time, and you can provide real-time networked information with links to networked search sources."+
|
||||||
|
@ -9,7 +9,7 @@ func UsingWebSegment(instance *conversation.Conversation) []globals.Message {
|
|||||||
segment := conversation.CopyMessage(instance.GetChatMessage())
|
segment := conversation.CopyMessage(instance.GetChatMessage())
|
||||||
|
|
||||||
if instance.IsEnableWeb() {
|
if instance.IsEnableWeb() {
|
||||||
segment = ChatWithWeb(segment, globals.IsLongContextModel(instance.GetModel()))
|
segment = ChatWithWeb(segment)
|
||||||
}
|
}
|
||||||
|
|
||||||
return segment
|
return segment
|
||||||
@ -17,7 +17,7 @@ func UsingWebSegment(instance *conversation.Conversation) []globals.Message {
|
|||||||
|
|
||||||
func UsingWebNativeSegment(enable bool, message []globals.Message) []globals.Message {
|
func UsingWebNativeSegment(enable bool, message []globals.Message) []globals.Message {
|
||||||
if enable {
|
if enable {
|
||||||
return ChatWithWeb(message, false)
|
return ChatWithWeb(message)
|
||||||
} else {
|
} else {
|
||||||
return message
|
return message
|
||||||
}
|
}
|
||||||
|
@ -63,4 +63,5 @@ type UserData struct {
|
|||||||
IsSubscribed bool `json:"is_subscribed"`
|
IsSubscribed bool `json:"is_subscribed"`
|
||||||
TotalMonth int64 `json:"total_month"`
|
TotalMonth int64 `json:"total_month"`
|
||||||
Enterprise bool `json:"enterprise"`
|
Enterprise bool `json:"enterprise"`
|
||||||
|
Level int `json:"level"`
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ func GetUserPagination(db *sql.DB, page int64, search string) PaginationForm {
|
|||||||
SELECT
|
SELECT
|
||||||
auth.id, auth.username, auth.is_admin,
|
auth.id, auth.username, auth.is_admin,
|
||||||
quota.quota, quota.used,
|
quota.quota, quota.used,
|
||||||
subscription.expired_at, subscription.total_month, subscription.enterprise
|
subscription.expired_at, subscription.total_month, subscription.enterprise, subscription.level
|
||||||
FROM auth
|
FROM auth
|
||||||
LEFT JOIN quota ON quota.user_id = auth.id
|
LEFT JOIN quota ON quota.user_id = auth.id
|
||||||
LEFT JOIN subscription ON subscription.user_id = auth.id
|
LEFT JOIN subscription ON subscription.user_id = auth.id
|
||||||
@ -49,8 +49,9 @@ func GetUserPagination(db *sql.DB, page int64, search string) PaginationForm {
|
|||||||
usedQuota sql.NullFloat64
|
usedQuota sql.NullFloat64
|
||||||
totalMonth sql.NullInt64
|
totalMonth sql.NullInt64
|
||||||
isEnterprise sql.NullBool
|
isEnterprise sql.NullBool
|
||||||
|
subscriptionLevel sql.NullInt64
|
||||||
)
|
)
|
||||||
if err := rows.Scan(&user.Id, &user.Username, &user.IsAdmin, "a, &usedQuota, &expired, &totalMonth, &isEnterprise); err != nil {
|
if err := rows.Scan(&user.Id, &user.Username, &user.IsAdmin, "a, &usedQuota, &expired, &totalMonth, &isEnterprise, &subscriptionLevel); err != nil {
|
||||||
return PaginationForm{
|
return PaginationForm{
|
||||||
Status: false,
|
Status: false,
|
||||||
Message: err.Error(),
|
Message: err.Error(),
|
||||||
@ -65,6 +66,9 @@ func GetUserPagination(db *sql.DB, page int64, search string) PaginationForm {
|
|||||||
if totalMonth.Valid {
|
if totalMonth.Valid {
|
||||||
user.TotalMonth = totalMonth.Int64
|
user.TotalMonth = totalMonth.Int64
|
||||||
}
|
}
|
||||||
|
if subscriptionLevel.Valid {
|
||||||
|
user.Level = int(subscriptionLevel.Int64)
|
||||||
|
}
|
||||||
stamp := utils.ConvertTime(expired)
|
stamp := utils.ConvertTime(expired)
|
||||||
if stamp != nil {
|
if stamp != nil {
|
||||||
user.IsSubscribed = stamp.After(time.Now())
|
user.IsSubscribed = stamp.After(time.Now())
|
||||||
|
@ -11,6 +11,6 @@
|
|||||||
},
|
},
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "src/components",
|
"components": "src/components",
|
||||||
"utils": "src/components/ui/lib/utils"
|
"utils": "@/components/ui/lib/utils"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
"@radix-ui/react-popover": "^1.0.7",
|
"@radix-ui/react-popover": "^1.0.7",
|
||||||
"@radix-ui/react-progress": "^1.0.3",
|
"@radix-ui/react-progress": "^1.0.3",
|
||||||
|
"@radix-ui/react-radio-group": "^1.1.3",
|
||||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||||
"@radix-ui/react-select": "^2.0.0",
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
"@radix-ui/react-separator": "^1.0.3",
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
|
33
app/pnpm-lock.yaml
generated
33
app/pnpm-lock.yaml
generated
@ -29,6 +29,9 @@ dependencies:
|
|||||||
'@radix-ui/react-progress':
|
'@radix-ui/react-progress':
|
||||||
specifier: ^1.0.3
|
specifier: ^1.0.3
|
||||||
version: 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-radio-group':
|
||||||
|
specifier: ^1.1.3
|
||||||
|
version: 1.1.3(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@radix-ui/react-scroll-area':
|
'@radix-ui/react-scroll-area':
|
||||||
specifier: ^1.0.5
|
specifier: ^1.0.5
|
||||||
version: 1.0.5(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.0.5(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
|
||||||
@ -1213,6 +1216,36 @@ packages:
|
|||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-radio-group@1.1.3(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.23.2
|
||||||
|
'@radix-ui/primitive': 1.0.1
|
||||||
|
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.33)(react@18.2.0)
|
||||||
|
'@radix-ui/react-context': 1.0.1(@types/react@18.2.33)(react@18.2.0)
|
||||||
|
'@radix-ui/react-direction': 1.0.1(@types/react@18.2.33)(react@18.2.0)
|
||||||
|
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.33)(react@18.2.0)
|
||||||
|
'@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.33)(react@18.2.0)
|
||||||
|
'@radix-ui/react-use-size': 1.0.1(@types/react@18.2.33)(react@18.2.0)
|
||||||
|
'@types/react': 18.2.33
|
||||||
|
'@types/react-dom': 18.2.14
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0):
|
/@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==}
|
resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { ChannelCommonResponse } from "@/admin/api/channel.ts";
|
|
||||||
|
|
||||||
export type Channel = {
|
export type Channel = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@ -175,15 +173,3 @@ export function getChannelType(type?: string): string {
|
|||||||
if (type && type in ChannelTypes) return ChannelTypes[type];
|
if (type && type in ChannelTypes) return ChannelTypes[type];
|
||||||
return ChannelTypes.openai;
|
return ChannelTypes.openai;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toastState(
|
|
||||||
toast: any,
|
|
||||||
t: any,
|
|
||||||
state: ChannelCommonResponse,
|
|
||||||
toastSuccess?: boolean,
|
|
||||||
) {
|
|
||||||
if (state.status)
|
|
||||||
toastSuccess &&
|
|
||||||
toast({ title: t("success"), description: t("request-success") });
|
|
||||||
else toast({ title: t("error"), description: state.error });
|
|
||||||
}
|
|
||||||
|
15
app/src/admin/charge.ts
Normal file
15
app/src/admin/charge.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export const tokenBilling = "token-billing";
|
||||||
|
export const timesBilling = "times-billing";
|
||||||
|
export const nonBilling = "non-billing";
|
||||||
|
|
||||||
|
export const defaultChargeType = tokenBilling;
|
||||||
|
export const chargeTypes = [nonBilling, timesBilling, tokenBilling];
|
||||||
|
|
||||||
|
export type ChargeProps = {
|
||||||
|
id: number;
|
||||||
|
models: string[];
|
||||||
|
type: string;
|
||||||
|
anonymous: boolean;
|
||||||
|
input: number;
|
||||||
|
output: number;
|
||||||
|
};
|
@ -66,6 +66,7 @@ export type UserData = {
|
|||||||
used_quota: number;
|
used_quota: number;
|
||||||
is_subscribed: boolean;
|
is_subscribed: boolean;
|
||||||
total_month: number;
|
total_month: number;
|
||||||
|
level: number;
|
||||||
enterprise: boolean;
|
enterprise: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
16
app/src/admin/utils.ts
Normal file
16
app/src/admin/utils.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export type CommonResponse = {
|
||||||
|
status: boolean;
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function toastState(
|
||||||
|
toast: any,
|
||||||
|
t: any,
|
||||||
|
state: CommonResponse,
|
||||||
|
toastSuccess?: boolean,
|
||||||
|
) {
|
||||||
|
if (state.status)
|
||||||
|
toastSuccess &&
|
||||||
|
toast({ title: t("success"), description: t("request-success") });
|
||||||
|
else toast({ title: t("error"), description: state.error });
|
||||||
|
}
|
@ -3,6 +3,7 @@
|
|||||||
@import "management";
|
@import "management";
|
||||||
@import "broadcast";
|
@import "broadcast";
|
||||||
@import "channel";
|
@import "channel";
|
||||||
|
@import "charge";
|
||||||
|
|
||||||
.admin-page {
|
.admin-page {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
.broadcast {
|
.broadcast {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: max-content;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
.channel {
|
.channel {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: max-content;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -10,6 +10,17 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 20vh;
|
min-height: 20vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.channel-table {
|
||||||
|
border-radius: var(--radius);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: hsl(var(--border-hover));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-wrapper {
|
.channel-wrapper {
|
||||||
|
46
app/src/assets/admin/charge.less
Normal file
46
app/src/assets/admin/charge.less
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
.charge {
|
||||||
|
width: 100%;
|
||||||
|
height: max-content;
|
||||||
|
padding: 2rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.charge-card {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 20vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.charge-widget {
|
||||||
|
height: max-content;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.charge-editor {
|
||||||
|
padding: 1.5rem;
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
border-radius: var(--radius);
|
||||||
|
|
||||||
|
.token {
|
||||||
|
color: hsl(var(--text-secondary));
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.charge-table {
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
@ -45,8 +45,9 @@
|
|||||||
height: max-content;
|
height: max-content;
|
||||||
padding: 0.75rem 1.5rem;
|
padding: 0.75rem 1.5rem;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background: hsl(var(--background-container));
|
background: hsl(var(--background));
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
|
box-shadow: 0.5rem 0.5rem 1rem 0 var(--shadow);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
max-width: 460px;
|
max-width: 460px;
|
||||||
|
|
||||||
@ -113,10 +114,10 @@
|
|||||||
padding: 1rem 2rem;
|
padding: 1rem 2rem;
|
||||||
margin: 0.5rem;
|
margin: 0.5rem;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background: hsl(var(--background-container));
|
background: hsl(var(--background));
|
||||||
|
box-shadow: 0.5rem 0.5rem 1rem 0 var(--shadow);
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
user-select: none;
|
user-select: none;
|
||||||
box-shadow: 0 0 1rem 0 hsla(var(--foreground), 0.1);
|
|
||||||
|
|
||||||
@media (max-width: 680px) {
|
@media (max-width: 680px) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -2,8 +2,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: calc(100vh - 56px);
|
||||||
max-height: calc(100vh - 56px);
|
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
|
@ -4,9 +4,9 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0;
|
padding: 0.75rem 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: var(--background-sidebar);
|
background: hsl(var(--background));
|
||||||
transition: 0.225s ease-in-out;
|
transition: 0.225s ease-in-out;
|
||||||
min-height: calc(100vh - 56px);
|
min-height: calc(100vh - 56px);
|
||||||
transition-property: width, background, box-shadow, opacity;
|
transition-property: width, background, box-shadow, opacity;
|
||||||
@ -28,13 +28,13 @@
|
|||||||
height: max-content;
|
height: max-content;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: var(--conversation-card);
|
margin: 0 0.75rem;
|
||||||
margin: 0.75rem 0.75rem -0.25rem;
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: 0.2s ease-in-out;
|
transition: 0.2s ease-in-out;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
color: hsl(var(--text-secondary));
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
content: '';
|
content: '';
|
||||||
@ -50,16 +50,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--conversation-card-hover);
|
background: hsl(var(--card-hover));
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
opacity: .25;
|
opacity: .05;
|
||||||
margin-right: 0.75rem;
|
margin-right: 0.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background: var(--conversation-card-active);
|
color: hsl(var(--text));
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@ -92,14 +92,9 @@
|
|||||||
|
|
||||||
.admin-content {
|
.admin-content {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
height: max-content;
|
height: calc(100vh - 56px);
|
||||||
min-height: calc(100vh - 56px);
|
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
touch-action: pan-y;
|
touch-action: pan-y;
|
||||||
scrollbar-width: none;
|
background: hsla(var(--background-container));
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -4,57 +4,59 @@
|
|||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 71 65% 97%;
|
--background: 0 0% 100%;
|
||||||
--background-hover: 56 43% 97%;
|
--background-hover: 5 12% 100%;
|
||||||
--background-container: 71 65% 97%;
|
--background-container: 0, 0%, 97%, 0.8;
|
||||||
--foreground: 222.2 84% 4.9%;
|
--foreground: 240 10% 3.9%;
|
||||||
|
|
||||||
--card: 0 0% 100%;
|
--card: 0 0% 100%;
|
||||||
|
--card-hover: 0 0% 97.5%;
|
||||||
|
--card-active: 0 0% 95%;
|
||||||
--card-foreground: 240 10% 3.9%;
|
--card-foreground: 240 10% 3.9%;
|
||||||
|
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 240 10% 3.9%;
|
--popover-foreground: 240 10% 3.9%;
|
||||||
|
|
||||||
--primary: 222.2 47.4% 11.2%;
|
--primary: 240 5.9% 10%;
|
||||||
--primary-foreground: 210 40% 98%;
|
--primary-foreground: 0 0% 98%;
|
||||||
|
|
||||||
--secondary: 210 40% 96.1%;
|
--secondary: 240 4.8% 95.9%;
|
||||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
--secondary-foreground: 240 5.9% 10%;
|
||||||
|
|
||||||
--muted: 60 26% 92%;
|
--muted: 240 4.8% 95.9%;
|
||||||
--muted-foreground: 240 3.8% 46.1%;
|
--muted-foreground: 240 3.8% 46.1%;
|
||||||
|
|
||||||
--accent: 37 26% 90%;
|
--accent: 240 4.8% 95.9%;
|
||||||
--accent-secondary: 37 26% 95%;
|
--accent-secondary: 240 4.8% 95.9%;
|
||||||
--accent-foreground: 240 5.9% 10%;
|
--accent-foreground: 240 5.9% 10%;
|
||||||
|
|
||||||
--destructive: 0 84.2% 60.2%;
|
--destructive: 0 67.22% 50.59%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--border: 240 5.9% 90%;
|
||||||
|
--border-hover: 240 5.9% 85%;
|
||||||
|
--border-active: 240 5.9% 80%;
|
||||||
|
|
||||||
|
--input: 240 5.9% 90%;
|
||||||
|
--input-unread: 240 5.9% 50%;
|
||||||
|
--ring: 240 5% 64.9%;
|
||||||
|
|
||||||
--border: 37 26% 83%;
|
|
||||||
--border-hover: 37 26% 78%;
|
|
||||||
--border-active: 37 26% 73%;
|
|
||||||
--input: 37 26% 90%;
|
|
||||||
--input-unread: 37 26% 70%;
|
|
||||||
--ring: 222.2 84% 4.9%;
|
|
||||||
--text: 0 0% 0%;
|
--text: 0 0% 0%;
|
||||||
--text-dark: 0 0% 100%;
|
--text-dark: 0 0% 100%;
|
||||||
--text-secondary: 0 0% 20%;
|
--text-secondary: 0 0% 20%;
|
||||||
--text-secondary-dark: 0 0% 80%;
|
--text-secondary-dark: 0 0% 80%;
|
||||||
|
|
||||||
|
--selection: 212 100% 41%;
|
||||||
|
--selection-foreground: 0 0% 98%;
|
||||||
|
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
|
|
||||||
--conversation-card: rgba(222,214,200,.05);
|
--shadow: #00000005;
|
||||||
--conversation-card-hover: rgba(222,214,200,.2);
|
|
||||||
--conversation-card-active: rgba(222,214,200,.3);
|
|
||||||
--conversation-card-border: rgba(222,214,200,.5);
|
|
||||||
|
|
||||||
--background-sidebar: rgba(240, 242, 231, 0.25);
|
--assistant-background: hsla(218, 100%, 64%, .04);
|
||||||
--chat-bot-background: rgba(88, 166, 255, .04);
|
--assistant-border: hsla(218, 100%, 64%, .12);
|
||||||
--chat-bot-border: rgba(88, 166, 255, .12);
|
--assistant-border-hover: hsla(218, 100%, 64%, .24);
|
||||||
--chat-bot-border-hover: rgba(88, 166, 255, .24);
|
--assistant-shadow: hsla(218, 100%, 64%, .03);
|
||||||
|
|
||||||
--model-card-background: rgba(222,214,200,.2);
|
|
||||||
|
|
||||||
--gold: 45 100% 50%;
|
--gold: 45 100% 50%;
|
||||||
}
|
}
|
||||||
@ -62,10 +64,12 @@
|
|||||||
.dark {
|
.dark {
|
||||||
--background: 0 0% 0%;
|
--background: 0 0% 0%;
|
||||||
--background-hover: 0 0% 7.8%;
|
--background-hover: 0 0% 7.8%;
|
||||||
--background-container: 0 0% 7.8%;
|
--background-container: 0, 0%, 7.8%, 0.8;
|
||||||
--foreground: 210 40% 98%;
|
--foreground: 210 40% 98%;
|
||||||
|
|
||||||
--card: 240 10% 3.9%;
|
--card: 240 10% 3.9%;
|
||||||
|
--card-hover: 240 10% 8.9%;
|
||||||
|
--card-active: 240 10% 13.9%;
|
||||||
--card-foreground: 0 0% 98%;
|
--card-foreground: 0 0% 98%;
|
||||||
|
|
||||||
--popover: 240 10% 3.9%;
|
--popover: 240 10% 3.9%;
|
||||||
@ -81,7 +85,7 @@
|
|||||||
--muted-foreground: 240 5% 64.9%;
|
--muted-foreground: 240 5% 64.9%;
|
||||||
|
|
||||||
--accent: 240 3.7% 15.9%;
|
--accent: 240 3.7% 15.9%;
|
||||||
--accent-secondary: 240 3.7% 15.9%;
|
--accent-secondary: 240 5% 15.9%;
|
||||||
--accent-foreground: 0 0% 98%;
|
--accent-foreground: 0 0% 98%;
|
||||||
|
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 62.8% 30.6%;
|
||||||
@ -96,17 +100,12 @@
|
|||||||
--text: 0 0% 100%;
|
--text: 0 0% 100%;
|
||||||
--text-secondary: 0 0% 80%;
|
--text-secondary: 0 0% 80%;
|
||||||
|
|
||||||
--conversation-card: rgba(152,153,165,.05);
|
--shadow: #ffffff05;
|
||||||
--conversation-card-hover: rgba(152,153,165,.2);
|
|
||||||
--conversation-card-active: rgba(152,153,165,.3);
|
|
||||||
--conversation-card-border: rgba(152,153,165,.1);
|
|
||||||
--background-sidebar: rgba(0, 0, 0, 0.25);
|
|
||||||
|
|
||||||
--chat-bot-background: rgba(88, 166, 255, .1);
|
--assistant-background: hsla(218, 100%, 64%, .1);
|
||||||
--chat-bot-border: rgba(88, 166, 255, .2);
|
--assistant-border: hsla(218, 100%, 64%, .2);
|
||||||
--chat-bot-border-hover: rgba(88, 166, 255, .25);
|
--assistant-border-hover: hsla(218, 100%, 64%, .25);
|
||||||
|
--assistant-shadow: hsla(218, 100%, 64%, .05);
|
||||||
--model-card-background: rgba(152,153,165,.05);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,7 +147,7 @@ strong {
|
|||||||
height: max-content;
|
height: max-content;
|
||||||
min-height: calc(100vh - 56px);
|
min-height: calc(100vh - 56px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: hsl(var(--background-container));
|
background: hsla(var(--background-container));
|
||||||
padding: 2.5rem 5rem;
|
padding: 2.5rem 5rem;
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
margin-bottom: 4px !important;
|
margin-bottom: 4px !important;
|
||||||
padding: 6px 12px !important;
|
padding: 6px 12px !important;
|
||||||
border: 1px solid hsl(var(--border-hover));
|
border: 1px solid hsl(var(--border-hover));
|
||||||
background: hsl(var(--background-container)) !important;
|
background: hsla(var(--background-container)) !important;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
|
|
||||||
@media (max-width: 668px) {
|
@media (max-width: 668px) {
|
||||||
|
@ -2,5 +2,5 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100vh - 56px);
|
height: calc(100vh - 56px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: hsl(var(--background-container));
|
background: hsla(var(--background-container));
|
||||||
}
|
}
|
||||||
|
@ -144,16 +144,28 @@
|
|||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.user {
|
||||||
|
.message-content {
|
||||||
|
background: hsl(var(--background));
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: hsl(var(--border-hover));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.assistant,
|
&.assistant,
|
||||||
&.system {
|
&.system {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|
||||||
.message-content {
|
.message-content {
|
||||||
background: var(--chat-bot-background);
|
background: var(--assistant-background);
|
||||||
border: 1px solid var(--chat-bot-border);
|
border: 1px solid var(--assistant-border);
|
||||||
|
box-shadow: 0.25rem 0.25rem 1rem 0 var(--assistant-shadow);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: var(--chat-bot-border-hover);
|
border-color: var(--assistant-border-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,7 +116,7 @@
|
|||||||
color: hsl(var(--text));
|
color: hsl(var(--text));
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
background: hsl(var(--background-container));
|
background: hsla(var(--background-container));
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border: 1px solid hsl(var(--border-hover));
|
border: 1px solid hsl(var(--border-hover));
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100vh - 56px);
|
height: calc(100vh - 56px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: hsl(var(--background-container));
|
background: hsla(var(--background-container));
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-market {
|
.model-market {
|
||||||
@ -126,7 +126,7 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: var(--model-card-background);
|
background: hsl(var(--background));
|
||||||
transition: 0.5s;
|
transition: 0.5s;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
@ -277,7 +277,7 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: var(--background-sidebar);
|
background: hsl(var(--background));
|
||||||
transition: 0.2s ease-in-out;
|
transition: 0.2s ease-in-out;
|
||||||
transition-property: width, background, box-shadow, border-right, opacity;
|
transition-property: width, background, box-shadow, border-right, opacity;
|
||||||
border-right: 0;
|
border-right: 0;
|
||||||
@ -381,9 +381,9 @@
|
|||||||
margin: 0 6px;
|
margin: 0 6px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
border: 1px solid var(--conversation-card-border);
|
border: 1px solid hsl(var(--border));
|
||||||
transition: 0.2s ease-in-out;
|
transition: 0.2s ease-in-out;
|
||||||
background: var(--conversation-card);
|
background: hsl(var(--card));
|
||||||
|
|
||||||
.more {
|
.more {
|
||||||
color: hsl(var(--text-secondary));
|
color: hsl(var(--text-secondary));
|
||||||
@ -399,7 +399,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--conversation-card-hover);
|
background: hsl(var(--card-hover));
|
||||||
|
border-color: hsl(var(--border-hover));
|
||||||
|
|
||||||
.id {
|
.id {
|
||||||
display: none;
|
display: none;
|
||||||
@ -412,7 +413,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background: var(--conversation-card-active);
|
background: hsl(var(--card-hover));
|
||||||
|
border-color: hsl(var(--border-active));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -508,7 +510,7 @@
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: hsl(var(--background-container));
|
background: hsla(var(--background-container));
|
||||||
transition: width 0.2s ease-in-out;
|
transition: width 0.2s ease-in-out;
|
||||||
|
|
||||||
.chat-wrapper {
|
.chat-wrapper {
|
||||||
|
@ -114,7 +114,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
background: hsl(var(--background-container));
|
background: hsla(var(--background-container));
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -58,7 +58,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
border-bottom: 1px solid hsl(var(--border));
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
background: hsl(var(--background-container));
|
background: hsla(var(--background-container));
|
||||||
color: hsl(var(--text));
|
color: hsl(var(--text));
|
||||||
padding: 0.85rem 1rem 0.75rem;
|
padding: 0.85rem 1rem 0.75rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -30,24 +30,12 @@
|
|||||||
color: hsl(var(--text));
|
color: hsl(var(--text));
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: hsl(var(--accent));
|
background: hsl(var(--accent-secondary));
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background: hsl(var(--text));
|
|
||||||
border-color: hsl(var(--border-hover));
|
border-color: hsl(var(--border-hover));
|
||||||
color: hsl(var(--background));
|
background: hsl(var(--accent));
|
||||||
|
|
||||||
.select-element.badge {
|
|
||||||
&.badge-default {
|
|
||||||
background: hsl(var(--background)) !important;
|
|
||||||
color: hsl(var(--text));
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: hsl(var(--background)) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -123,3 +111,15 @@ input[type="number"] {
|
|||||||
transform: rotate(135deg);
|
transform: rotate(135deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selection {
|
||||||
|
::selection {
|
||||||
|
color: hsl(var(--selection-foreground));
|
||||||
|
background: hsl(var(--selection));
|
||||||
|
}
|
||||||
|
|
||||||
|
::-moz-selection {
|
||||||
|
color: hsl(var(--selection-foreground));
|
||||||
|
background: hsl(var(--selection));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -36,7 +36,7 @@ function OperationAction({ tooltip, children, onClick, variant }: ActionProps) {
|
|||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
size={`icon`}
|
size={`icon`}
|
||||||
className={`mx-1 w-8 h-8`}
|
className={`w-8 h-8`}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@ -56,7 +56,7 @@ function OperationAction({ tooltip, children, onClick, variant }: ActionProps) {
|
|||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
size={`icon`}
|
size={`icon`}
|
||||||
className={`mx-1 w-8 h-8`}
|
className={`w-8 h-8`}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
>
|
>
|
||||||
|
367
app/src/components/admin/ChargeWidget.tsx
Normal file
367
app/src/components/admin/ChargeWidget.tsx
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group.tsx";
|
||||||
|
import { Label } from "@/components/ui/label.tsx";
|
||||||
|
import {
|
||||||
|
ChargeProps,
|
||||||
|
chargeTypes,
|
||||||
|
defaultChargeType, nonBilling, timesBilling, tokenBilling,
|
||||||
|
} from "@/admin/charge.ts";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Input } from "@/components/ui/input.tsx";
|
||||||
|
import {useMemo, useReducer, useState} from "react";
|
||||||
|
import { Button } from "@/components/ui/button.tsx";
|
||||||
|
import {
|
||||||
|
Cloud,
|
||||||
|
DownloadCloud,
|
||||||
|
Eraser,
|
||||||
|
EyeOff,
|
||||||
|
Minus,
|
||||||
|
Plus, RotateCw,
|
||||||
|
Search,
|
||||||
|
Settings2, Trash,
|
||||||
|
UploadCloud,
|
||||||
|
} from "lucide-react";
|
||||||
|
import {DropdownMenu, DropdownMenuContent, DropdownMenuTrigger} from "@/components/ui/dropdown-menu.tsx";
|
||||||
|
import {Command, CommandInput, CommandItem, CommandList} from "@/components/ui/command.tsx";
|
||||||
|
import {ChannelModels} from "@/admin/channel.ts";
|
||||||
|
import {toastState} from "@/admin/utils.ts";
|
||||||
|
import {Switch} from "@/components/ui/switch.tsx";
|
||||||
|
import {NumberInput} from "@/components/ui/number-input.tsx";
|
||||||
|
import {Table, TableBody, TableCell, TableHeader, TableRow} from "@/components/ui/table.tsx";
|
||||||
|
import OperationAction from "@/components/OperationAction.tsx";
|
||||||
|
import {Badge} from "@/components/ui/badge.tsx";
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
|
||||||
|
const initialState: ChargeProps = {
|
||||||
|
id: 0,
|
||||||
|
type: defaultChargeType,
|
||||||
|
models: [],
|
||||||
|
anonymous: false,
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
function reducer(state: ChargeProps, action: any): ChargeProps {
|
||||||
|
switch (action.type) {
|
||||||
|
case "set-models":
|
||||||
|
return { ...state, models: action.payload };
|
||||||
|
case "add-model":
|
||||||
|
const model = action.payload.trim();
|
||||||
|
if (model.length === 0 || state.models.includes(model)) return state;
|
||||||
|
return { ...state, models: [...state.models, model] };
|
||||||
|
case "remove-model":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
models: state.models.filter((model) => model !== action.payload),
|
||||||
|
};
|
||||||
|
case "set-type":
|
||||||
|
return { ...state, type: action.payload };
|
||||||
|
case "set-anonymous":
|
||||||
|
return { ...state, anonymous: action.payload };
|
||||||
|
case "set-input":
|
||||||
|
return { ...state, input: action.payload };
|
||||||
|
case "set-output":
|
||||||
|
return { ...state, output: action.payload };
|
||||||
|
case "clear":
|
||||||
|
return initialState;
|
||||||
|
case "clear-param":
|
||||||
|
return { ...initialState, id: state.id };
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function preflight(state: ChargeProps): ChargeProps {
|
||||||
|
state.models = state.models.map((model) => model.trim()).filter((model) => model.length > 0);
|
||||||
|
switch (state.type) {
|
||||||
|
case nonBilling:
|
||||||
|
state.input = 0;
|
||||||
|
state.output = 0;
|
||||||
|
break;
|
||||||
|
case timesBilling:
|
||||||
|
state.input = 0;
|
||||||
|
state.anonymous = false;
|
||||||
|
break;
|
||||||
|
case tokenBilling:
|
||||||
|
state.anonymous = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.input < 0) state.input = 0;
|
||||||
|
if (state.output < 0) state.output = 0;
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChargeEditor() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const [model, setModel] = useState("");
|
||||||
|
const [form, dispatch] = useReducer(reducer, initialState);
|
||||||
|
const unusedModels = useMemo(() => {
|
||||||
|
return ChannelModels.filter(
|
||||||
|
(model) => !form.models.includes(model) && model !== "",
|
||||||
|
);
|
||||||
|
}, [form.models]);
|
||||||
|
|
||||||
|
async function post() {
|
||||||
|
const data = preflight({ ...form });
|
||||||
|
console.log(data);
|
||||||
|
toastState(toast, t, {});
|
||||||
|
dispatch({ type: "clear" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`charge-editor`}>
|
||||||
|
<div className={`w-full h-max mb-5`}>
|
||||||
|
<RadioGroup
|
||||||
|
value={form.type}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
dispatch({ type: "set-type", payload: value })
|
||||||
|
}
|
||||||
|
className={`flex flex-row gap-5 whitespace-nowrap flex-wrap`}
|
||||||
|
>
|
||||||
|
{chargeTypes.map((chargeType, index) => (
|
||||||
|
<div
|
||||||
|
className="flex items-center space-x-2 cursor-pointer"
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
<RadioGroupItem
|
||||||
|
className={`transition-all duration-200`}
|
||||||
|
value={chargeType}
|
||||||
|
id={chargeType}
|
||||||
|
/>
|
||||||
|
<Label htmlFor={chargeType} className={`cursor-pointer`}>
|
||||||
|
{t(`admin.charge.${chargeType}`)}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
<div className={`flex flex-row w-full h-max mb-4`}>
|
||||||
|
<Button
|
||||||
|
onClick={() => dispatch({ type: "add-model", payload: model })}
|
||||||
|
size={`icon`}
|
||||||
|
className={`mr-2 shrink-0`}
|
||||||
|
>
|
||||||
|
<Plus className={`w-4 h-4`} />
|
||||||
|
</Button>
|
||||||
|
<Input
|
||||||
|
value={model}
|
||||||
|
onChange={(e) => setModel(e.target.value)}
|
||||||
|
placeholder={t("admin.channels.model")}
|
||||||
|
/>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button size={`icon`} className={`ml-2 shrink-0`}>
|
||||||
|
<Search className={`w-4 h-4`} />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align={`end`} asChild>
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder={t("admin.channels.search-model")}
|
||||||
|
/>
|
||||||
|
<CommandList className={`thin-scrollbar`}>
|
||||||
|
{unusedModels.map((model, idx) => (
|
||||||
|
<CommandItem
|
||||||
|
key={idx}
|
||||||
|
value={model}
|
||||||
|
onSelect={(value) => dispatch({ type: "add-model", payload: value })}
|
||||||
|
className={`px-2`}
|
||||||
|
>
|
||||||
|
{model}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
<div className={`flex flex-col w-full h-max mb-2`}>
|
||||||
|
{form.models.map((model, index) => (
|
||||||
|
<div className={`flex flex-row w-full h-max shrink-0 mb-2 select-none`} key={index}>
|
||||||
|
<Input value={model} readOnly />
|
||||||
|
<Button
|
||||||
|
onClick={() => dispatch({ type: "remove-model", payload: model })}
|
||||||
|
size={`icon`} variant={`outline`}
|
||||||
|
className={`ml-2 shrink-0`}
|
||||||
|
>
|
||||||
|
<Minus className={`w-4 h-4`} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
form.type === nonBilling && (
|
||||||
|
<div className={`flex flex-row w-full h-max items-center mt-4 mb-6`}>
|
||||||
|
<EyeOff className={`w-4 h-4 mr-2`} />
|
||||||
|
<Label className={`grow`}>{t("admin.charge.anonymous")}</Label>
|
||||||
|
<Switch
|
||||||
|
checked={form.anonymous}
|
||||||
|
onCheckedChange={(checked) => dispatch({ type: "set-anonymous", payload: checked })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
form.type === timesBilling && (
|
||||||
|
<div className={`flex flex-row w-full h-max items-center`}>
|
||||||
|
<Cloud className={`w-4 h-4 mr-2`} />
|
||||||
|
<Label className={`grow`}>{t("admin.charge.time-count")}</Label>
|
||||||
|
<NumberInput
|
||||||
|
value={form.output}
|
||||||
|
onValueChange={(value) => dispatch({ type: "set-output", payload: value })}
|
||||||
|
acceptNegative={false}
|
||||||
|
className={`w-20`}
|
||||||
|
min={0}
|
||||||
|
max={99999}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
form.type === tokenBilling && (
|
||||||
|
<div className={`flex flex-col w-full h-max gap-2`}>
|
||||||
|
<div className={`flex flex-row w-full h-max items-center`}>
|
||||||
|
<UploadCloud className={`w-4 h-4 mr-2`} />
|
||||||
|
<Label className={`grow`}>
|
||||||
|
{t("admin.charge.input-count")}
|
||||||
|
<span className={`token`}> / 1k tokens</span>
|
||||||
|
</Label>
|
||||||
|
<NumberInput
|
||||||
|
value={form.input}
|
||||||
|
onValueChange={(value) => dispatch({ type: "set-input", payload: value })}
|
||||||
|
acceptNegative={false}
|
||||||
|
className={`w-20`}
|
||||||
|
min={0}
|
||||||
|
max={99999}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={`flex flex-row w-full h-max items-center`}>
|
||||||
|
<DownloadCloud className={`w-4 h-4 mr-2`} />
|
||||||
|
<Label className={`grow`}>
|
||||||
|
{t("admin.charge.output-count")}
|
||||||
|
<span className={`token`}> / 1k tokens</span>
|
||||||
|
</Label>
|
||||||
|
<NumberInput
|
||||||
|
value={form.output}
|
||||||
|
onValueChange={(value) => dispatch({ type: "set-output", payload: value })}
|
||||||
|
acceptNegative={false}
|
||||||
|
className={`w-20`}
|
||||||
|
min={0}
|
||||||
|
max={99999}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<div className={`flex flex-row w-full h-max mt-5 gap-2`}>
|
||||||
|
<div className={`grow`} />
|
||||||
|
<Button variant={`outline`} size={`icon`} onClick={() => dispatch({ type: "clear-param" })}>
|
||||||
|
<Eraser className={`w-4 h-4`} />
|
||||||
|
</Button>
|
||||||
|
<Button onClick={post}>
|
||||||
|
<Plus className={`w-4 h-4 mr-2`} />
|
||||||
|
{t("admin.charge.add-rule")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChargeTable() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [data, setData] = useState<ChargeProps[]>([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: nonBilling,
|
||||||
|
models: ["gpt-4", "gpt-4-0613"],
|
||||||
|
anonymous: true,
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`charge-table`}>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className={`select-none whitespace-nowrap`}>
|
||||||
|
<TableCell>{t("admin.charge.id")}</TableCell>
|
||||||
|
<TableCell>{t("admin.charge.type")}</TableCell>
|
||||||
|
<TableCell>{t("admin.charge.model")}</TableCell>
|
||||||
|
<TableCell>{t("admin.charge.input")}</TableCell>
|
||||||
|
<TableCell>{t("admin.charge.output")}</TableCell>
|
||||||
|
<TableCell>{t("admin.charge.support-anonymous")}</TableCell>
|
||||||
|
<TableCell>{t("admin.charge.action")}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data.map((charge, idx) => (
|
||||||
|
<TableRow key={idx}>
|
||||||
|
<TableCell>{charge.id}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge>
|
||||||
|
{charge.type.split("-")[0]}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className={`select-none`}>
|
||||||
|
<pre>{charge.models.join("\n")}</pre>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{charge.input === 0 ? 0 : charge.input.toFixed(2)}</TableCell>
|
||||||
|
<TableCell>{charge.output === 0 ? 0 : charge.output.toFixed(2)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{t(String(charge.anonymous))}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className={`flex flex-row flex-wrap gap-2`}>
|
||||||
|
<OperationAction
|
||||||
|
tooltip={t("admin.channels.edit")}
|
||||||
|
>
|
||||||
|
<Settings2 className={`h-4 w-4`} />
|
||||||
|
</OperationAction>
|
||||||
|
<OperationAction
|
||||||
|
tooltip={t("admin.channels.delete")}
|
||||||
|
variant={`destructive`}
|
||||||
|
>
|
||||||
|
<Trash className={`h-4 w-4`} />
|
||||||
|
</OperationAction>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
<div className={`mt-6 pr-2 flex flex-row w-full h-max`}>
|
||||||
|
<div className={`grow`} />
|
||||||
|
<Button
|
||||||
|
variant={`outline`}
|
||||||
|
size={`icon`}
|
||||||
|
className={`mr-2`}
|
||||||
|
onClick={refresh}
|
||||||
|
>
|
||||||
|
<RotateCw className={`h-4 w-4`} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChargeWidget() {
|
||||||
|
return (
|
||||||
|
<div className={`charge-widget`}>
|
||||||
|
<ChargeEditor />
|
||||||
|
<ChargeTable />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChargeWidget;
|
@ -66,7 +66,7 @@ function MenuBar() {
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
title={t("admin.prize")}
|
title={t("admin.prize")}
|
||||||
icon={<CandlestickChart />}
|
icon={<CandlestickChart />}
|
||||||
path={"/prize"}
|
path={"/charge"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -156,6 +156,7 @@ function UserTable() {
|
|||||||
<TableHead>{t("admin.quota")}</TableHead>
|
<TableHead>{t("admin.quota")}</TableHead>
|
||||||
<TableHead>{t("admin.used-quota")}</TableHead>
|
<TableHead>{t("admin.used-quota")}</TableHead>
|
||||||
<TableHead>{t("admin.is-subscribed")}</TableHead>
|
<TableHead>{t("admin.is-subscribed")}</TableHead>
|
||||||
|
<TableHead>{t("admin.level")}</TableHead>
|
||||||
<TableHead>{t("admin.total-month")}</TableHead>
|
<TableHead>{t("admin.total-month")}</TableHead>
|
||||||
<TableHead>{t("admin.enterprise")}</TableHead>
|
<TableHead>{t("admin.enterprise")}</TableHead>
|
||||||
<TableHead>{t("admin.is-admin")}</TableHead>
|
<TableHead>{t("admin.is-admin")}</TableHead>
|
||||||
@ -166,10 +167,11 @@ function UserTable() {
|
|||||||
{(data.data || []).map((user, idx) => (
|
{(data.data || []).map((user, idx) => (
|
||||||
<TableRow key={idx}>
|
<TableRow key={idx}>
|
||||||
<TableCell>{user.id}</TableCell>
|
<TableCell>{user.id}</TableCell>
|
||||||
<TableCell>{user.username}</TableCell>
|
<TableCell className={`whitespace-nowrap`}>{user.username}</TableCell>
|
||||||
<TableCell>{user.quota}</TableCell>
|
<TableCell>{user.quota}</TableCell>
|
||||||
<TableCell>{user.used_quota}</TableCell>
|
<TableCell>{user.used_quota}</TableCell>
|
||||||
<TableCell>{t(user.is_subscribed.toString())}</TableCell>
|
<TableCell>{t(user.is_subscribed.toString())}</TableCell>
|
||||||
|
<TableCell>{user.level}</TableCell>
|
||||||
<TableCell>{user.total_month}</TableCell>
|
<TableCell>{user.total_month}</TableCell>
|
||||||
<TableCell>{t(user.enterprise.toString())}</TableCell>
|
<TableCell>{t(user.enterprise.toString())}</TableCell>
|
||||||
<TableCell>{t(user.is_admin.toString())}</TableCell>
|
<TableCell>{t(user.is_admin.toString())}</TableCell>
|
||||||
|
@ -13,8 +13,8 @@ import {
|
|||||||
ChannelModels,
|
ChannelModels,
|
||||||
ChannelTypes,
|
ChannelTypes,
|
||||||
getChannelInfo,
|
getChannelInfo,
|
||||||
toastState,
|
|
||||||
} from "@/admin/channel.ts";
|
} from "@/admin/channel.ts";
|
||||||
|
import {toastState} from "@/admin/utils.ts";
|
||||||
import { Textarea } from "@/components/ui/textarea.tsx";
|
import { Textarea } from "@/components/ui/textarea.tsx";
|
||||||
import { NumberInput } from "@/components/ui/number-input.tsx";
|
import { NumberInput } from "@/components/ui/number-input.tsx";
|
||||||
import { Button } from "@/components/ui/button.tsx";
|
import { Button } from "@/components/ui/button.tsx";
|
||||||
@ -288,7 +288,7 @@ function ChannelEditor({ display, id, setEnabled }: ChannelEditorProps) {
|
|||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button>{t("admin.channels.add-model")}</Button>
|
<Button>{t("admin.channels.add-model")}</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent asChild>
|
<DropdownMenuContent align={`start`} asChild>
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder={t("admin.channels.search-model")}
|
placeholder={t("admin.channels.search-model")}
|
||||||
|
@ -10,7 +10,8 @@ import { Check, Plus, RotateCw, Settings2, Trash, X } from "lucide-react";
|
|||||||
import { Button } from "@/components/ui/button.tsx";
|
import { Button } from "@/components/ui/button.tsx";
|
||||||
import OperationAction from "@/components/OperationAction.tsx";
|
import OperationAction from "@/components/OperationAction.tsx";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Channel, getChannelType, toastState } from "@/admin/channel.ts";
|
import { Channel, getChannelType } from "@/admin/channel.ts";
|
||||||
|
import {toastState} from "@/admin/utils.ts";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useEffectAsync } from "@/utils/hook.ts";
|
import { useEffectAsync } from "@/utils/hook.ts";
|
||||||
import {
|
import {
|
||||||
@ -87,10 +88,10 @@ function ChannelTable({ display, setId, setEnabled }: ChannelTableProps) {
|
|||||||
{chan.state ? (
|
{chan.state ? (
|
||||||
<Check className={`h-4 w-4 text-green-500`} />
|
<Check className={`h-4 w-4 text-green-500`} />
|
||||||
) : (
|
) : (
|
||||||
<X className={`h-4 w-4 text-red-500`} />
|
<X className={`h-4 w-4 text-destructive`} />
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className={`flex flex-row flex-wrap gap-2`}>
|
||||||
<OperationAction
|
<OperationAction
|
||||||
tooltip={t("admin.channels.edit")}
|
tooltip={t("admin.channels.edit")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
42
app/src/components/ui/radio-group.tsx
Normal file
42
app/src/components/ui/radio-group.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||||
|
import { Circle } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/components/ui/lib/utils";
|
||||||
|
|
||||||
|
const RadioGroup = React.forwardRef<
|
||||||
|
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Root
|
||||||
|
className={cn("grid gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
const RadioGroupItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||||
|
<Circle className="h-2.5 w-2.5 fill-current text-current" />
|
||||||
|
</RadioGroupPrimitive.Indicator>
|
||||||
|
</RadioGroupPrimitive.Item>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
|
||||||
|
|
||||||
|
export { RadioGroup, RadioGroupItem };
|
@ -141,7 +141,7 @@ function SettingsDialog() {
|
|||||||
<div className={`name`}>{t("settings.history")}</div>
|
<div className={`name`}>{t("settings.history")}</div>
|
||||||
<div className={`grow`} />
|
<div className={`grow`} />
|
||||||
<NumberInput
|
<NumberInput
|
||||||
className={cn(`value`, history === 0 && `text-red-500`)}
|
className={cn(`value`, history === 0 && `text-destructive`)}
|
||||||
value={history}
|
value={history}
|
||||||
acceptNaN={false}
|
acceptNaN={false}
|
||||||
min={0}
|
min={0}
|
||||||
|
@ -345,6 +345,7 @@ const resources = {
|
|||||||
"post-success-prompt": "公告发布成功。",
|
"post-success-prompt": "公告发布成功。",
|
||||||
"post-failed": "发布失败",
|
"post-failed": "发布失败",
|
||||||
"post-failed-prompt": "发布失败,原因:{{reason}}",
|
"post-failed-prompt": "发布失败,原因:{{reason}}",
|
||||||
|
level: "等级",
|
||||||
"is-admin": "管理员",
|
"is-admin": "管理员",
|
||||||
"used-quota": "已用点数",
|
"used-quota": "已用点数",
|
||||||
"is-subscribed": "是否订阅",
|
"is-subscribed": "是否订阅",
|
||||||
@ -402,6 +403,24 @@ const resources = {
|
|||||||
"add-model": "添加模型",
|
"add-model": "添加模型",
|
||||||
"clear-models": "清空全部模型",
|
"clear-models": "清空全部模型",
|
||||||
},
|
},
|
||||||
|
charge: {
|
||||||
|
id: "ID",
|
||||||
|
type: "类型",
|
||||||
|
model: "模型",
|
||||||
|
quota: "点数",
|
||||||
|
action: "操作",
|
||||||
|
input: "输入",
|
||||||
|
output: "输出",
|
||||||
|
"support-anonymous": "支持匿名",
|
||||||
|
"non-billing": "不计费",
|
||||||
|
"times-billing": "按次计费",
|
||||||
|
"token-billing": "按 Token 计费",
|
||||||
|
anonymous: "支持匿名调用",
|
||||||
|
"time-count": "单次请求点数",
|
||||||
|
"input-count": "输入点数",
|
||||||
|
"output-count": "输出点数",
|
||||||
|
"add-rule": "添加规则",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mask: {
|
mask: {
|
||||||
title: "预设设置",
|
title: "预设设置",
|
||||||
@ -759,6 +778,7 @@ const resources = {
|
|||||||
"post-success-prompt": "Broadcast posted successfully.",
|
"post-success-prompt": "Broadcast posted successfully.",
|
||||||
"post-failed": "Post Failed",
|
"post-failed": "Post Failed",
|
||||||
"post-failed-prompt": "Post failed, reason: {{reason}}",
|
"post-failed-prompt": "Post failed, reason: {{reason}}",
|
||||||
|
level: "Level",
|
||||||
"is-admin": "Admin",
|
"is-admin": "Admin",
|
||||||
"used-quota": "Used Quota",
|
"used-quota": "Used Quota",
|
||||||
"is-subscribed": "Subscribed",
|
"is-subscribed": "Subscribed",
|
||||||
@ -1182,6 +1202,7 @@ const resources = {
|
|||||||
"post-failed": "Не удалось",
|
"post-failed": "Не удалось",
|
||||||
"post-failed-prompt":
|
"post-failed-prompt":
|
||||||
"Не удалось отправить объявление, причина: {{reason}}",
|
"Не удалось отправить объявление, причина: {{reason}}",
|
||||||
|
level: "Уровень",
|
||||||
"is-admin": "Админ",
|
"is-admin": "Админ",
|
||||||
"used-quota": "Использовано",
|
"used-quota": "Использовано",
|
||||||
"is-subscribed": "Подписан",
|
"is-subscribed": "Подписан",
|
||||||
|
@ -11,7 +11,7 @@ const Article = lazy(() => import("@/routes/Article.tsx"));
|
|||||||
const Admin = lazy(() => import("@/routes/Admin.tsx"));
|
const Admin = lazy(() => import("@/routes/Admin.tsx"));
|
||||||
const Dashboard = lazy(() => import("@/routes/admin/DashBoard.tsx"));
|
const Dashboard = lazy(() => import("@/routes/admin/DashBoard.tsx"));
|
||||||
const Channel = lazy(() => import("@/routes/admin/Channel.tsx"));
|
const Channel = lazy(() => import("@/routes/admin/Channel.tsx"));
|
||||||
const Prize = lazy(() => import("@/routes/admin/Prize.tsx"));
|
const Charge = lazy(() => import("@/routes/admin/Charge.tsx"));
|
||||||
const Users = lazy(() => import("@/routes/admin/Users.tsx"));
|
const Users = lazy(() => import("@/routes/admin/Users.tsx"));
|
||||||
const Broadcast = lazy(() => import("@/routes/admin/Broadcast.tsx"));
|
const Broadcast = lazy(() => import("@/routes/admin/Broadcast.tsx"));
|
||||||
|
|
||||||
@ -95,11 +95,11 @@ const router = createBrowserRouter([
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "admin-prize",
|
id: "admin-charge",
|
||||||
path: "prize",
|
path: "charge",
|
||||||
element: (
|
element: (
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<Prize />
|
<Charge />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
@ -17,7 +17,7 @@ function Admin() {
|
|||||||
return (
|
return (
|
||||||
<div className={`admin-page`}>
|
<div className={`admin-page`}>
|
||||||
<MenuBar />
|
<MenuBar />
|
||||||
<div className={`admin-content`}>
|
<div className={`admin-content thin-scrollbar`}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
27
app/src/routes/admin/Charge.tsx
Normal file
27
app/src/routes/admin/Charge.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card.tsx";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import ChargeWidget from "@/components/admin/ChargeWidget.tsx";
|
||||||
|
|
||||||
|
function Charge() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`charge`}>
|
||||||
|
<Card className={`charge-card`}>
|
||||||
|
<CardHeader className={`select-none`}>
|
||||||
|
<CardTitle>{t("admin.prize")}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ChargeWidget />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Charge;
|
@ -1,5 +0,0 @@
|
|||||||
function Prize() {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Prize;
|
|
38
auth/rule.go
38
auth/rule.go
@ -1,41 +1,23 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"chat/globals"
|
"chat/channel"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"github.com/go-redis/redis/v8"
|
"github.com/go-redis/redis/v8"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CanEnableModel returns whether the model can be enabled (without subscription)
|
// CanEnableModel returns whether the model can be enabled (without subscription)
|
||||||
func CanEnableModel(db *sql.DB, user *User, model string) bool {
|
func CanEnableModel(db *sql.DB, user *User, model string) bool {
|
||||||
switch model {
|
isAuth := user != nil
|
||||||
case globals.GPT3Turbo, globals.GPT3TurboInstruct, globals.GPT3Turbo0301, globals.GPT3Turbo0613:
|
charge := channel.ChargeInstance.GetCharge(model)
|
||||||
return true
|
|
||||||
case globals.GPT4, globals.GPT40613, globals.GPT40314, globals.GPT41106Preview, globals.GPT41106VisionPreview,
|
if !charge.IsBilling() {
|
||||||
globals.GPT4Dalle, globals.GPT4Vision, globals.Dalle3:
|
// return if is the user is authenticated or anonymous is allowed for this model
|
||||||
return user != nil && user.GetQuota(db) >= 5
|
return charge.SupportAnonymous() || isAuth
|
||||||
case globals.GPT432k, globals.GPT432k0613, globals.GPT432k0314:
|
|
||||||
return user != nil && user.GetQuota(db) >= 50
|
|
||||||
case globals.SparkDesk, globals.SparkDeskV2, globals.SparkDeskV3:
|
|
||||||
return user != nil && user.GetQuota(db) >= 1
|
|
||||||
case globals.Claude1100k, globals.Claude2100k, globals.Claude2200k:
|
|
||||||
return user != nil && user.GetQuota(db) >= 1
|
|
||||||
case globals.ZhiPuChatGLMTurbo, globals.ZhiPuChatGLMPro, globals.ZhiPuChatGLMStd:
|
|
||||||
return user != nil && user.GetQuota(db) >= 1
|
|
||||||
case globals.QwenTurbo, globals.QwenPlus, globals.QwenPlusNet, globals.QwenTurboNet:
|
|
||||||
return user != nil && user.GetQuota(db) >= 1
|
|
||||||
case globals.StableDiffusion, globals.Midjourney, globals.MidjourneyFast, globals.MidjourneyTurbo:
|
|
||||||
return user != nil && user.GetQuota(db) >= 1
|
|
||||||
case globals.LLaMa27B, globals.LLaMa213B, globals.LLaMa270B,
|
|
||||||
globals.CodeLLaMa34B, globals.CodeLLaMa13B, globals.CodeLLaMa7B:
|
|
||||||
return user != nil && user.GetQuota(db) >= 1
|
|
||||||
case globals.Hunyuan, globals.GPT360V9, globals.Baichuan53B:
|
|
||||||
return user != nil && user.GetQuota(db) >= 1
|
|
||||||
case globals.SkylarkLite, globals.SkylarkPlus, globals.SkylarkPro, globals.SkylarkChat:
|
|
||||||
return user != nil && user.GetQuota(db) >= 1
|
|
||||||
default:
|
|
||||||
return user != nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// return if the user is authenticated and has enough quota
|
||||||
|
return isAuth && user.GetQuota(db) >= charge.GetLimit()
|
||||||
}
|
}
|
||||||
|
|
||||||
func CanEnableModelWithSubscription(db *sql.DB, cache *redis.Client, user *User, model string) (canEnable bool, usePlan bool) {
|
func CanEnableModelWithSubscription(db *sql.DB, cache *redis.Client, user *User, model string) (canEnable bool, usePlan bool) {
|
||||||
|
179
channel/charge.go
Normal file
179
channel/charge.go
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
package channel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chat/globals"
|
||||||
|
"chat/utils"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewChargeManager() *ChargeManager {
|
||||||
|
var seq ChargeSequence
|
||||||
|
if err := viper.UnmarshalKey("charge", &seq); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := &ChargeManager{
|
||||||
|
Sequence: seq,
|
||||||
|
Models: map[string]*Charge{},
|
||||||
|
NonBillingModels: []string{},
|
||||||
|
}
|
||||||
|
m.Load()
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ChargeManager) Load() {
|
||||||
|
// init support models
|
||||||
|
m.Models = map[string]*Charge{}
|
||||||
|
for _, charge := range m.Sequence {
|
||||||
|
for _, model := range charge.Models {
|
||||||
|
if _, ok := m.Models[model]; !ok {
|
||||||
|
m.Models[model] = charge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.NonBillingModels = []string{}
|
||||||
|
for _, charge := range m.Sequence {
|
||||||
|
if !charge.IsBilling() {
|
||||||
|
for _, model := range charge.Models {
|
||||||
|
m.NonBillingModels = append(m.NonBillingModels, model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ChargeManager) GetModels() map[string]*Charge {
|
||||||
|
return m.Models
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ChargeManager) GetNonBillingModels() []string {
|
||||||
|
return m.NonBillingModels
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ChargeManager) IsBilling(model string) bool {
|
||||||
|
return !utils.Contains(model, m.NonBillingModels)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ChargeManager) GetCharge(model string) Charge {
|
||||||
|
if charge, ok := m.Models[model]; ok {
|
||||||
|
return *charge
|
||||||
|
}
|
||||||
|
return Charge{
|
||||||
|
Type: globals.NonBilling,
|
||||||
|
Anonymous: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ChargeManager) SaveConfig() error {
|
||||||
|
viper.Set("charge", m.Sequence)
|
||||||
|
m.Load()
|
||||||
|
return viper.WriteConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ChargeManager) GetMaxId() int {
|
||||||
|
max := 0
|
||||||
|
for _, charge := range m.Sequence {
|
||||||
|
if charge.Id > max {
|
||||||
|
max = charge.Id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ChargeManager) AddRule(charge Charge) error {
|
||||||
|
charge.Id = m.GetMaxId() + 1
|
||||||
|
m.Sequence = append(m.Sequence, &charge)
|
||||||
|
return m.SaveConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ChargeManager) UpdateRule(charge Charge) error {
|
||||||
|
for _, item := range m.Sequence {
|
||||||
|
if item.Id == charge.Id {
|
||||||
|
*item = charge
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m.SaveConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ChargeManager) SetRule(charge Charge) error {
|
||||||
|
if charge.Id == -1 {
|
||||||
|
return m.AddRule(charge)
|
||||||
|
}
|
||||||
|
return m.UpdateRule(charge)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ChargeManager) DeleteRule(id int) error {
|
||||||
|
for i, item := range m.Sequence {
|
||||||
|
if item.Id == id {
|
||||||
|
m.Sequence = append(m.Sequence[:i], m.Sequence[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m.SaveConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ChargeManager) ListRules() ChargeSequence {
|
||||||
|
return m.Sequence
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *ChargeManager) GetRule(id int) *Charge {
|
||||||
|
for _, item := range m.Sequence {
|
||||||
|
if item.Id == id {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Charge) GetType() string {
|
||||||
|
if c.Type == "" {
|
||||||
|
return globals.NonBilling
|
||||||
|
}
|
||||||
|
return c.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Charge) GetModels() []string {
|
||||||
|
return c.Models
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Charge) GetInput() float32 {
|
||||||
|
if c.Input <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return c.Input
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Charge) GetOutput() float32 {
|
||||||
|
if c.Output <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return c.Output
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Charge) SupportAnonymous() bool {
|
||||||
|
return c.Anonymous
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Charge) IsBilling() bool {
|
||||||
|
return c.GetType() != globals.NonBilling
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Charge) IsBillingType(t string) bool {
|
||||||
|
return c.GetType() == t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Charge) GetLimit() float32 {
|
||||||
|
switch c.GetType() {
|
||||||
|
case globals.NonBilling:
|
||||||
|
return 0
|
||||||
|
case globals.TimesBilling:
|
||||||
|
return c.GetOutput()
|
||||||
|
case globals.TokenBilling:
|
||||||
|
// 1k input tokens + 1k output tokens
|
||||||
|
return c.GetInput() + c.GetOutput()
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
@ -89,3 +89,37 @@ func UpdateChannel(c *gin.Context) {
|
|||||||
"error": utils.GetError(state),
|
"error": utils.GetError(state),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetCharge(c *gin.Context) {
|
||||||
|
var charge Charge
|
||||||
|
if err := c.ShouldBindJSON(&charge); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"status": false,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state := ChargeInstance.SetRule(charge)
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"status": state == nil,
|
||||||
|
"error": utils.GetError(state),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetChargeList(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"status": true,
|
||||||
|
"data": ChargeInstance.ListRules(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteCharge(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
state := ChargeInstance.DeleteRule(utils.ParseInt(id))
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"status": state == nil,
|
||||||
|
"error": utils.GetError(state),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -7,9 +7,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var ManagerInstance *Manager
|
var ManagerInstance *Manager
|
||||||
|
var ChargeInstance *ChargeManager
|
||||||
|
|
||||||
func InitManager() {
|
func InitManager() {
|
||||||
ManagerInstance = NewManager()
|
ManagerInstance = NewManager()
|
||||||
|
ChargeInstance = NewChargeManager()
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewManager() *Manager {
|
func NewManager() *Manager {
|
||||||
|
@ -10,4 +10,8 @@ func Register(app *gin.Engine) {
|
|||||||
app.GET("/admin/channel/delete/:id", DeleteChannel)
|
app.GET("/admin/channel/delete/:id", DeleteChannel)
|
||||||
app.GET("/admin/channel/activate/:id", ActivateChannel)
|
app.GET("/admin/channel/activate/:id", ActivateChannel)
|
||||||
app.GET("/admin/channel/deactivate/:id", DeactivateChannel)
|
app.GET("/admin/channel/deactivate/:id", DeactivateChannel)
|
||||||
|
|
||||||
|
app.GET("/admin/charge/list", GetChargeList)
|
||||||
|
app.POST("/admin/charge/set", SetCharge)
|
||||||
|
app.GET("/admin/charge/delete/:id", DeleteCharge)
|
||||||
}
|
}
|
||||||
|
@ -28,3 +28,20 @@ type Ticker struct {
|
|||||||
Sequence Sequence `json:"sequence"`
|
Sequence Sequence `json:"sequence"`
|
||||||
Cursor int `json:"cursor"`
|
Cursor int `json:"cursor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Charge struct {
|
||||||
|
Id int `json:"id" mapstructure:"id"`
|
||||||
|
Type string `json:"type" mapstructure:"type"`
|
||||||
|
Models []string `json:"models" mapstructure:"models"`
|
||||||
|
Input float32 `json:"input" mapstructure:"input"`
|
||||||
|
Output float32 `json:"output" mapstructure:"output"`
|
||||||
|
Anonymous bool `json:"anonymous" mapstructure:"anonymous"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChargeSequence []*Charge
|
||||||
|
|
||||||
|
type ChargeManager struct {
|
||||||
|
Sequence ChargeSequence `json:"sequence"`
|
||||||
|
Models map[string]*Charge `json:"models"`
|
||||||
|
NonBillingModels []string `json:"non_billing_models"`
|
||||||
|
}
|
||||||
|
@ -3,13 +3,12 @@ package cli
|
|||||||
import (
|
import (
|
||||||
"chat/adapter/chatgpt"
|
"chat/adapter/chatgpt"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/spf13/viper"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func FilterApiKeyCommand(args []string) {
|
func FilterApiKeyCommand(args []string) {
|
||||||
data := strings.Trim(strings.TrimSpace(GetArgString(args, 0)), "\"")
|
data := strings.Trim(strings.TrimSpace(GetArgString(args, 0)), "\"")
|
||||||
endpoint := viper.GetString("openai.test")
|
endpoint := "https://api.openai.com"
|
||||||
keys := strings.Split(data, "|")
|
keys := strings.Split(data, "|")
|
||||||
|
|
||||||
available := chatgpt.FilterKeysNative(endpoint, keys)
|
available := chatgpt.FilterKeysNative(endpoint, keys)
|
||||||
|
@ -23,3 +23,9 @@ const (
|
|||||||
MidjourneyChannelType = "midjourney"
|
MidjourneyChannelType = "midjourney"
|
||||||
OneAPIChannelType = "oneapi"
|
OneAPIChannelType = "oneapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
NonBilling = "non-billing"
|
||||||
|
TimesBilling = "times-billing"
|
||||||
|
TokenBilling = "token-billing"
|
||||||
|
)
|
||||||
|
@ -104,44 +104,6 @@ var GPT4Array = []string{
|
|||||||
GPT4Vision, GPT4Dalle, GPT4All,
|
GPT4Vision, GPT4Dalle, GPT4All,
|
||||||
}
|
}
|
||||||
|
|
||||||
var GPT432kArray = []string{
|
|
||||||
GPT432k,
|
|
||||||
GPT432k0314,
|
|
||||||
GPT432k0613,
|
|
||||||
}
|
|
||||||
|
|
||||||
var SparkDeskModelArray = []string{
|
|
||||||
SparkDesk,
|
|
||||||
SparkDeskV2,
|
|
||||||
SparkDeskV3,
|
|
||||||
}
|
|
||||||
|
|
||||||
var LongContextModelArray = []string{
|
|
||||||
GPT3Turbo16k, GPT3Turbo16k0613, GPT3Turbo16k0301,
|
|
||||||
GPT41106Preview, GPT41106VisionPreview, GPT432k, GPT432k0314, GPT432k0613,
|
|
||||||
Claude1, Claude1100k,
|
|
||||||
CodeLLaMa34B, LLaMa270B,
|
|
||||||
Claude2, Claude2100k, Claude2200k,
|
|
||||||
}
|
|
||||||
|
|
||||||
var FreeModelArray = []string{
|
|
||||||
GPT3Turbo,
|
|
||||||
GPT3TurboInstruct,
|
|
||||||
GPT3Turbo0613,
|
|
||||||
GPT3Turbo0301,
|
|
||||||
GPT3Turbo1106,
|
|
||||||
GPT3Turbo16k,
|
|
||||||
GPT3Turbo16k0613,
|
|
||||||
GPT3Turbo16k0301,
|
|
||||||
Claude1,
|
|
||||||
Claude2,
|
|
||||||
ChatBison001,
|
|
||||||
BingCreative,
|
|
||||||
BingBalanced,
|
|
||||||
BingPrecise,
|
|
||||||
ZhiPuChatGLMLite,
|
|
||||||
}
|
|
||||||
|
|
||||||
func in(value string, slice []string) bool {
|
func in(value string, slice []string) bool {
|
||||||
for _, item := range slice {
|
for _, item := range slice {
|
||||||
if item == value {
|
if item == value {
|
||||||
@ -166,15 +128,3 @@ func IsClaude100KModel(model string) bool {
|
|||||||
func IsMidjourneyFastModel(model string) bool {
|
func IsMidjourneyFastModel(model string) bool {
|
||||||
return model == MidjourneyFast
|
return model == MidjourneyFast
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsSparkDeskModel(model string) bool {
|
|
||||||
return in(model, SparkDeskModelArray)
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsLongContextModel(model string) bool {
|
|
||||||
return in(model, LongContextModelArray)
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsFreeModel(model string) bool {
|
|
||||||
return in(model, FreeModelArray)
|
|
||||||
}
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package manager
|
package manager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"chat/channel"
|
||||||
"chat/globals"
|
"chat/globals"
|
||||||
"chat/utils"
|
"chat/utils"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -28,7 +29,7 @@ func ExtractCacheData(c *gin.Context, props *CacheProps) *CacheData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func SaveCacheData(c *gin.Context, props *CacheProps, data *CacheData) {
|
func SaveCacheData(c *gin.Context, props *CacheProps, data *CacheData) {
|
||||||
if !globals.IsFreeModel(props.Model) {
|
if channel.ChargeInstance.IsBilling(props.Model) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-redis/redis/v8"
|
"github.com/go-redis/redis/v8"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Limiter struct {
|
type Limiter struct {
|
||||||
@ -15,16 +14,10 @@ type Limiter struct {
|
|||||||
Count int64
|
Count int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Limiter) RateLimit(ctx *gin.Context, rds *redis.Client, ip string, path string) bool {
|
func (l *Limiter) RateLimit(client *redis.Client, ip string, path string) bool {
|
||||||
key := fmt.Sprintf("rate%s:%s", path, ip)
|
key := fmt.Sprintf("rate:%s:%s", path, ip)
|
||||||
count, err := rds.Incr(ctx, key).Result()
|
rate := utils.IncrWithLimit(client, key, 1, l.Count, int64(l.Duration))
|
||||||
if err != nil {
|
return !rate
|
||||||
return true
|
|
||||||
}
|
|
||||||
if count == 1 {
|
|
||||||
rds.Expire(ctx, key, time.Duration(l.Duration)*time.Second)
|
|
||||||
}
|
|
||||||
return count > l.Count
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var limits = map[string]Limiter{
|
var limits = map[string]Limiter{
|
||||||
@ -62,7 +55,7 @@ func ThrottleMiddleware() gin.HandlerFunc {
|
|||||||
cache := utils.GetCacheFromContext(c)
|
cache := utils.GetCacheFromContext(c)
|
||||||
admin.IncrRequest(cache)
|
admin.IncrRequest(cache)
|
||||||
limiter := GetPrefixMap[Limiter](path, limits)
|
limiter := GetPrefixMap[Limiter](path, limits)
|
||||||
if limiter != nil && limiter.RateLimit(c, cache, ip, path) {
|
if limiter != nil && limiter.RateLimit(cache, ip, path) {
|
||||||
c.JSON(200, gin.H{"status": false, "reason": "You have sent too many requests. Please try again later."})
|
c.JSON(200, gin.H{"status": false, "reason": "You have sent too many requests. Please try again later."})
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
|
@ -58,6 +58,7 @@ func IncrWithLimit(cache *redis.Client, key string, delta int64, limit int64, ex
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if res > limit {
|
if res > limit {
|
||||||
|
// reset
|
||||||
cache.Set(context.Background(), key, limit, time.Duration(expiration)*time.Second)
|
cache.Set(context.Background(), key, limit, time.Duration(expiration)*time.Second)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"chat/channel"
|
||||||
"chat/globals"
|
"chat/globals"
|
||||||
"github.com/pkoukk/tiktoken-go"
|
"github.com/pkoukk/tiktoken-go"
|
||||||
"strings"
|
"strings"
|
||||||
@ -18,19 +19,10 @@ func GetWeightByModel(model string) int {
|
|||||||
return 2
|
return 2
|
||||||
case globals.GPT3Turbo, globals.GPT3Turbo0613, globals.GPT3Turbo1106,
|
case globals.GPT3Turbo, globals.GPT3Turbo0613, globals.GPT3Turbo1106,
|
||||||
globals.GPT3Turbo16k, globals.GPT3Turbo16k0613,
|
globals.GPT3Turbo16k, globals.GPT3Turbo16k0613,
|
||||||
globals.GPT4, globals.GPT4Vision, globals.GPT4Dalle, globals.GPT4All, globals.GPT40314, globals.GPT40613, globals.GPT41106Preview, globals.GPT41106VisionPreview,
|
globals.GPT4, globals.GPT40314, globals.GPT40613, globals.GPT41106Preview, globals.GPT41106VisionPreview,
|
||||||
globals.GPT432k, globals.GPT432k0613, globals.GPT432k0314,
|
globals.GPT432k, globals.GPT432k0613, globals.GPT432k0314:
|
||||||
globals.LLaMa27B, globals.LLaMa213B, globals.LLaMa270B,
|
|
||||||
globals.CodeLLaMa34B, globals.CodeLLaMa13B, globals.CodeLLaMa7B,
|
|
||||||
|
|
||||||
globals.SparkDesk, globals.SparkDeskV2, globals.SparkDeskV3,
|
|
||||||
globals.QwenTurbo, globals.QwenPlus, globals.QwenTurboNet, globals.QwenPlusNet,
|
|
||||||
globals.BingPrecise, globals.BingCreative, globals.BingBalanced,
|
|
||||||
globals.Hunyuan, globals.GPT360V9, globals.Baichuan53B,
|
|
||||||
globals.SkylarkLite, globals.SkylarkPlus, globals.SkylarkPro, globals.SkylarkChat:
|
|
||||||
return 3
|
return 3
|
||||||
case globals.GPT3Turbo0301, globals.GPT3Turbo16k0301,
|
case globals.GPT3Turbo0301, globals.GPT3Turbo16k0301:
|
||||||
globals.ZhiPuChatGLMTurbo, globals.ZhiPuChatGLMLite, globals.ZhiPuChatGLMStd, globals.ZhiPuChatGLMPro:
|
|
||||||
return 4 // every message follows <|start|>{role/name}\n{content}<|end|>\n
|
return 4 // every message follows <|start|>{role/name}\n{content}<|end|>\n
|
||||||
default:
|
default:
|
||||||
if strings.Contains(model, globals.GPT3Turbo) {
|
if strings.Contains(model, globals.GPT3Turbo) {
|
||||||
@ -47,7 +39,6 @@ func GetWeightByModel(model string) int {
|
|||||||
return GetWeightByModel(globals.Claude2100k)
|
return GetWeightByModel(globals.Claude2100k)
|
||||||
} else {
|
} else {
|
||||||
// not implemented: See https://github.com/openai/openai-python/blob/main/chatml.md for information on how messages are converted to tokens
|
// not implemented: See https://github.com/openai/openai-python/blob/main/chatml.md for information on how messages are converted to tokens
|
||||||
//panic(fmt.Errorf("not implemented for model %s", model))
|
|
||||||
return 3
|
return 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -76,107 +67,23 @@ func CountTokenPrice(messages []globals.Message, model string) int {
|
|||||||
return NumTokensFromMessages(messages, model)
|
return NumTokensFromMessages(messages, model)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CountInputToken(model string, v []globals.Message) float32 {
|
func CountInputToken(model string, message []globals.Message) float32 {
|
||||||
switch model {
|
charge := channel.ChargeInstance.GetCharge(model)
|
||||||
case globals.GPT3Turbo, globals.GPT3Turbo0613, globals.GPT3Turbo0301, globals.GPT3TurboInstruct, globals.GPT3Turbo1106,
|
|
||||||
globals.GPT3Turbo16k, globals.GPT3Turbo16k0613, globals.GPT3Turbo16k0301:
|
if charge.IsBillingType(globals.TokenBilling) {
|
||||||
return 0
|
return float32(CountTokenPrice(message, model)) / 1000 * charge.GetInput()
|
||||||
case globals.GPT41106Preview, globals.GPT41106VisionPreview:
|
|
||||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.7 * 0.6
|
|
||||||
case globals.GPT4, globals.GPT4Vision, globals.GPT4All, globals.GPT4Dalle, globals.GPT40314, globals.GPT40613:
|
|
||||||
return float32(CountTokenPrice(v, model)) / 1000 * 2.1 * 0.6
|
|
||||||
case globals.GPT432k, globals.GPT432k0613, globals.GPT432k0314:
|
|
||||||
return float32(CountTokenPrice(v, model)) / 1000 * 4.2
|
|
||||||
case globals.SparkDesk:
|
|
||||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.15
|
|
||||||
case globals.SparkDeskV2, globals.SparkDeskV3:
|
|
||||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.3
|
|
||||||
case globals.Claude1, globals.Claude2:
|
|
||||||
return 0
|
|
||||||
case globals.Claude1100k, globals.Claude2100k, globals.Claude2200k:
|
|
||||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.8
|
|
||||||
case globals.LLaMa270B, globals.CodeLLaMa34B:
|
|
||||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.25
|
|
||||||
case globals.LLaMa213B, globals.CodeLLaMa13B, globals.LLaMa27B, globals.CodeLLaMa7B:
|
|
||||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.1
|
|
||||||
case globals.ZhiPuChatGLMPro:
|
|
||||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.1
|
|
||||||
case globals.ZhiPuChatGLMTurbo, globals.ZhiPuChatGLMStd:
|
|
||||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.05
|
|
||||||
case globals.QwenTurbo, globals.QwenTurboNet:
|
|
||||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.08
|
|
||||||
case globals.QwenPlus, globals.QwenPlusNet:
|
|
||||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.2
|
|
||||||
case globals.Hunyuan:
|
|
||||||
return float32(CountTokenPrice(v, model)) / 1000 * 1
|
|
||||||
case globals.GPT360V9:
|
|
||||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.12
|
|
||||||
case globals.Baichuan53B:
|
|
||||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.2
|
|
||||||
case globals.SkylarkLite:
|
|
||||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.04
|
|
||||||
case globals.SkylarkPlus:
|
|
||||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.08
|
|
||||||
case globals.SkylarkPro, globals.SkylarkChat:
|
|
||||||
return float32(CountTokenPrice(v, model)) / 1000 * 0.11
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func CountOutputToken(model string, t int) float32 {
|
|
||||||
switch model {
|
|
||||||
case globals.GPT3Turbo, globals.GPT3Turbo0613, globals.GPT3Turbo0301, globals.GPT3TurboInstruct, globals.GPT3Turbo1106,
|
|
||||||
globals.GPT3Turbo16k, globals.GPT3Turbo16k0613, globals.GPT3Turbo16k0301:
|
|
||||||
return 0
|
return 0
|
||||||
case globals.GPT41106Preview, globals.GPT41106VisionPreview:
|
}
|
||||||
return float32(t*GetWeightByModel(model)) / 1000 * 2.1 * 0.6
|
|
||||||
case globals.GPT4, globals.GPT4Vision, globals.GPT4All, globals.GPT4Dalle, globals.GPT40314, globals.GPT40613:
|
func CountOutputToken(model string, token int) float32 {
|
||||||
return float32(t*GetWeightByModel(model)) / 1000 * 4.3 * 0.6
|
charge := channel.ChargeInstance.GetCharge(model)
|
||||||
case globals.GPT432k, globals.GPT432k0613, globals.GPT432k0314:
|
switch charge.GetType() {
|
||||||
return float32(t*GetWeightByModel(model)) / 1000 * 8.6
|
case globals.TokenBilling:
|
||||||
case globals.SparkDesk:
|
return float32(token*GetWeightByModel(model)) / 1000 * charge.GetOutput()
|
||||||
return float32(t*GetWeightByModel(model)) / 1000 * 0.15
|
case globals.TimesBilling:
|
||||||
case globals.SparkDeskV2, globals.SparkDeskV3:
|
return charge.GetOutput()
|
||||||
return float32(t*GetWeightByModel(model)) / 1000 * 0.3
|
|
||||||
case globals.Claude1, globals.Claude2:
|
|
||||||
return 0
|
|
||||||
case globals.Claude1100k, globals.Claude2100k, globals.Claude2200k:
|
|
||||||
return float32(t*GetWeightByModel(model)) / 1000 * 2.4
|
|
||||||
case globals.LLaMa270B, globals.CodeLLaMa34B:
|
|
||||||
return float32(t*GetWeightByModel(model)) / 1000 * 0.25
|
|
||||||
case globals.LLaMa213B, globals.CodeLLaMa13B, globals.LLaMa27B, globals.CodeLLaMa7B:
|
|
||||||
return float32(t*GetWeightByModel(model)) / 1000 * 0.1
|
|
||||||
case globals.ZhiPuChatGLMPro:
|
|
||||||
return float32(t*GetWeightByModel(model)) / 1000 * 0.1
|
|
||||||
case globals.ZhiPuChatGLMTurbo, globals.ZhiPuChatGLMStd:
|
|
||||||
return float32(t*GetWeightByModel(model)) / 1000 * 0.05
|
|
||||||
case globals.QwenTurbo, globals.QwenTurboNet:
|
|
||||||
return float32(t*GetWeightByModel(model)) / 1000 * 0.08
|
|
||||||
case globals.QwenPlus, globals.QwenPlusNet:
|
|
||||||
return float32(t*GetWeightByModel(model)) / 1000 * 0.2
|
|
||||||
case globals.Hunyuan:
|
|
||||||
return float32(t*GetWeightByModel(model)) / 1000 * 1
|
|
||||||
case globals.GPT360V9:
|
|
||||||
return float32(t*GetWeightByModel(model)) / 1000 * 0.12
|
|
||||||
case globals.Baichuan53B:
|
|
||||||
return float32(t*GetWeightByModel(model)) / 1000 * 0.2
|
|
||||||
case globals.SkylarkLite:
|
|
||||||
return float32(t*GetWeightByModel(model)) / 1000 * 0.04
|
|
||||||
case globals.SkylarkPlus:
|
|
||||||
return float32(t*GetWeightByModel(model)) / 1000 * 0.08
|
|
||||||
case globals.SkylarkPro, globals.SkylarkChat:
|
|
||||||
return float32(t*GetWeightByModel(model)) / 1000 * 0.11
|
|
||||||
case globals.StableDiffusion:
|
|
||||||
return 0.25
|
|
||||||
case globals.Midjourney:
|
|
||||||
return 0.1
|
|
||||||
case globals.MidjourneyFast:
|
|
||||||
return 0.5
|
|
||||||
case globals.MidjourneyTurbo:
|
|
||||||
return 1
|
|
||||||
case globals.Dalle3:
|
|
||||||
return 5.6
|
|
||||||
default:
|
default:
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user