Commit 19fc384e by CaIon

feat(performance): update performance metrics handling and UI components

parent ba474393
......@@ -53,9 +53,10 @@
> This is an open-source project developed based on [One API](https://github.com/songquanpeng/one-api)
> [!IMPORTANT]
> - This project is for personal learning purposes only, with no guarantee of stability or technical support
> - Users must comply with OpenAI's [Terms of Use](https://openai.com/policies/terms-of-use) and **applicable laws and regulations**, and must not use it for illegal purposes
> - According to the [《Interim Measures for the Management of Generative Artificial Intelligence Services》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm), please do not provide any unregistered generative AI services to the public in China.
> - This project is intended solely for lawful and authorized AI API gateway, organization-level authentication, multi-model management, usage analytics, cost accounting, and private deployment scenarios.
> - Users must lawfully obtain upstream API keys, accounts, model services, and interface permissions, and must comply with upstream terms of service and applicable laws and regulations.
> - Users should ensure their use complies with upstream terms of service and applicable laws and regulations.
> - When providing generative AI services to the public, users should comply with applicable regulatory requirements and fulfill all filing, licensing, content safety, real-name verification, log retention, tax, and upstream authorization obligations required by their jurisdiction.
---
......@@ -146,6 +147,9 @@ docker run --name new-api -d --restart always \
🎉 After deployment is complete, visit `http://localhost:3000` to start using!
> [!WARNING]
> When operating this project as a public generative AI service or API resale service, users should first complete all required filing, licensing, content safety, real-name verification, log retention, tax, payment, and upstream authorization obligations.
📖 For more deployment methods, please refer to [Deployment Guide](https://docs.newapi.pro/en/docs/installation)
---
......@@ -184,12 +188,12 @@ docker run --name new-api -d --restart always \
| 📈 Data Dashboard | Visual console and statistical analysis |
| 🔒 Permission Management | Token grouping, model restrictions, user management |
### 💰 Payment and Billing
### 💰 Authorized Usage Accounting and Billing
-Online recharge (EPay, Stripe)
-Pay-per-use model pricing
- ✅ Cache billing support (OpenAI, Azure, DeepSeek, Claude, Qwen and all supported models)
- ✅ Flexible billing policy configuration
-Internal top-up and quota allocation for lawful authorized scenarios (EPay, Stripe)
-Organization-level per-request, usage-based, and cache-hit cost accounting
- ✅ Cache billing statistics for OpenAI, Azure, DeepSeek, Claude, Qwen, and supported models
- ✅ Flexible billing policies for internal management or authorized enterprise customers
### 🔐 Authorization and Security
......@@ -248,7 +252,7 @@ docker run --name new-api -d --restart always \
## 🤖 Model Support
> For details, please refer to [API Documentation - Relay Interface](https://docs.newapi.pro/en/docs/api)
> For details, please refer to [API Documentation - Gateway Interface](https://docs.newapi.pro/en/docs/api)
| Model Type | Description | Documentation |
|---------|------|------|
......@@ -259,7 +263,7 @@ docker run --name new-api -d --restart always \
| 💬 Claude | Messages format | [Documentation](https://docs.newapi.pro/en/docs/api/ai-model/chat/create-message) |
| 🌐 Gemini | Google Gemini format | [Documentation](https://doc.newapi.pro/en/api/google-gemini-chat) |
| 🔧 Dify | ChatFlow mode | - |
| 🎯 Custom | Supports complete call address | - |
| 🎯 Custom upstream | Supports configuring legally authorized upstream endpoints | - |
### 📡 Supported Interfaces
......
......@@ -55,9 +55,10 @@
## 📝 Description du projet
> [!IMPORTANT]
> - Ce projet est uniquement destiné à des fins d'apprentissage personnel, sans garantie de stabilité ni de support technique.
> - Les utilisateurs doivent se conformer aux [Conditions d'utilisation](https://openai.com/policies/terms-of-use) d'OpenAI et aux **lois et réglementations applicables**, et ne doivent pas l'utiliser à des fins illégales.
> - Conformément aux [《Mesures provisoires pour la gestion des services d'intelligence artificielle générative》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm), veuillez ne fournir aucun service d'IA générative non enregistré au public en Chine.
> - Ce projet est exclusivement destiné aux scénarios de passerelle API d'IA légalement autorisés, d'authentification organisationnelle, de gestion multi-modèles, d'analyse d'utilisation, de comptabilisation des coûts et de déploiement privé.
> - Les utilisateurs doivent obtenir légalement les clés API, comptes, services de modèles et autorisations d'interface en amont, et doivent respecter les conditions d'utilisation en amont et les lois et réglementations applicables.
> - Les utilisateurs doivent s'assurer que leur utilisation est conforme aux conditions d'utilisation en amont et aux lois et réglementations applicables.
> - Lors de la fourniture de services d'IA générative au public, les utilisateurs doivent se conformer aux exigences réglementaires applicables et remplir toutes les obligations d'enregistrement, de licence, de sécurité du contenu, de vérification d'identité, de conservation des journaux, de fiscalité et d'autorisation en amont requises par leur juridiction.
---
......@@ -151,6 +152,9 @@ docker run --name new-api -d --restart always \
🎉 Après le déploiement, visitez `http://localhost:3000` pour commencer à utiliser!
> [!WARNING]
> Lorsque vous exploitez ce projet en tant que service public d'IA générative ou service de revente d'API, les utilisateurs doivent d'abord remplir toutes les obligations requises en matière d'enregistrement, de licence, de sécurité du contenu, de vérification d'identité, de conservation des journaux, de fiscalité, de paiement et d'autorisation en amont.
📖 Pour plus de méthodes de déploiement, veuillez vous référer à [Guide de déploiement](https://docs.newapi.pro/en/docs/installation)
---
......@@ -189,12 +193,12 @@ docker run --name new-api -d --restart always \
| 📈 Tableau de bord des données | Console visuelle et analyse statistique |
| 🔒 Gestion des permissions | Regroupement de jetons, restrictions de modèles, gestion des utilisateurs |
### 💰 Paiement et facturation
### 💰 Comptabilisation et facturation des usages autorisés
- ✅ Recharge en ligne (EPay, Stripe)
-Tarification des modèles de paiement à l'utilisation
-Prise en charge de la facturation du cache (OpenAI, Azure, DeepSeek, Claude, Qwen et tous les modèles pris en charge)
-Configuration flexible des politiques de facturation
- ✅ Rechargement interne et allocation de quotas pour les scénarios légalement autorisés (EPay, Stripe)
-Comptabilisation des coûts par requête, par utilisation et par hit de cache au niveau organisationnel
-Statistiques de facturation du cache pour OpenAI, Azure, DeepSeek, Claude, Qwen et les modèles pris en charge
-Politiques de facturation flexibles pour la gestion interne ou les clients entreprise autorisés
### 🔐 Autorisation et sécurité
......@@ -254,7 +258,7 @@ docker run --name new-api -d --restart always \
## 🤖 Prise en charge des modèles
> Pour les détails, veuillez vous référer à [Documentation de l'API - Interface de relais](https://docs.newapi.pro/en/docs/api)
> Pour les détails, veuillez vous référer à [Documentation de l'API - Interface de passerelle](https://docs.newapi.pro/en/docs/api)
| Type de modèle | Description | Documentation |
|---------|------|------|
......@@ -266,7 +270,7 @@ docker run --name new-api -d --restart always \
| 💬 Claude | Format Messages | [Documentation](https://docs.newapi.pro/en/docs/api/ai-model/chat/createmessage) |
| 🌐 Gemini | Format Google Gemini | [Documentation](https://docs.newapi.pro/en/docs/api/ai-model/chat/gemini/geminirelayv1beta) |
| 🔧 Dify | Mode ChatFlow | - |
| 🎯 Personnalisé | Prise en charge de l'adresse d'appel complète | - |
| 🎯 Amont personnalisé | Configuration des points d'accès amont légalement autorisés | - |
### 📡 Interfaces prises en charge
......
......@@ -55,9 +55,10 @@
## 📝 プロジェクト説明
> [!IMPORTANT]
> - 本プロジェクトは個人学習用のみであり、安定性の保証や技術サポートは提供しません。
> - ユーザーは、OpenAIの[利用規約](https://openai.com/policies/terms-of-use)および**法律法規**を遵守する必要があり、違法な目的で使用してはいけません。
> - [《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)の要求に従い、中国地域の公衆に未登録の生成式AI サービスを提供しないでください。
> - 本プロジェクトは、合法的に許可された AI API ゲートウェイ、組織レベルの認証、マルチモデル管理、利用量分析、コスト管理、プライベートデプロイのシナリオのみを対象としています。
> - ユーザーは、上流の API キー、アカウント、モデルサービス、インターフェース権限を合法的に取得し、上流のサービス利用規約および適用される法律法規を遵守する必要があります。
> - ユーザーは、利用方法が上流のサービス利用規約および適用される法律法規に準拠していることを確認してください。
> - 生成 AI サービスを公衆に提供する場合、ユーザーは適用される規制要件を遵守し、管轄区域で求められる届出、ライセンス、コンテンツセキュリティ、本人確認、ログ保持、税務、上流認可などのすべての義務を履行してください。
---
......@@ -151,6 +152,9 @@ docker run --name new-api -d --restart always \
🎉 デプロイが完了したら、`http://localhost:3000` にアクセスして使用を開始してください!
> [!WARNING]
> 本プロジェクトを公衆向け生成 AI サービスまたは API 再販サービスとして運営する場合、ユーザーは届出、コンテンツセキュリティ、本人確認、ログ保持、税務、決済、上流認可などの必要なコンプライアンス義務を先に完了してください。
📖 その他のデプロイ方法については[デプロイガイド](https://docs.newapi.pro/ja/docs/installation)を参照してください。
---
......@@ -189,12 +193,12 @@ docker run --name new-api -d --restart always \
| 📈 データダッシュボード | ビジュアルコンソールと統計分析 |
| 🔒 権限管理 | トークングループ化、モデル制限、ユーザー管理 |
### 💰 支払いと課金
### 💰 認可済み利用量とコスト管理
-オンライン充電(EPay、Stripe)
-モデルの従量課金
-キャッシュ課金サポート(OpenAI、Azure、DeepSeek、Claude、Qwenなどすべてのサポートされているモデル)
-柔軟な課金ポリシー設定
-合法的に許可されたシナリオでの内部チャージとクォータ割り当て(EPay、Stripe)
-組織レベルのリクエスト単位、使用量ベース、キャッシュヒットのコスト会計
-OpenAI、Azure、DeepSeek、Claude、Qwen などのモデルのキャッシュ課金統計
-内部管理または認可済み企業顧客向けの柔軟な課金ポリシー
### 🔐 認証とセキュリティ
......@@ -256,7 +260,7 @@ docker run --name new-api -d --restart always \
## 🤖 モデルサポート
> 詳細については[APIドキュメント - 中継インターフェース](https://docs.newapi.pro/ja/docs/api)
> 詳細については[APIドキュメント - ゲートウェイインターフェース](https://docs.newapi.pro/ja/docs/api)
| モデルタイプ | 説明 | ドキュメント |
|---------|------|------|
......@@ -268,7 +272,7 @@ docker run --name new-api -d --restart always \
| 💬 Claude | Messagesフォーマット | [ドキュメント](https://docs.newapi.pro/ja/docs/api/ai-model/chat/createmessage) |
| 🌐 Gemini | Google Geminiフォーマット | [ドキュメント](https://docs.newapi.pro/ja/docs/api/ai-model/chat/gemini/geminirelayv1beta) |
| 🔧 Dify | ChatFlowモード | - |
| 🎯 カスタム | 完全な呼び出しアドレスの入力をサポート | - |
| 🎯 カスタム上流 | 合法的に許可された上流エンドポイントの設定をサポート | - |
### 📡 サポートされているインターフェース
......
......@@ -55,9 +55,10 @@
## 📝 Project Description
> [!IMPORTANT]
> - This project is for personal learning purposes only, with no guarantee of stability or technical support
> - Users must comply with OpenAI's [Terms of Use](https://openai.com/policies/terms-of-use) and **applicable laws and regulations**, and must not use it for illegal purposes
> - According to the [《Interim Measures for the Management of Generative Artificial Intelligence Services》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm), please do not provide any unregistered generative AI services to the public in China.
> - This project is intended solely for lawful and authorized AI API gateway, organization-level authentication, multi-model management, usage analytics, cost accounting, and private deployment scenarios.
> - Users must lawfully obtain upstream API keys, accounts, model services, and interface permissions, and must comply with upstream terms of service and applicable laws and regulations.
> - Users should ensure their use complies with upstream terms of service and applicable laws and regulations.
> - When providing generative AI services to the public, users should comply with applicable regulatory requirements and fulfill all filing, licensing, content safety, real-name verification, log retention, tax, and upstream authorization obligations required by their jurisdiction.
---
......@@ -151,6 +152,9 @@ docker run --name new-api -d --restart always \
🎉 After deployment is complete, visit `http://localhost:3000` to start using!
> [!WARNING]
> When operating this project as a public generative AI service or API resale service, users should first complete all required filing, licensing, content safety, real-name verification, log retention, tax, payment, and upstream authorization obligations.
📖 For more deployment methods, please refer to [Deployment Guide](https://docs.newapi.pro/en/docs/installation)
---
......@@ -189,12 +193,12 @@ docker run --name new-api -d --restart always \
| 📈 Data Dashboard | Visual console and statistical analysis |
| 🔒 Permission Management | Token grouping, model restrictions, user management |
### 💰 Payment and Billing
### 💰 Authorized Usage Accounting and Billing
-Online recharge (EPay, Stripe)
-Pay-per-use model pricing
- ✅ Cache billing support (OpenAI, Azure, DeepSeek, Claude, Qwen and all supported models)
- ✅ Flexible billing policy configuration
-Internal top-up and quota allocation for lawful authorized scenarios (EPay, Stripe)
-Organization-level per-request, usage-based, and cache-hit cost accounting
- ✅ Cache billing statistics for OpenAI, Azure, DeepSeek, Claude, Qwen, and supported models
- ✅ Flexible billing policies for internal management or authorized enterprise customers
### 🔐 Authorization and Security
......@@ -254,7 +258,7 @@ docker run --name new-api -d --restart always \
## 🤖 Model Support
> For details, please refer to [API Documentation - Relay Interface](https://docs.newapi.pro/en/docs/api)
> For details, please refer to [API Documentation - Gateway Interface](https://docs.newapi.pro/en/docs/api)
| Model Type | Description | Documentation |
|---------|------|------|
......@@ -266,7 +270,7 @@ docker run --name new-api -d --restart always \
| 💬 Claude | Messages format | [Documentation](https://docs.newapi.pro/en/docs/api/ai-model/chat/createmessage) |
| 🌐 Gemini | Google Gemini format | [Documentation](https://docs.newapi.pro/en/docs/api/ai-model/chat/gemini/geminirelayv1beta) |
| 🔧 Dify | ChatFlow mode | - |
| 🎯 Custom | Supports complete call address | - |
| 🎯 Custom upstream | Supports configuring legally authorized upstream endpoints | - |
### 📡 Supported Interfaces
......
......@@ -55,9 +55,10 @@
## 📝 项目说明
> [!IMPORTANT]
> - 本项目仅供个人学习使用,不保证稳定性,且不提供任何技术支持
> - 使用者必须在遵循 OpenAI 的 [使用条款](https://openai.com/policies/terms-of-use) 以及**法律法规**的情况下使用,不得用于非法用途
> - 根据 [《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm) 的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务
> - 本项目仅面向合法授权的 AI API 网关、组织内部鉴权、多模型管理、用量统计、成本核算和私有化部署场景。
> - 使用者必须合法取得上游 API Key、账号、模型服务或接口权限,并遵守上游服务条款及适用法律法规。
> - 使用者应确保其使用方式符合上游服务条款及适用法律法规。
> - 面向公众提供生成式人工智能服务时,使用者应遵守[《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)等监管要求,自行完成所在司法辖区要求的备案、许可、内容安全、实名、日志留存、税务和上游授权等合规义务。
---
......@@ -151,6 +152,9 @@ docker run --name new-api -d --restart always \
🎉 部署完成后,访问 `http://localhost:3000` 即可使用!
> [!WARNING]
> 将本项目作为面向公众的生成式 AI 服务或 API 转售服务运营时,使用者应先完成备案、内容安全、实名、日志留存、税务、支付和上游授权等合规义务。
📖 更多部署方式请参考 [部署指南](https://docs.newapi.pro/zh/docs/installation)
---
......@@ -189,12 +193,12 @@ docker run --name new-api -d --restart always \
| 📈 数据看板 | 可视化控制台与统计分析 |
| 🔒 权限管理 | 令牌分组、模型限制、用户管理 |
### 💰 支付与计费
### 💰 授权用量与成本管理
-在线充值(易支付、Stripe)
-模型按次数收费
-缓存计费支持(OpenAI、Azure、DeepSeek、Claude、Qwen等所有支持的模型)
-灵活的计费策略配置
-合法授权场景下的内部充值与额度分配(易支付、Stripe)
-组织内按次、按量或缓存命中成本核算
-支持 OpenAI、Azure、DeepSeek、Claude、Qwen 等模型的缓存计费统计
-面向内部管理或企业客户的灵活计费策略配置
### 🔐 授权与安全
......@@ -254,7 +258,7 @@ docker run --name new-api -d --restart always \
## 🤖 模型支持
> 详情请参考 [接口文档 - 中继接口](https://docs.newapi.pro/zh/docs/api)
> 详情请参考 [接口文档 - 网关接口](https://docs.newapi.pro/zh/docs/api)
| 模型类型 | 说明 | 文档 |
|---------|------|------|
......@@ -266,7 +270,7 @@ docker run --name new-api -d --restart always \
| 💬 Claude | Messages 格式 | [文档](https://docs.newapi.pro/zh/docs/api/ai-model/chat/createmessage) |
| 🌐 Gemini | Google Gemini 格式 | [文档](https://docs.newapi.pro/zh/docs/api/ai-model/chat/gemini/geminirelayv1beta) |
| 🔧 Dify | ChatFlow 模式 | - |
| 🎯 自定义 | 支持完整调用地址 | - |
| 🎯 自定义上游 | 支持配置合法授权的上游接口地址 | - |
### 📡 支持的接口
......
......@@ -55,9 +55,10 @@
## 📝 項目說明
> [!IMPORTANT]
> - 本項目僅供個人學習使用,不保證穩定性,且不提供任何技術支援
> - 使用者必須在遵循 OpenAI 的 [使用條款](https://openai.com/policies/terms-of-use) 以及**法律法規**的情況下使用,不得用於非法用途
> - 根據 [《生成式人工智慧服務管理暫行辦法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm) 的要求,請勿對中國地區公眾提供一切未經備案的生成式人工智慧服務
> - 本專案僅面向合法授權的 AI API 閘道、組織內部鑑權、多模型管理、用量統計、成本核算和私有化部署場景。
> - 使用者必須合法取得上游 API Key、帳號、模型服務或介面權限,並遵守上游服務條款及適用法律法規。
> - 使用者應確保其使用方式符合上游服務條款及適用法律法規。
> - 面向公眾提供生成式人工智慧服務時,使用者應遵守[《生成式人工智慧服務管理暫行辦法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)等監管要求,自行完成所在司法轄區要求的備案、許可、內容安全、實名、日誌留存、稅務和上游授權等合規義務。
---
......@@ -151,6 +152,9 @@ docker run --name new-api -d --restart always \
🎉 部署完成後,訪問 `http://localhost:3000` 即可使用!
> [!WARNING]
> 將本專案作為面向公眾的生成式 AI 服務或 API 轉售服務運營時,使用者應先完成備案、內容安全、實名、日誌留存、稅務、支付和上游授權等合規義務。
📖 更多部署方式請參考 [部署指南](https://docs.newapi.pro/zh/docs/installation)
---
......@@ -189,12 +193,12 @@ docker run --name new-api -d --restart always \
| 📈 數據看板 | 視覺化控制檯與統計分析 |
| 🔒 權限管理 | 令牌分組、模型限制、用戶管理 |
### 💰 支付與計費
### 💰 授權用量與成本管理
-在線儲值(易支付、Stripe)
-模型按次數收費
-快取計費支援(OpenAI、Azure、DeepSeek、Claude、Qwen等所有支援的模型)
-靈活的計費策略配置
-合法授權場景下的內部儲值與額度分配(易支付、Stripe)
-組織內按次、按量或快取命中成本核算
-支援 OpenAI、Azure、DeepSeek、Claude、Qwen 等模型的快取計費統計
-面向內部管理或企業客戶的靈活計費策略配置
### 🔐 授權與安全
......@@ -254,7 +258,7 @@ docker run --name new-api -d --restart always \
## 🤖 模型支援
> 詳情請參考 [接口文件 - 中繼接口](https://docs.newapi.pro/zh/docs/api)
> 詳情請參考 [接口文件 - 閘道接口](https://docs.newapi.pro/zh/docs/api)
| 模型類型 | 說明 | 文件 |
|---------|------|------|
......@@ -266,7 +270,7 @@ docker run --name new-api -d --restart always \
| 💬 Claude | Messages 格式 | [文件](https://docs.newapi.pro/zh/docs/api/ai-model/chat/createmessage) |
| 🌐 Gemini | Google Gemini 格式 | [文件](https://docs.newapi.pro/zh/docs/api/ai-model/chat/gemini/geminirelayv1beta) |
| 🔧 Dify | ChatFlow 模式 | - |
| 🎯 自訂 | 支援完整調用位址 | - |
| 🎯 自訂上游 | 支援配置合法授權的上游介面位址 | - |
### 📡 支援的接口
......
......@@ -187,7 +187,7 @@ func QuerySummaryAll(hours int) (SummaryAllResult, error) {
})
}
sort.Slice(models, func(i, j int) bool {
return models[i].ModelName < models[j].ModelName
return models[i].RequestCount > models[j].RequestCount
})
return SummaryAllResult{Models: models}, nil
......
......@@ -52,7 +52,7 @@ type ModelSummary struct {
AvgLatencyMs int64 `json:"avg_latency_ms"`
SuccessRate float64 `json:"success_rate"`
AvgTps float64 `json:"avg_tps"`
RequestCount int64 `json:"request_count"`
RequestCount int64 `json:"-"`
}
type SummaryAllResult struct {
......
......@@ -1927,7 +1927,7 @@
"更多": "Expand more",
"更多信息请参考": "For more information, please refer to",
"更多参数请参考": "For more parameters, please refer to",
"更好的价格,更好的稳定性,只需要将模型基址替换为:": "Better price, better stability, no subscription required, just replace the model BASE URL with: ",
"多模型统一接入,只需将基址替换为:": "Unified multi-model access, just replace the base URL with: ",
"更新": "Update",
"更新 Creem 设置": "Update Creem Settings",
"更新 Stripe 设置": "Update Stripe settings",
......
......@@ -1920,7 +1920,7 @@
"更多": "Développer plus",
"更多信息请参考": "Pour plus d'informations, veuillez vous référer à",
"更多参数请参考": "Pour plus de paramètres, veuillez vous référer à",
"更好的价格,更好的稳定性,只需要将模型基址替换为:": "Meilleur prix, meilleure stabilité, aucun abonnement requis, il suffit de remplacer l'URL de BASE du modèle par : ",
"多模型统一接入,只需将基址替换为:": "Accès unifié multi-modèles, remplacez simplement l'URL de base par : ",
"更新": "Mettre à jour",
"更新 Creem 设置": "Mettre à jour les paramètres Creem",
"更新 Stripe 设置": "Mettre à jour les paramètres Stripe",
......
......@@ -1891,7 +1891,7 @@
"更多": "もっと見る",
"更多信息请参考": "詳細については、こちらをご参照ください",
"更多参数请参考": "その他のパラメータについては、こちらをご参照ください",
"更好的价格,更好的稳定性,只需要将模型基址替换为:": "よりお得な料金、さらに向上した安定性。モデルのベースURLを以下に置換するだけで、すぐにご利用いただけます:",
"多模型统一接入,只需将基址替换为:": "マルチモデル統一アクセス。ベースURLを以下に置換するだけでご利用いただけます:",
"更新": "更新",
"更新 Creem 设置": "Update Creem Settings",
"更新 Stripe 设置": "Stripe設定の更新",
......
......@@ -1938,7 +1938,7 @@
"更多": "Больше",
"更多信息请参考": "Для получения дополнительной информации см.",
"更多参数请参考": "Для получения дополнительных параметров см.",
"更好的价格,更好的稳定性,只需要将模型基址替换为:": "Лучшая цена, лучшая стабильность, просто замените базовый адрес модели на:",
"多模型统一接入,只需将基址替换为:": "Единый доступ к множеству моделей, просто замените базовый адрес на:",
"更新": "Обновить",
"更新 Creem 设置": "Обновить настройки Creem",
"更新 Stripe 设置": "Обновить настройки Stripe",
......
......@@ -1892,7 +1892,7 @@
"更多": "Mở rộng thêm",
"更多信息请参考": "Để biết thêm thông tin, vui lòng tham khảo",
"更多参数请参考": "Để biết thêm tham số, vui lòng tham khảo",
"更好的价格,更好的稳定性,只需要将模型基址替换为:": "Giá tốt hơn, ổn định hơn, không cần đăng ký, chỉ cần thay thế URL CƠ SỞ mô hình bằng: ",
"多模型统一接入,只需将基址替换为:": "Truy cập thống nhất đa mô hình, chỉ cần thay thế URL cơ sở bằng: ",
"更新": "Cập nhật",
"更新 Creem 设置": "Cập nhật cài đặt Creem",
"更新 Stripe 设置": "Cập nhật cài đặt Stripe",
......
......@@ -1886,7 +1886,7 @@
"更多": "更多",
"更多信息请参考": "更多信息请参考",
"更多参数请参考": "更多参数请参考",
"更好的价格,更好的稳定性,只需要将模型基址替换为:": "更好的价格,更好的稳定性,只需要将模型基址替换为:",
"多模型统一接入,只需将基址替换为:": "多模型统一接入,只需将基址替换为:",
"更新": "更新",
"更新 Creem 设置": "更新 Creem 设置",
"更新 Stripe 设置": "更新 Stripe 设置",
......
......@@ -1896,7 +1896,7 @@
"更多": "更多",
"更多信息请参考": "更多資訊請參考",
"更多参数请参考": "更多參數請參考",
"更好的价格,更好的稳定性,只需要将模型基址替换为:": "更好的價格,更好的穩定性,只需要將模型基址替換為:",
"多模型统一接入,只需将基址替换为:": "多模型統一接入,只需將基址替換為:",
"更新": "更新",
"更新 Creem 设置": "更新 Creem 設定",
"更新 Stripe 设置": "更新 Stripe 設定",
......
......@@ -1304,7 +1304,7 @@
"更多": "更多",
"更多信息请参考": "更多信息请参考",
"更多参数请参考": "更多参数请参考",
"更好的价格,更好的稳定性,只需要将模型基址替换为:": "更好的价格,更好的稳定性,只需要将模型基址替换为:",
"多模型统一接入,只需将基址替换为:": "多模型统一接入,只需将基址替换为:",
"更新": "更新",
"更新 Creem 设置": "更新 Creem 设置",
"更新 Stripe 设置": "更新 Stripe 设置",
......
......@@ -176,7 +176,7 @@ const Home = () => {
</>
</h1>
<p className='text-base md:text-lg lg:text-xl text-semi-color-text-1 mt-4 md:mt-6 max-w-xl'>
{t('更好的价格,更好的稳定性,只需要将模型基址替换为:')}
{t('多模型统一接入,只需将基址替换为:')}
</p>
{/* BASE URL 与端点选择 */}
<div className='flex flex-col md:flex-row items-center justify-center gap-4 w-full mt-4 md:mt-6 max-w-md'>
......
......@@ -56,6 +56,7 @@ import { useApiInfo } from '../../hooks/use-status-data'
import { AnnouncementsPanel } from './announcements-panel'
import { ApiInfoPanel } from './api-info-panel'
import { FAQPanel } from './faq-panel'
import { PerformanceHealthPanel } from './performance-health-panel'
import { SummaryCards } from './summary-cards'
import { UptimePanel } from './uptime-panel'
......@@ -716,6 +717,11 @@ export function OverviewDashboard() {
<CardStaggerContainer className='grid grid-cols-1 gap-4 xl:grid-cols-[minmax(0,1fr)_22rem]'>
<div className='grid min-w-0 grid-cols-1 gap-4 lg:grid-cols-2'>
{isAdmin && (
<CardStaggerItem className='lg:col-span-2'>
<PerformanceHealthPanel />
</CardStaggerItem>
)}
<CardStaggerItem>
<ApiInfoPanel />
</CardStaggerItem>
......
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useMemo } from 'react'
import { useQuery } from '@tanstack/react-query'
import { Gauge, HeartPulse, Timer } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/lib/utils'
import { Skeleton } from '@/components/ui/skeleton'
import { getPerfMetricsSummary } from '@/features/performance-metrics/api'
import {
formatLatency,
formatThroughput,
formatUptimePct,
} from '@/features/performance-metrics/lib/format'
import type { PerfModelSummary } from '@/features/performance-metrics/types'
const PERFORMANCE_WINDOW_HOURS = 24
const TOP_MODEL_LIMIT = 5
type WeightedMetric = 'avg_latency_ms' | 'avg_tps' | 'success_rate'
function simpleAverage(
rows: PerfModelSummary[],
metric: WeightedMetric,
isValid: (value: number) => boolean
): number {
let total = 0
let count = 0
for (const row of rows) {
const value = Number(row[metric])
if (!isValid(value)) continue
total += value
count++
}
return count > 0 ? total / count : NaN
}
function rateTextClass(rate: number): string {
if (!Number.isFinite(rate)) return 'text-muted-foreground'
if (rate >= 99.9) return 'text-success'
if (rate >= 99) return 'text-warning'
return 'text-destructive'
}
function rateDotClass(rate: number): string {
if (!Number.isFinite(rate)) return 'bg-muted-foreground'
if (rate >= 99.9) return 'bg-success'
if (rate >= 99) return 'bg-warning'
return 'bg-destructive'
}
export function PerformanceHealthPanel() {
const { t } = useTranslation()
const metricsQuery = useQuery({
queryKey: ['perf-metrics-summary', PERFORMANCE_WINDOW_HOURS],
queryFn: () => getPerfMetricsSummary(PERFORMANCE_WINDOW_HOURS),
staleTime: 60 * 1000,
retry: false,
})
const models = useMemo(
() => metricsQuery.data?.data.models ?? [],
[metricsQuery.data]
)
const summary = useMemo(() => {
return {
avgLatencyMs: Math.round(
simpleAverage(models, 'avg_latency_ms', (v) => Number.isFinite(v) && v > 0)
),
avgTps: simpleAverage(models, 'avg_tps', (v) => Number.isFinite(v) && v > 0),
successRate: simpleAverage(models, 'success_rate', Number.isFinite),
}
}, [models])
const topModels = useMemo(() => models.slice(0, TOP_MODEL_LIMIT), [models])
const loading = metricsQuery.isLoading
const hasData = models.length > 0
return (
<section className='bg-card h-full overflow-hidden rounded-2xl border shadow-xs'>
<div className='flex items-center gap-2 border-b px-4 py-3 sm:px-5'>
<HeartPulse className='text-muted-foreground/60 size-4 shrink-0' aria-hidden='true' />
<h3 className='text-sm font-semibold'>{t('Performance health')}</h3>
<span className='text-muted-foreground ml-auto text-xs'>
{t('Performance metrics for the last 24 hours')}
</span>
</div>
<div className='grid gap-3 p-4 sm:p-5 lg:grid-cols-[minmax(0,1fr)_14rem]'>
{/* KPI metrics */}
<div className='grid grid-cols-3 gap-2'>
<MetricCell
icon={HeartPulse}
label={t('Success rate')}
value={formatUptimePct(summary.successRate)}
loading={loading}
valueClassName={rateTextClass(summary.successRate)}
/>
<MetricCell
icon={Timer}
label={t('Average latency')}
value={formatLatency(summary.avgLatencyMs)}
loading={loading}
/>
<MetricCell
icon={Gauge}
label={t('Throughput')}
value={formatThroughput(summary.avgTps)}
loading={loading}
/>
</div>
{/* Top models */}
<div className='flex flex-col'>
<span className='text-muted-foreground mb-1 text-[11px] font-medium'>
{t('Top models by traffic')}
</span>
{loading ? (
<div className='space-y-1'>
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className='h-5 w-full rounded' />
))}
</div>
) : !hasData ? (
<span className='text-muted-foreground/60 text-xs'>
{t('No performance data available')}
</span>
) : (
<div className='space-y-0.5'>
{topModels.map((model) => (
<div
key={model.model_name}
className='flex items-center justify-between gap-2 rounded px-1.5 py-1'
>
<span className='min-w-0 flex-1 truncate font-mono text-[11px]'>
{model.model_name}
</span>
<span className='inline-flex shrink-0 items-center gap-1'>
<span
className={cn('size-1.5 rounded-full', rateDotClass(model.success_rate))}
aria-hidden='true'
/>
<span
className={cn(
'font-mono text-[11px] font-semibold tabular-nums',
rateTextClass(model.success_rate)
)}
>
{formatUptimePct(model.success_rate)}
</span>
</span>
</div>
))}
</div>
)}
</div>
</div>
</section>
)
}
function MetricCell(props: {
icon: React.ComponentType<{ className?: string }>
label: string
value: string
loading: boolean
valueClassName?: string
}) {
const Icon = props.icon
return (
<div className='bg-muted/40 rounded-xl px-3 py-2.5'>
<div className='text-muted-foreground flex items-center gap-1.5 text-[11px] font-medium'>
<Icon className='size-3 shrink-0' aria-hidden='true' />
<span className='truncate'>{props.label}</span>
</div>
{props.loading ? (
<Skeleton className='mt-1.5 h-5 w-16' />
) : (
<div
className={cn(
'mt-1.5 font-mono text-sm font-semibold tabular-nums',
props.valueClassName
)}
>
{props.value}
</div>
)}
</div>
)
}
......@@ -19,12 +19,18 @@ For commercial licensing, please contact support@quantumnous.com
import { useMemo } from 'react'
import { useQuery } from '@tanstack/react-query'
import { Link } from '@tanstack/react-router'
import { ArrowRight, CreditCard } from 'lucide-react'
import {
ArrowRight,
Flame,
ShieldCheck,
TrendingDown,
} from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { useAuthStore } from '@/stores/auth-store'
import { getCurrencyLabel, isCurrencyDisplayEnabled } from '@/lib/currency'
import { formatNumber, formatQuota } from '@/lib/format'
import { computeTimeRange } from '@/lib/time'
import { cn } from '@/lib/utils'
import { useStatus } from '@/hooks/use-status'
import { Button } from '@/components/ui/button'
import { StaggerContainer, StaggerItem } from '@/components/page-transition'
......@@ -87,12 +93,62 @@ function buildSummarySparklines(
}
}
function getSummarySparkline(
key: string,
sparklineData: Record<SummarySparklineKey, number[]>
): number[] | undefined {
if (key === 'usage') return sparklineData.usage
if (key === 'requests') return sparklineData.requests
return undefined
}
function getRunwayDays(remainQuota: number, recentUsage: number): number | null {
if (remainQuota <= 0 || recentUsage <= 0) return null
const days = remainQuota / recentUsage
if (!Number.isFinite(days)) return null
return days
}
type HealthLevel = 'healthy' | 'caution' | 'critical'
function getHealthLevel(
remainQuota: number,
recentUsage: number
): HealthLevel {
if (remainQuota <= 0) return 'critical'
const days = getRunwayDays(remainQuota, recentUsage)
if (days !== null && days < 3) return 'caution'
return 'healthy'
}
const HEALTH_CONFIG: Record<
HealthLevel,
{ dotClass: string; labelKey: string }
> = {
healthy: {
dotClass: 'bg-success',
labelKey: 'Healthy',
},
caution: {
dotClass: 'bg-warning',
labelKey: 'Low balance',
},
critical: {
dotClass: 'bg-destructive',
labelKey: 'Balance depleted',
},
}
export function SummaryCards() {
const { t } = useTranslation()
const user = useAuthStore((state) => state.auth.user)
const { status, loading } = useStatus()
const summaryTimeRange = useMemo(() => computeTimeRange(1), [])
const remainQuota = Number(user?.quota ?? 0)
const usedQuota = Number(user?.used_quota ?? 0)
const requestCount = Number(user?.request_count ?? 0)
const usageTrendQuery = useQuery({
queryKey: [
......@@ -112,16 +168,11 @@ export function SummaryCards() {
})
const summaryValues = useMemo(() => {
const remainQuota = Number(user?.quota ?? 0)
const usedQuota = Number(user?.used_quota ?? 0)
const requestCount = Number(user?.request_count ?? 0)
return {
remainDisplay: formatQuota(remainQuota),
usedDisplay: formatQuota(usedQuota),
requestCountDisplay: formatNumber(requestCount),
}
}, [user])
}, [requestCount, usedQuota])
const currencyEnabledFromStore = isCurrencyDisplayEnabled()
const statusCurrencyFlag =
......@@ -138,37 +189,53 @@ export function SummaryCards() {
() =>
buildSummarySparklines(
usageTrendQuery.data?.data ?? [],
Number(user?.quota ?? 0),
remainQuota,
summaryTimeRange.start_timestamp,
summaryTimeRange.end_timestamp
),
[
remainQuota,
summaryTimeRange.end_timestamp,
summaryTimeRange.start_timestamp,
usageTrendQuery.data?.data,
user?.quota,
]
)
const recentUsage = useMemo(
() =>
(usageTrendQuery.data?.data ?? []).reduce(
(total, item) => total + (Number(item.quota) || 0),
0
),
[usageTrendQuery.data?.data]
)
const healthLevel = getHealthLevel(remainQuota, recentUsage)
const healthCfg = HEALTH_CONFIG[healthLevel]
const runwayDays = getRunwayDays(remainQuota, recentUsage)
const todayUsageDisplay = formatQuota(recentUsage)
const items = useSummaryCardsConfig({
...summaryValues,
todayUsageDisplay,
currencyEnabled,
currencyLabel,
}).map((config, index) => {
const tones = ['rose', 'teal', 'gray'] as const
return {
key: config.key,
title: config.title,
value: config.value,
desc: config.description,
icon: config.icon,
tone: tones[index] ?? 'gray',
sparkline:
config.key === 'balance'
? sparklineData.balance
: config.key === 'usage'
? sparklineData.usage
: sparklineData.requests,
config.key === 'todayUsage'
? sparklineData.usage
: getSummarySparkline(config.key, sparklineData),
sparklineVariant: 'line' as const,
}
})
......@@ -189,7 +256,7 @@ export function SummaryCards() {
<StaggerContainer className='grid gap-3 md:grid-cols-3'>
{items.map((it) => (
<StaggerItem
key={it.title}
key={it.key}
className='bg-background/60 rounded-xl border p-3'
>
<StatCard
......@@ -199,6 +266,7 @@ export function SummaryCards() {
icon={it.icon}
tone={it.tone}
sparkline={it.sparkline}
sparklineVariant={it.sparklineVariant}
loading={loading}
/>
</StaggerItem>
......@@ -206,28 +274,78 @@ export function SummaryCards() {
</StaggerContainer>
</div>
<div className='bg-warning/10 flex flex-col justify-between gap-5 border-t p-4 sm:p-5 xl:border-t-0 xl:border-l'>
<div className='flex flex-col gap-2'>
<div className='text-muted-foreground text-sm'>
{t('Credit remaining')}
</div>
<div className='flex items-center gap-2'>
<span className='font-mono text-2xl font-semibold tracking-tight'>
{summaryValues.remainDisplay}
<div className='bg-warning/10 flex flex-col justify-between gap-4 border-t p-4 sm:p-5 xl:border-t-0 xl:border-l'>
<div className='flex flex-col gap-3'>
<div className='flex items-center justify-between'>
<span className='text-muted-foreground text-xs font-medium'>
{t('Credit remaining')}
</span>
<span className='flex items-center gap-1.5'>
<span
className={cn('size-1.5 rounded-full', healthCfg.dotClass)}
aria-hidden='true'
/>
<span className='text-muted-foreground text-[11px] font-medium'>
{t(healthCfg.labelKey)}
</span>
</span>
<CreditCard
className='text-muted-foreground size-4'
aria-hidden='true'
/>
</div>
<p className='text-muted-foreground text-sm leading-relaxed'>
{currencyEnabled
? `${t('Displayed in')} ${currencyLabel}`
: t('Balance is shown in quota units')}
</p>
<div className='font-mono text-2xl font-semibold tracking-tight'>
{formatQuota(remainQuota)}
</div>
<div className='grid grid-cols-2 gap-2'>
<div className='bg-background/60 rounded-lg px-2.5 py-2'>
<div className='text-muted-foreground flex items-center gap-1 text-[11px] leading-none font-medium'>
<Flame className='size-3 shrink-0' aria-hidden='true' />
<span className='truncate'>{t('Last 24h usage')}</span>
</div>
<div className='text-foreground mt-1.5 truncate text-xs font-semibold tabular-nums'>
{formatQuota(recentUsage)}
</div>
</div>
<div className='bg-background/60 rounded-lg px-2.5 py-2'>
<div className='text-muted-foreground flex items-center gap-1 text-[11px] leading-none font-medium'>
{runwayDays !== null && runwayDays < 3 ? (
<TrendingDown
className='size-3 shrink-0'
aria-hidden='true'
/>
) : (
<ShieldCheck
className='size-3 shrink-0'
aria-hidden='true'
/>
)}
<span className='truncate'>{t('Runway')}</span>
</div>
<div
className={cn(
'mt-1.5 truncate text-xs font-semibold tabular-nums',
healthLevel === 'critical' && 'text-destructive',
healthLevel === 'caution' && 'text-warning'
)}
>
{runwayDays !== null
? runwayDays < 1
? t('Less than 1 day left')
: runwayDays > 999
? `999+ ${t('days')}`
: `~${formatNumber(Math.floor(runwayDays))} ${t('days')}`
: remainQuota <= 0
? t('Balance depleted')
: t('No recent usage')}
</div>
</div>
</div>
</div>
<Button className='justify-between' render={<Link to='/wallet' />}>
<span>{t('Recharge')}</span>
<Button
className='justify-between'
render={<Link to='/wallet' />}
>
<span>{t('Wallet')}</span>
<ArrowRight data-icon='inline-end' />
</Button>
</div>
......
......@@ -16,12 +16,25 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import type { ReactNode } from 'react'
import { useId, type ReactNode } from 'react'
import { type LucideIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Skeleton } from '@/components/ui/skeleton'
type StatCardTone = 'rose' | 'teal' | 'gray'
type StatCardSparklineVariant = 'bars' | 'line'
type StatCardDetailTone =
| 'default'
| 'muted'
| 'success'
| 'warning'
| 'destructive'
export interface StatCardDetail {
label: string
value: string
tone?: StatCardDetailTone
}
interface StatCardProps {
title: string
......@@ -29,6 +42,8 @@ interface StatCardProps {
description: string
icon: LucideIcon
sparkline?: number[]
sparklineVariant?: StatCardSparklineVariant
details?: StatCardDetail[]
tone?: StatCardTone
loading?: boolean
error?: boolean
......@@ -41,6 +56,20 @@ const TONE_CLASSES: Record<StatCardTone, string> = {
gray: 'from-muted-foreground/50 via-muted-foreground/20 to-transparent dark:from-muted-foreground/40 dark:via-muted-foreground/20',
}
const LINE_TONE_CLASSES: Record<StatCardTone, string> = {
rose: 'text-warning',
teal: 'text-primary',
gray: 'text-muted-foreground',
}
const DETAIL_TONE_CLASSES: Record<StatCardDetailTone, string> = {
default: 'text-foreground',
muted: 'text-muted-foreground',
success: 'text-success',
warning: 'text-warning',
destructive: 'text-destructive',
}
function normalizeSparkline(values?: number[]): number[] {
if (!values?.length) return []
......@@ -51,10 +80,133 @@ function normalizeSparkline(values?: number[]): number[] {
return sanitized.map((value) => Math.max(8, (value / max) * 100))
}
function buildLineSparkline(values?: number[]) {
if (!values?.length) return null
const sanitized = values.map((value) => Math.max(0, Number(value) || 0))
const width = 160
const height = 36
const padding = 3
const max = Math.max(...sanitized)
const min = Math.min(...sanitized)
const range = max - min
const points = sanitized.map((value, index) => {
const x =
sanitized.length === 1
? width / 2
: (index / (sanitized.length - 1)) * width
const normalized =
range > 0 ? (value - min) / range : max > 0 ? 0.5 : 0
const y = height - padding - normalized * (height - padding * 2)
return { x, y }
})
const linePath = points
.map((point, index) => `${index === 0 ? 'M' : 'L'} ${point.x} ${point.y}`)
.join(' ')
const firstPoint = points[0]
const lastPoint = points[points.length - 1]
const areaPath = `${linePath} L ${lastPoint.x} ${height} L ${firstPoint.x} ${height} Z`
return {
areaPath,
linePath,
}
}
function LineSparkline(props: { values?: number[]; tone: StatCardTone }) {
const rawGradientId = useId()
const gradientId = `stat-card-line-${rawGradientId.replace(/:/g, '')}`
const paths = buildLineSparkline(props.values)
if (!paths) return <div className='h-8' aria-hidden='true' />
return (
<div
className={cn(
'relative h-8 overflow-hidden rounded-lg',
LINE_TONE_CLASSES[props.tone]
)}
aria-hidden='true'
>
<svg
viewBox='0 0 160 36'
preserveAspectRatio='none'
className='size-full'
>
<defs>
<linearGradient id={gradientId} x1='0' x2='0' y1='0' y2='1'>
<stop offset='0%' stopColor='currentColor' stopOpacity='0.24' />
<stop offset='100%' stopColor='currentColor' stopOpacity='0' />
</linearGradient>
</defs>
<path d={paths.areaPath} fill={`url(#${gradientId})`} />
<path
d={paths.linePath}
fill='none'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='2.25'
vectorEffect='non-scaling-stroke'
/>
</svg>
</div>
)
}
function BarSparkline(props: { values?: number[]; tone: StatCardTone }) {
const sparkline = normalizeSparkline(props.values)
return (
<div className='flex h-8 items-end gap-1' aria-hidden='true'>
{sparkline.map((height, index) => (
<span
key={`spark-${index}`}
className={cn(
'flex-1 rounded-t-sm bg-linear-to-t',
height <= 0 && 'opacity-20',
TONE_CLASSES[props.tone]
)}
style={{ height: `${height}%` }}
/>
))}
</div>
)
}
function StatCardDetails(props: { details: StatCardDetail[] }) {
return (
<div className='grid grid-cols-2 gap-2'>
{props.details.map((detail) => (
<div
key={detail.label}
className='bg-muted/40 rounded-lg border border-transparent px-2.5 py-2'
>
<div className='text-muted-foreground truncate text-[11px] leading-none font-medium'>
{detail.label}
</div>
<div
className={cn(
'mt-1.5 truncate text-xs font-semibold tabular-nums',
DETAIL_TONE_CLASSES[detail.tone ?? 'default']
)}
title={detail.value}
>
{detail.value}
</div>
</div>
))}
</div>
)
}
export function StatCard(props: StatCardProps) {
const Icon = props.icon
const tone = props.tone ?? 'gray'
const sparkline = normalizeSparkline(props.sparkline)
const sparklineVariant = props.sparklineVariant ?? 'bars'
return (
<div className='group flex min-h-32 flex-col justify-between gap-3'>
......@@ -94,19 +246,13 @@ export function StatCard(props: StatCardProps) {
</div>
)}
<div className='flex h-8 items-end gap-1' aria-hidden='true'>
{sparkline.map((height, index) => (
<span
key={`${props.title}-spark-${index}`}
className={cn(
'flex-1 rounded-t-sm bg-linear-to-t',
height <= 0 && 'opacity-20',
TONE_CLASSES[tone]
)}
style={{ height: `${height}%` }}
/>
))}
</div>
{props.details?.length ? (
<StatCardDetails details={props.details} />
) : sparklineVariant === 'line' ? (
<LineSparkline values={props.sparkline} tone={tone} />
) : (
<BarSparkline values={props.sparkline} tone={tone} />
)}
</div>
)
}
......@@ -22,7 +22,7 @@ import {
Layers,
Gauge,
Zap,
Wallet,
Flame,
TrendingUp,
Activity,
type LucideIcon,
......@@ -83,7 +83,7 @@ export function useModelStatCardsConfig(): StatCardConfig[] {
}
export function useSummaryCardsConfig(totals: {
remainDisplay: string
todayUsageDisplay: string
usedDisplay: string
requestCountDisplay: string
currencyLabel: string
......@@ -93,13 +93,13 @@ export function useSummaryCardsConfig(totals: {
return [
{
key: 'balance',
title: t('Current Balance'),
value: totals.remainDisplay,
key: 'todayUsage',
title: t('Last 24h usage'),
value: totals.todayUsageDisplay,
description: totals.currencyEnabled
? `${t('Remaining quota')} (${totals.currencyLabel})`
: t('Remaining quota units'),
icon: Wallet,
? `${t('Consumed in the last 24 hours')} (${totals.currencyLabel})`
: t('Consumed in the last 24 hours'),
icon: Flame,
},
{
key: 'usage',
......
......@@ -109,24 +109,22 @@ function ModelChartsFallback() {
function PerformanceOverviewFallback() {
return (
<div className='space-y-3 sm:space-y-4'>
<div className='overflow-hidden rounded-lg border'>
<div className='divide-border/60 grid grid-cols-2 divide-x sm:grid-cols-4'>
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className='px-3 py-2.5 sm:px-5 sm:py-4'>
<Skeleton className='h-4 w-24' />
<Skeleton className='mt-2 h-7 w-20' />
<Skeleton className='mt-1.5 h-3.5 w-28' />
</div>
))}
<div className='overflow-hidden rounded-lg border'>
<div className='flex flex-wrap items-center gap-x-6 gap-y-2 px-4 py-3 sm:px-5'>
<div className='flex items-center gap-2'>
<Skeleton className='h-4 w-24' />
</div>
</div>
<div className='overflow-hidden rounded-lg border'>
<div className='flex items-center justify-between border-b px-4 py-3 sm:px-5'>
<Skeleton className='h-5 w-40' />
<Skeleton className='h-4 w-48' />
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className='flex items-center gap-1.5'>
<Skeleton className='h-3 w-14' />
<Skeleton className='h-4 w-16' />
</div>
))}
<div className='ml-auto flex items-center gap-2'>
{Array.from({ length: 2 }).map((_, i) => (
<Skeleton key={i} className='h-5 w-28 rounded-full' />
))}
</div>
<Skeleton className='h-44 w-full' />
</div>
</div>
)
......@@ -267,12 +265,14 @@ export function Dashboard() {
/>
</Suspense>
</FadeIn>
{isAdmin && (
<FadeIn delay={0.05}>
<Suspense fallback={<PerformanceOverviewFallback />}>
<LazyPerformanceOverview />
</Suspense>
</FadeIn>
)}
<FadeIn delay={0.1}>
<Suspense fallback={<PerformanceOverviewFallback />}>
<LazyPerformanceOverview />
</Suspense>
</FadeIn>
<FadeIn delay={0.15}>
<Suspense fallback={<ModelChartsFallback />}>
<LazyConsumptionDistributionChart
data={modelData}
......@@ -286,7 +286,7 @@ export function Dashboard() {
/>
</Suspense>
</FadeIn>
<FadeIn delay={0.2}>
<FadeIn delay={0.15}>
<Suspense fallback={<ModelChartsFallback />}>
<LazyModelCharts
data={modelData}
......
......@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
export function formatThroughput(tps: number): string {
if (tps <= 0) return '—'
if (!Number.isFinite(tps) || tps <= 0) return '—'
if (tps >= 1_000) return `${(tps / 1_000).toFixed(1)}K t/s`
return `${tps.toFixed(tps < 10 ? 2 : 1)} t/s`
}
......
......@@ -48,7 +48,7 @@ export type PerfModelSummary = {
avg_latency_ms: number
success_rate: number
avg_tps: number
request_count: number
request_count?: number
}
export type PerfSummaryAllData = {
......
......@@ -59,9 +59,7 @@ export function ModelCardGrid(props: ModelCardGridProps) {
const perfMap = useMemo(() => {
const map = new Map<string, ModelPerfBadgeData>()
for (const model of perfQuery.data?.data?.models ?? []) {
if (model.request_count > 0) {
map.set(model.model_name, model)
}
map.set(model.model_name, model)
}
return map
}, [perfQuery.data])
......
......@@ -475,14 +475,14 @@ export function RechargeFormCard({
</div>
{topupLink && (
<p className='text-muted-foreground text-xs'>
{t('Need a code?')}{' '}
{t('Need a redemption code?')}{' '}
<a
href={topupLink}
target='_blank'
rel='noopener noreferrer'
className='inline-flex items-center gap-1 underline-offset-4 hover:underline'
>
{t('Purchase here')}
{t('Get one here')}
<ExternalLink className='h-3 w-3' />
</a>
</p>
......
......@@ -254,7 +254,7 @@ export function SubscriptionPlansCard({
<>
<TitledCard
title={t('Subscription Plans')}
description={t('Purchase a plan to enjoy model benefits')}
description={t('Subscribe to a plan for model access')}
icon={<Crown className='h-4 w-4' />}
contentClassName='space-y-4 sm:space-y-5'
>
......@@ -499,7 +499,7 @@ export function SubscriptionPlansCard({
{!hasAny && (
<p className='text-muted-foreground mt-2 text-xs'>
{t('Purchase a plan to enjoy model benefits')}
{t('Subscribe to a plan for model access')}
</p>
)}
</div>
......
......@@ -4,6 +4,7 @@
"/status/": "/status/",
"/your/endpoint": "/your/endpoint",
"AIGC2D": "AIGC2D",
"Alipay": "Alipay",
"Anthropic": "Anthropic",
"API URL": "API URL",
"API2GPT": "API2GPT",
......@@ -86,9 +87,8 @@
"VolcEngine": "VolcEngine",
"Webhook URL": "Webhook URL",
"Webhook URL:": "Webhook URL:",
"WeChat Pay": "WeChat Pay",
"whsec_xxx": "whsec_xxx",
"Xinference": "Xinference",
"Xunfei": "Xunfei",
"Alipay": "Alipay",
"WeChat Pay": "WeChat Pay"
"Xunfei": "Xunfei"
}
......@@ -8,6 +8,7 @@
"AccessKey / SecretAccessKey": "AccessKey / SecretAccessKey",
"AI Proxy": "AI Proxy",
"AIGC2D": "AIGC2D",
"Alipay": "Alipay",
"All conditions must match before this tier is used.": "All conditions must match before this tier is used.",
"Anthropic": "Anthropic",
"API2GPT": "API2GPT",
......@@ -100,10 +101,9 @@
"Vertex AI": "Vertex AI",
"VolcEngine": "VolcEngine",
"WeChat": "WeChat",
"WeChat Pay": "WeChat Pay",
"whsec_xxx": "whsec_xxx",
"Xinference": "Xinference",
"Xunfei": "Xunfei",
"Alipay": "Alipay",
"WeChat Pay": "WeChat Pay",
"Zhipu V4": "Zhipu V4"
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment