Compare commits
70 Commits
v0.8.0-alpha.1
...
v0.8.0
| Author | SHA1 | Date | |
|---|---|---|---|
| e2df80118f | |||
| 91fd8b3762 | |||
| e9416e620c | |||
| 51ea2bac0d | |||
| 46e6d593a6 | |||
| 0e9a193ed8 | |||
| 6b965995ca | |||
| f4b6ec4f50 | |||
| da535b9587 | |||
| 85ade00e95 | |||
| ed2677c7f5 | |||
| 4c92025d55 | |||
| 5a2272553c | |||
| 6439ff4b95 | |||
| 0546725ac3 | |||
| 6731a2c202 | |||
| 1ed8a1e0bd | |||
| 6282eccf16 | |||
| f31144545e | |||
| d8d0a6fb64 | |||
| eb23dffa04 | |||
| 84a9a54af6 | |||
| 8add0b1592 | |||
| 7601f7f969 | |||
| d84da74a0a | |||
| 2c3464c2c6 | |||
| f24e2b0d40 | |||
| 508188caa6 | |||
| 24abf51b8d | |||
| b72e641d22 | |||
| a813b24184 | |||
| 74c00e2296 | |||
| a1c1a336ac | |||
| 8212d391d3 | |||
| 465cd77b4c | |||
| 28e40c90eb | |||
| 5bb8f2dfed | |||
| 9b3c48c7ce | |||
| 085bb658e9 | |||
| 7111ec2e10 | |||
| 5a7314eea0 | |||
| 0827ebd22e | |||
| 5e88e76001 | |||
| 7c0c6b848c | |||
| 04007b5955 | |||
| f2bca12d9a | |||
| 56491f4df6 | |||
| 3feed08651 | |||
| d5a3b05ba6 | |||
| 80d43b0082 | |||
| 618ed976f1 | |||
| 3b1d1de912 | |||
| f44e7258b9 | |||
| 0ea194e7d7 | |||
| 21613f8f73 | |||
| eaaee594ef | |||
| 6a8971dd98 | |||
| 912c11ab2c | |||
| 60e6fc67aa | |||
| 0cd5e5cf48 | |||
| 1c0c24e0ac | |||
| 3daa5f70bd | |||
| d98fb70328 | |||
| 4794a7cadc | |||
| 61d1add156 | |||
| 1a8888211f | |||
| b90aa227ef | |||
| 77d6299557 | |||
| 253f487d80 | |||
| 75d859dce2 |
@@ -38,6 +38,9 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
|
||||
@@ -44,6 +44,9 @@
|
||||
|
||||
For detailed documentation, please visit our official Wiki: [https://docs.newapi.pro/](https://docs.newapi.pro/)
|
||||
|
||||
You can also access the AI-generated DeepWiki:
|
||||
[](https://deepwiki.com/QuantumNous/new-api)
|
||||
|
||||
## ✨ Key Features
|
||||
|
||||
New API offers a wide range of features, please refer to [Features Introduction](https://docs.newapi.pro/wiki/features-introduction) for details:
|
||||
|
||||
@@ -44,6 +44,9 @@
|
||||
|
||||
详细文档请访问我们的官方Wiki:[https://docs.newapi.pro/](https://docs.newapi.pro/)
|
||||
|
||||
也可访问AI生成的DeepWiki:
|
||||
[](https://deepwiki.com/QuantumNous/new-api)
|
||||
|
||||
## ✨ 主要特性
|
||||
|
||||
New API提供了丰富的功能,详细特性请参考[特性说明](https://docs.newapi.pro/wiki/features-introduction):
|
||||
|
||||
@@ -40,6 +40,8 @@ func relayHandler(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusCode
|
||||
err = relay.EmbeddingHelper(c)
|
||||
case relayconstant.RelayModeResponses:
|
||||
err = relay.ResponsesHelper(c)
|
||||
case relayconstant.RelayModeGemini:
|
||||
err = relay.GeminiHelper(c)
|
||||
default:
|
||||
err = relay.TextHelper(c)
|
||||
}
|
||||
|
||||
+12
-11
@@ -7,17 +7,18 @@ type ClaudeMetadata struct {
|
||||
}
|
||||
|
||||
type ClaudeMediaMessage struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Text *string `json:"text,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Source *ClaudeMessageSource `json:"source,omitempty"`
|
||||
Usage *ClaudeUsage `json:"usage,omitempty"`
|
||||
StopReason *string `json:"stop_reason,omitempty"`
|
||||
PartialJson *string `json:"partial_json,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
Thinking string `json:"thinking,omitempty"`
|
||||
Signature string `json:"signature,omitempty"`
|
||||
Delta string `json:"delta,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Text *string `json:"text,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Source *ClaudeMessageSource `json:"source,omitempty"`
|
||||
Usage *ClaudeUsage `json:"usage,omitempty"`
|
||||
StopReason *string `json:"stop_reason,omitempty"`
|
||||
PartialJson *string `json:"partial_json,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
Thinking string `json:"thinking,omitempty"`
|
||||
Signature string `json:"signature,omitempty"`
|
||||
Delta string `json:"delta,omitempty"`
|
||||
CacheControl json.RawMessage `json:"cache_control,omitempty"`
|
||||
// tool_calls
|
||||
Id string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
|
||||
@@ -29,7 +29,6 @@ type GeneralOpenAIRequest struct {
|
||||
MaxTokens uint `json:"max_tokens,omitempty"`
|
||||
MaxCompletionTokens uint `json:"max_completion_tokens,omitempty"`
|
||||
ReasoningEffort string `json:"reasoning_effort,omitempty"`
|
||||
//Reasoning json.RawMessage `json:"reasoning,omitempty"`
|
||||
Temperature *float64 `json:"temperature,omitempty"`
|
||||
TopP float64 `json:"top_p,omitempty"`
|
||||
TopK int `json:"top_k,omitempty"`
|
||||
@@ -56,6 +55,8 @@ type GeneralOpenAIRequest struct {
|
||||
EnableThinking any `json:"enable_thinking,omitempty"` // ali
|
||||
ExtraBody any `json:"extra_body,omitempty"`
|
||||
WebSearchOptions *WebSearchOptions `json:"web_search_options,omitempty"`
|
||||
// OpenRouter Params
|
||||
Reasoning json.RawMessage `json:"reasoning,omitempty"`
|
||||
}
|
||||
|
||||
func (r *GeneralOpenAIRequest) ToMap() map[string]any {
|
||||
@@ -125,6 +126,8 @@ type MediaContent struct {
|
||||
InputAudio any `json:"input_audio,omitempty"`
|
||||
File any `json:"file,omitempty"`
|
||||
VideoUrl any `json:"video_url,omitempty"`
|
||||
// OpenRouter Params
|
||||
CacheControl json.RawMessage `json:"cache_control,omitempty"`
|
||||
}
|
||||
|
||||
func (m *MediaContent) GetImageMedia() *MessageImageUrl {
|
||||
|
||||
+15
-2
@@ -1,13 +1,14 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func validUserInfo(username string, role int) bool {
|
||||
@@ -182,6 +183,18 @@ func TokenAuth() func(c *gin.Context) {
|
||||
c.Request.Header.Set("Authorization", "Bearer "+key)
|
||||
}
|
||||
}
|
||||
// gemini api 从query中获取key
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/v1beta/models/") {
|
||||
skKey := c.Query("key")
|
||||
if skKey != "" {
|
||||
c.Request.Header.Set("Authorization", "Bearer "+skKey)
|
||||
}
|
||||
// 从x-goog-api-key header中获取key
|
||||
xGoogKey := c.Request.Header.Get("x-goog-api-key")
|
||||
if xGoogKey != "" {
|
||||
c.Request.Header.Set("Authorization", "Bearer "+xGoogKey)
|
||||
}
|
||||
}
|
||||
key := c.Request.Header.Get("Authorization")
|
||||
parts := make([]string, 0)
|
||||
key = strings.TrimPrefix(key, "Bearer ")
|
||||
|
||||
@@ -162,6 +162,14 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
|
||||
}
|
||||
c.Set("platform", string(constant.TaskPlatformSuno))
|
||||
c.Set("relay_mode", relayMode)
|
||||
} else if strings.HasPrefix(c.Request.URL.Path, "/v1beta/models/") {
|
||||
// Gemini API 路径处理: /v1beta/models/gemini-2.0-flash:generateContent
|
||||
relayMode := relayconstant.RelayModeGemini
|
||||
modelName := extractModelNameFromGeminiPath(c.Request.URL.Path)
|
||||
if modelName != "" {
|
||||
modelRequest.Model = modelName
|
||||
}
|
||||
c.Set("relay_mode", relayMode)
|
||||
} else if !strings.HasPrefix(c.Request.URL.Path, "/v1/audio/transcriptions") && !strings.HasPrefix(c.Request.URL.Path, "/v1/images/edits") {
|
||||
err = common.UnmarshalBodyReusable(c, &modelRequest)
|
||||
}
|
||||
@@ -244,3 +252,31 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode
|
||||
c.Set("bot_id", channel.Other)
|
||||
}
|
||||
}
|
||||
|
||||
// extractModelNameFromGeminiPath 从 Gemini API URL 路径中提取模型名
|
||||
// 输入格式: /v1beta/models/gemini-2.0-flash:generateContent
|
||||
// 输出: gemini-2.0-flash
|
||||
func extractModelNameFromGeminiPath(path string) string {
|
||||
// 查找 "/models/" 的位置
|
||||
modelsPrefix := "/models/"
|
||||
modelsIndex := strings.Index(path, modelsPrefix)
|
||||
if modelsIndex == -1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 从 "/models/" 之后开始提取
|
||||
startIndex := modelsIndex + len(modelsPrefix)
|
||||
if startIndex >= len(path) {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 查找 ":" 的位置,模型名在 ":" 之前
|
||||
colonIndex := strings.Index(path[startIndex:], ":")
|
||||
if colonIndex == -1 {
|
||||
// 如果没有找到 ":",返回从 "/models/" 到路径结尾的部分
|
||||
return path[startIndex:]
|
||||
}
|
||||
|
||||
// 返回模型名部分
|
||||
return path[startIndex : startIndex+colonIndex]
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"one-api/dto"
|
||||
"one-api/relay/channel"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/relay/constant"
|
||||
"one-api/service"
|
||||
"one-api/setting/model_setting"
|
||||
"strings"
|
||||
@@ -165,6 +166,14 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
|
||||
}
|
||||
|
||||
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
|
||||
if info.RelayMode == constant.RelayModeGemini {
|
||||
if info.IsStream {
|
||||
return GeminiTextGenerationStreamHandler(c, resp, info)
|
||||
} else {
|
||||
return GeminiTextGenerationHandler(c, resp, info)
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(info.UpstreamModelName, "imagen") {
|
||||
return GeminiImageHandler(c, resp, info)
|
||||
}
|
||||
|
||||
+12
-11
@@ -27,14 +27,9 @@ type FunctionCall struct {
|
||||
Arguments any `json:"args"`
|
||||
}
|
||||
|
||||
type GeminiFunctionResponseContent struct {
|
||||
Name string `json:"name"`
|
||||
Content any `json:"content"`
|
||||
}
|
||||
|
||||
type FunctionResponse struct {
|
||||
Name string `json:"name"`
|
||||
Response GeminiFunctionResponseContent `json:"response"`
|
||||
Name string `json:"name"`
|
||||
Response map[string]interface{} `json:"response"`
|
||||
}
|
||||
|
||||
type GeminiPartExecutableCode struct {
|
||||
@@ -117,10 +112,16 @@ type GeminiChatResponse struct {
|
||||
}
|
||||
|
||||
type GeminiUsageMetadata struct {
|
||||
PromptTokenCount int `json:"promptTokenCount"`
|
||||
CandidatesTokenCount int `json:"candidatesTokenCount"`
|
||||
TotalTokenCount int `json:"totalTokenCount"`
|
||||
ThoughtsTokenCount int `json:"thoughtsTokenCount"`
|
||||
PromptTokenCount int `json:"promptTokenCount"`
|
||||
CandidatesTokenCount int `json:"candidatesTokenCount"`
|
||||
TotalTokenCount int `json:"totalTokenCount"`
|
||||
ThoughtsTokenCount int `json:"thoughtsTokenCount"`
|
||||
PromptTokensDetails []GeminiPromptTokensDetails `json:"promptTokensDetails"`
|
||||
}
|
||||
|
||||
type GeminiPromptTokensDetails struct {
|
||||
Modality string `json:"modality"`
|
||||
TokenCount int `json:"tokenCount"`
|
||||
}
|
||||
|
||||
// Imagen related structs
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package gemini
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/dto"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/relay/helper"
|
||||
"one-api/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func GeminiTextGenerationHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.Usage, *dto.OpenAIErrorWithStatusCode) {
|
||||
// 读取响应体
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError)
|
||||
}
|
||||
err = resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if common.DebugEnabled {
|
||||
println(string(responseBody))
|
||||
}
|
||||
|
||||
// 解析为 Gemini 原生响应格式
|
||||
var geminiResponse GeminiChatResponse
|
||||
err = common.DecodeJson(responseBody, &geminiResponse)
|
||||
if err != nil {
|
||||
return nil, service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// 检查是否有候选响应
|
||||
if len(geminiResponse.Candidates) == 0 {
|
||||
return nil, &dto.OpenAIErrorWithStatusCode{
|
||||
Error: dto.OpenAIError{
|
||||
Message: "No candidates returned",
|
||||
Type: "server_error",
|
||||
Param: "",
|
||||
Code: 500,
|
||||
},
|
||||
StatusCode: resp.StatusCode,
|
||||
}
|
||||
}
|
||||
|
||||
// 计算使用量(基于 UsageMetadata)
|
||||
usage := dto.Usage{
|
||||
PromptTokens: geminiResponse.UsageMetadata.PromptTokenCount,
|
||||
CompletionTokens: geminiResponse.UsageMetadata.CandidatesTokenCount,
|
||||
TotalTokens: geminiResponse.UsageMetadata.TotalTokenCount,
|
||||
}
|
||||
|
||||
usage.CompletionTokenDetails.ReasoningTokens = geminiResponse.UsageMetadata.ThoughtsTokenCount
|
||||
|
||||
for _, detail := range geminiResponse.UsageMetadata.PromptTokensDetails {
|
||||
if detail.Modality == "AUDIO" {
|
||||
usage.PromptTokensDetails.AudioTokens = detail.TokenCount
|
||||
} else if detail.Modality == "TEXT" {
|
||||
usage.PromptTokensDetails.TextTokens = detail.TokenCount
|
||||
}
|
||||
}
|
||||
|
||||
// 直接返回 Gemini 原生格式的 JSON 响应
|
||||
jsonResponse, err := json.Marshal(geminiResponse)
|
||||
if err != nil {
|
||||
return nil, service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// 设置响应头并写入响应
|
||||
c.Writer.Header().Set("Content-Type", "application/json")
|
||||
c.Writer.WriteHeader(resp.StatusCode)
|
||||
_, err = c.Writer.Write(jsonResponse)
|
||||
if err != nil {
|
||||
return nil, service.OpenAIErrorWrapper(err, "write_response_failed", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return &usage, nil
|
||||
}
|
||||
|
||||
func GeminiTextGenerationStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.Usage, *dto.OpenAIErrorWithStatusCode) {
|
||||
var usage = &dto.Usage{}
|
||||
var imageCount int
|
||||
|
||||
helper.SetEventStreamHeaders(c)
|
||||
|
||||
helper.StreamScannerHandler(c, resp, info, func(data string) bool {
|
||||
var geminiResponse GeminiChatResponse
|
||||
err := common.DecodeJsonStr(data, &geminiResponse)
|
||||
if err != nil {
|
||||
common.LogError(c, "error unmarshalling stream response: "+err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
// 统计图片数量
|
||||
for _, candidate := range geminiResponse.Candidates {
|
||||
for _, part := range candidate.Content.Parts {
|
||||
if part.InlineData != nil && part.InlineData.MimeType != "" {
|
||||
imageCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新使用量统计
|
||||
if geminiResponse.UsageMetadata.TotalTokenCount != 0 {
|
||||
usage.PromptTokens = geminiResponse.UsageMetadata.PromptTokenCount
|
||||
usage.CompletionTokens = geminiResponse.UsageMetadata.CandidatesTokenCount
|
||||
usage.TotalTokens = geminiResponse.UsageMetadata.TotalTokenCount
|
||||
usage.CompletionTokenDetails.ReasoningTokens = geminiResponse.UsageMetadata.ThoughtsTokenCount
|
||||
for _, detail := range geminiResponse.UsageMetadata.PromptTokensDetails {
|
||||
if detail.Modality == "AUDIO" {
|
||||
usage.PromptTokensDetails.AudioTokens = detail.TokenCount
|
||||
} else if detail.Modality == "TEXT" {
|
||||
usage.PromptTokensDetails.TextTokens = detail.TokenCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 直接发送 GeminiChatResponse 响应
|
||||
err = helper.ObjectData(c, geminiResponse)
|
||||
if err != nil {
|
||||
common.LogError(c, err.Error())
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
if imageCount != 0 {
|
||||
if usage.CompletionTokens == 0 {
|
||||
usage.CompletionTokens = imageCount * 258
|
||||
}
|
||||
}
|
||||
|
||||
// 计算最终使用量
|
||||
usage.CompletionTokens = usage.TotalTokens - usage.PromptTokens
|
||||
|
||||
// 移除流式响应结尾的[Done],因为Gemini API没有发送Done的行为
|
||||
//helper.Done(c)
|
||||
|
||||
return usage, nil
|
||||
}
|
||||
@@ -57,25 +57,63 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
|
||||
}
|
||||
|
||||
if model_setting.GetGeminiSettings().ThinkingAdapterEnabled {
|
||||
if strings.HasSuffix(info.OriginModelName, "-thinking") {
|
||||
// 如果模型名以 gemini-2.5-pro 开头,不设置 ThinkingBudget
|
||||
if strings.HasPrefix(info.OriginModelName, "gemini-2.5-pro") {
|
||||
geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
|
||||
IncludeThoughts: true,
|
||||
}
|
||||
} else {
|
||||
budgetTokens := model_setting.GetGeminiSettings().ThinkingAdapterBudgetTokensPercentage * float64(geminiRequest.GenerationConfig.MaxOutputTokens)
|
||||
if budgetTokens == 0 || budgetTokens > 24576 {
|
||||
budgetTokens = 24576
|
||||
}
|
||||
geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
|
||||
ThinkingBudget: common.GetPointer(int(budgetTokens)),
|
||||
IncludeThoughts: true,
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(info.OriginModelName, "-thinking") {
|
||||
// 硬编码不支持 ThinkingBudget 的旧模型
|
||||
unsupportedModels := []string{
|
||||
"gemini-2.5-pro-preview-05-06",
|
||||
"gemini-2.5-pro-preview-03-25",
|
||||
}
|
||||
|
||||
isUnsupported := false
|
||||
for _, unsupportedModel := range unsupportedModels {
|
||||
if strings.HasPrefix(info.OriginModelName, unsupportedModel) {
|
||||
isUnsupported = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if isUnsupported {
|
||||
geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
|
||||
IncludeThoughts: true,
|
||||
}
|
||||
} else {
|
||||
budgetTokens := model_setting.GetGeminiSettings().ThinkingAdapterBudgetTokensPercentage * float64(geminiRequest.GenerationConfig.MaxOutputTokens)
|
||||
|
||||
// 检查是否为新的2.5pro模型(支持ThinkingBudget但有特殊范围)
|
||||
isNew25Pro := strings.HasPrefix(info.OriginModelName, "gemini-2.5-pro") &&
|
||||
!strings.HasPrefix(info.OriginModelName, "gemini-2.5-pro-preview-05-06") &&
|
||||
!strings.HasPrefix(info.OriginModelName, "gemini-2.5-pro-preview-03-25")
|
||||
|
||||
if isNew25Pro {
|
||||
// 新的2.5pro模型:ThinkingBudget范围为128-32768
|
||||
if budgetTokens == 0 || budgetTokens < 128 {
|
||||
budgetTokens = 128
|
||||
} else if budgetTokens > 32768 {
|
||||
budgetTokens = 32768
|
||||
}
|
||||
} else {
|
||||
// 其他模型:ThinkingBudget范围为0-24576
|
||||
if budgetTokens == 0 || budgetTokens > 24576 {
|
||||
budgetTokens = 24576
|
||||
}
|
||||
}
|
||||
|
||||
geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
|
||||
ThinkingBudget: common.GetPointer(int(budgetTokens)),
|
||||
IncludeThoughts: true,
|
||||
}
|
||||
}
|
||||
} else if strings.HasSuffix(info.OriginModelName, "-nothinking") {
|
||||
geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
|
||||
ThinkingBudget: common.GetPointer(0),
|
||||
// 检查是否为新的2.5pro模型(不支持-nothinking,因为最低值只能为128)
|
||||
isNew25Pro := strings.HasPrefix(info.OriginModelName, "gemini-2.5-pro") &&
|
||||
!strings.HasPrefix(info.OriginModelName, "gemini-2.5-pro-preview-05-06") &&
|
||||
!strings.HasPrefix(info.OriginModelName, "gemini-2.5-pro-preview-03-25")
|
||||
|
||||
if !isNew25Pro {
|
||||
// 只有非新2.5pro模型才支持-nothinking
|
||||
geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{
|
||||
ThinkingBudget: common.GetPointer(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,17 +211,12 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
|
||||
} else if val, exists := tool_call_ids[message.ToolCallId]; exists {
|
||||
name = val
|
||||
}
|
||||
content := common.StrToMap(message.StringContent())
|
||||
contentMap := common.StrToMap(message.StringContent())
|
||||
functionResp := &FunctionResponse{
|
||||
Name: name,
|
||||
Response: GeminiFunctionResponseContent{
|
||||
Name: name,
|
||||
Content: content,
|
||||
},
|
||||
}
|
||||
if content == nil {
|
||||
functionResp.Response.Content = message.StringContent()
|
||||
Name: name,
|
||||
Response: contentMap,
|
||||
}
|
||||
|
||||
*parts = append(*parts, GeminiPart{
|
||||
FunctionResponse: functionResp,
|
||||
})
|
||||
@@ -280,13 +313,13 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
|
||||
if part.GetInputAudio().Data == "" {
|
||||
return nil, fmt.Errorf("only base64 audio is supported in gemini")
|
||||
}
|
||||
format, base64String, err := service.DecodeBase64FileData(part.GetInputAudio().Data)
|
||||
base64String, err := service.DecodeBase64AudioData(part.GetInputAudio().Data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode base64 audio data failed: %s", err.Error())
|
||||
}
|
||||
parts = append(parts, GeminiPart{
|
||||
InlineData: &GeminiInlineData{
|
||||
MimeType: format,
|
||||
MimeType: "audio/" + part.GetInputAudio().Format,
|
||||
Data: base64String,
|
||||
},
|
||||
})
|
||||
@@ -738,6 +771,13 @@ func GeminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycom
|
||||
usage.CompletionTokens = geminiResponse.UsageMetadata.CandidatesTokenCount
|
||||
usage.CompletionTokenDetails.ReasoningTokens = geminiResponse.UsageMetadata.ThoughtsTokenCount
|
||||
usage.TotalTokens = geminiResponse.UsageMetadata.TotalTokenCount
|
||||
for _, detail := range geminiResponse.UsageMetadata.PromptTokensDetails {
|
||||
if detail.Modality == "AUDIO" {
|
||||
usage.PromptTokensDetails.AudioTokens = detail.TokenCount
|
||||
} else if detail.Modality == "TEXT" {
|
||||
usage.PromptTokensDetails.TextTokens = detail.TokenCount
|
||||
}
|
||||
}
|
||||
}
|
||||
err = helper.ObjectData(c, response)
|
||||
if err != nil {
|
||||
@@ -812,6 +852,14 @@ func GeminiChatHandler(c *gin.Context, resp *http.Response, info *relaycommon.Re
|
||||
usage.CompletionTokenDetails.ReasoningTokens = geminiResponse.UsageMetadata.ThoughtsTokenCount
|
||||
usage.CompletionTokens = usage.TotalTokens - usage.PromptTokens
|
||||
|
||||
for _, detail := range geminiResponse.UsageMetadata.PromptTokensDetails {
|
||||
if detail.Modality == "AUDIO" {
|
||||
usage.PromptTokensDetails.AudioTokens = detail.TokenCount
|
||||
} else if detail.Modality == "TEXT" {
|
||||
usage.PromptTokensDetails.TextTokens = detail.TokenCount
|
||||
}
|
||||
}
|
||||
|
||||
fullTextResponse.Usage = usage
|
||||
jsonResponse, err := json.Marshal(fullTextResponse)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package openrouter
|
||||
|
||||
type RequestReasoning struct {
|
||||
// One of the following (not both):
|
||||
Effort string `json:"effort,omitempty"` // Can be "high", "medium", or "low" (OpenAI-style)
|
||||
MaxTokens int `json:"max_tokens,omitempty"` // Specific token limit (Anthropic-style)
|
||||
// Optional: Default is false. All models support this.
|
||||
Exclude bool `json:"exclude,omitempty"` // Set to true to exclude reasoning tokens from response
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"one-api/relay/channel/gemini"
|
||||
"one-api/relay/channel/openai"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/relay/constant"
|
||||
"one-api/setting/model_setting"
|
||||
"strings"
|
||||
|
||||
@@ -201,7 +202,11 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
|
||||
case RequestModeClaude:
|
||||
err, usage = claude.ClaudeStreamHandler(c, resp, info, claude.RequestModeMessage)
|
||||
case RequestModeGemini:
|
||||
err, usage = gemini.GeminiChatStreamHandler(c, resp, info)
|
||||
if info.RelayMode == constant.RelayModeGemini {
|
||||
usage, err = gemini.GeminiTextGenerationStreamHandler(c, resp, info)
|
||||
} else {
|
||||
err, usage = gemini.GeminiChatStreamHandler(c, resp, info)
|
||||
}
|
||||
case RequestModeLlama:
|
||||
err, usage = openai.OaiStreamHandler(c, resp, info)
|
||||
}
|
||||
@@ -210,7 +215,11 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
|
||||
case RequestModeClaude:
|
||||
err, usage = claude.ClaudeHandler(c, resp, claude.RequestModeMessage, info)
|
||||
case RequestModeGemini:
|
||||
err, usage = gemini.GeminiChatHandler(c, resp, info)
|
||||
if info.RelayMode == constant.RelayModeGemini {
|
||||
usage, err = gemini.GeminiTextGenerationHandler(c, resp, info)
|
||||
} else {
|
||||
err, usage = gemini.GeminiChatHandler(c, resp, info)
|
||||
}
|
||||
case RequestModeLlama:
|
||||
err, usage = openai.OpenaiHandler(c, resp, info)
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ const (
|
||||
RelayModeResponses
|
||||
|
||||
RelayModeRealtime
|
||||
|
||||
RelayModeGemini
|
||||
)
|
||||
|
||||
func Path2RelayMode(path string) int {
|
||||
@@ -75,6 +77,8 @@ func Path2RelayMode(path string) int {
|
||||
relayMode = RelayModeRerank
|
||||
} else if strings.HasPrefix(path, "/v1/realtime") {
|
||||
relayMode = RelayModeRealtime
|
||||
} else if strings.HasPrefix(path, "/v1beta/models") {
|
||||
relayMode = RelayModeGemini
|
||||
}
|
||||
return relayMode
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
package relay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/dto"
|
||||
"one-api/relay/channel/gemini"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/relay/helper"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func getAndValidateGeminiRequest(c *gin.Context) (*gemini.GeminiChatRequest, error) {
|
||||
request := &gemini.GeminiChatRequest{}
|
||||
err := common.UnmarshalBodyReusable(c, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(request.Contents) == 0 {
|
||||
return nil, errors.New("contents is required")
|
||||
}
|
||||
return request, nil
|
||||
}
|
||||
|
||||
// 流模式
|
||||
// /v1beta/models/gemini-2.0-flash:streamGenerateContent?alt=sse&key=xxx
|
||||
func checkGeminiStreamMode(c *gin.Context, relayInfo *relaycommon.RelayInfo) {
|
||||
if c.Query("alt") == "sse" {
|
||||
relayInfo.IsStream = true
|
||||
}
|
||||
|
||||
// if strings.Contains(c.Request.URL.Path, "streamGenerateContent") {
|
||||
// relayInfo.IsStream = true
|
||||
// }
|
||||
}
|
||||
|
||||
func checkGeminiInputSensitive(textRequest *gemini.GeminiChatRequest) ([]string, error) {
|
||||
var inputTexts []string
|
||||
for _, content := range textRequest.Contents {
|
||||
for _, part := range content.Parts {
|
||||
if part.Text != "" {
|
||||
inputTexts = append(inputTexts, part.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(inputTexts) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
sensitiveWords, err := service.CheckSensitiveInput(inputTexts)
|
||||
return sensitiveWords, err
|
||||
}
|
||||
|
||||
func getGeminiInputTokens(req *gemini.GeminiChatRequest, info *relaycommon.RelayInfo) (int, error) {
|
||||
// 计算输入 token 数量
|
||||
var inputTexts []string
|
||||
for _, content := range req.Contents {
|
||||
for _, part := range content.Parts {
|
||||
if part.Text != "" {
|
||||
inputTexts = append(inputTexts, part.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inputText := strings.Join(inputTexts, "\n")
|
||||
inputTokens, err := service.CountTokenInput(inputText, info.UpstreamModelName)
|
||||
info.PromptTokens = inputTokens
|
||||
return inputTokens, err
|
||||
}
|
||||
|
||||
func GeminiHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
|
||||
req, err := getAndValidateGeminiRequest(c)
|
||||
if err != nil {
|
||||
common.LogError(c, fmt.Sprintf("getAndValidateGeminiRequest error: %s", err.Error()))
|
||||
return service.OpenAIErrorWrapperLocal(err, "invalid_gemini_request", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
relayInfo := relaycommon.GenRelayInfo(c)
|
||||
|
||||
// 检查 Gemini 流式模式
|
||||
checkGeminiStreamMode(c, relayInfo)
|
||||
|
||||
if setting.ShouldCheckPromptSensitive() {
|
||||
sensitiveWords, err := checkGeminiInputSensitive(req)
|
||||
if err != nil {
|
||||
common.LogWarn(c, fmt.Sprintf("user sensitive words detected: %s", strings.Join(sensitiveWords, ", ")))
|
||||
return service.OpenAIErrorWrapperLocal(err, "check_request_sensitive_error", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
// model mapped 模型映射
|
||||
err = helper.ModelMappedHelper(c, relayInfo)
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapperLocal(err, "model_mapped_error", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if value, exists := c.Get("prompt_tokens"); exists {
|
||||
promptTokens := value.(int)
|
||||
relayInfo.SetPromptTokens(promptTokens)
|
||||
} else {
|
||||
promptTokens, err := getGeminiInputTokens(req, relayInfo)
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapperLocal(err, "count_input_tokens_error", http.StatusBadRequest)
|
||||
}
|
||||
c.Set("prompt_tokens", promptTokens)
|
||||
}
|
||||
|
||||
priceData, err := helper.ModelPriceHelper(c, relayInfo, relayInfo.PromptTokens, int(req.GenerationConfig.MaxOutputTokens))
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapperLocal(err, "model_price_error", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// pre consume quota
|
||||
preConsumedQuota, userQuota, openaiErr := preConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
|
||||
if openaiErr != nil {
|
||||
return openaiErr
|
||||
}
|
||||
defer func() {
|
||||
if openaiErr != nil {
|
||||
returnPreConsumedQuota(c, relayInfo, userQuota, preConsumedQuota)
|
||||
}
|
||||
}()
|
||||
|
||||
adaptor := GetAdaptor(relayInfo.ApiType)
|
||||
if adaptor == nil {
|
||||
return service.OpenAIErrorWrapperLocal(fmt.Errorf("invalid api type: %d", relayInfo.ApiType), "invalid_api_type", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
adaptor.Init(relayInfo)
|
||||
|
||||
requestBody, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapperLocal(err, "marshal_text_request_failed", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
resp, err := adaptor.DoRequest(c, relayInfo, bytes.NewReader(requestBody))
|
||||
if err != nil {
|
||||
common.LogError(c, "Do gemini request failed: "+err.Error())
|
||||
return service.OpenAIErrorWrapperLocal(err, "do_request_failed", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
usage, openaiErr := adaptor.DoResponse(c, resp.(*http.Response), relayInfo)
|
||||
if openaiErr != nil {
|
||||
return openaiErr
|
||||
}
|
||||
|
||||
postConsumeQuota(c, relayInfo, usage.(*dto.Usage), preConsumedQuota, userQuota, priceData, "")
|
||||
return nil
|
||||
}
|
||||
+38
-9
@@ -352,6 +352,7 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
|
||||
promptTokens := usage.PromptTokens
|
||||
cacheTokens := usage.PromptTokensDetails.CachedTokens
|
||||
imageTokens := usage.PromptTokensDetails.ImageTokens
|
||||
audioTokens := usage.PromptTokensDetails.AudioTokens
|
||||
completionTokens := usage.CompletionTokens
|
||||
modelName := relayInfo.OriginModelName
|
||||
|
||||
@@ -367,6 +368,7 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
|
||||
dPromptTokens := decimal.NewFromInt(int64(promptTokens))
|
||||
dCacheTokens := decimal.NewFromInt(int64(cacheTokens))
|
||||
dImageTokens := decimal.NewFromInt(int64(imageTokens))
|
||||
dAudioTokens := decimal.NewFromInt(int64(audioTokens))
|
||||
dCompletionTokens := decimal.NewFromInt(int64(completionTokens))
|
||||
dCompletionRatio := decimal.NewFromFloat(completionRatio)
|
||||
dCacheRatio := decimal.NewFromFloat(cacheRatio)
|
||||
@@ -412,23 +414,43 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
|
||||
dFileSearchQuota = decimal.NewFromFloat(fileSearchPrice).
|
||||
Mul(decimal.NewFromInt(int64(fileSearchTool.CallCount))).
|
||||
Div(decimal.NewFromInt(1000)).Mul(dGroupRatio).Mul(dQuotaPerUnit)
|
||||
extraContent += fmt.Sprintf("File Search 调用 %d 次,调用花费 $%s",
|
||||
extraContent += fmt.Sprintf("File Search 调用 %d 次,调用花费 %s",
|
||||
fileSearchTool.CallCount, dFileSearchQuota.String())
|
||||
}
|
||||
}
|
||||
|
||||
var quotaCalculateDecimal decimal.Decimal
|
||||
if !priceData.UsePrice {
|
||||
nonCachedTokens := dPromptTokens.Sub(dCacheTokens)
|
||||
cachedTokensWithRatio := dCacheTokens.Mul(dCacheRatio)
|
||||
|
||||
promptQuota := nonCachedTokens.Add(cachedTokensWithRatio)
|
||||
if imageTokens > 0 {
|
||||
nonImageTokens := dPromptTokens.Sub(dImageTokens)
|
||||
imageTokensWithRatio := dImageTokens.Mul(dImageRatio)
|
||||
promptQuota = nonImageTokens.Add(imageTokensWithRatio)
|
||||
var audioInputQuota decimal.Decimal
|
||||
var audioInputPrice float64
|
||||
if !priceData.UsePrice {
|
||||
baseTokens := dPromptTokens
|
||||
// 减去 cached tokens
|
||||
var cachedTokensWithRatio decimal.Decimal
|
||||
if !dCacheTokens.IsZero() {
|
||||
baseTokens = baseTokens.Sub(dCacheTokens)
|
||||
cachedTokensWithRatio = dCacheTokens.Mul(dCacheRatio)
|
||||
}
|
||||
|
||||
// 减去 image tokens
|
||||
var imageTokensWithRatio decimal.Decimal
|
||||
if !dImageTokens.IsZero() {
|
||||
baseTokens = baseTokens.Sub(dImageTokens)
|
||||
imageTokensWithRatio = dImageTokens.Mul(dImageRatio)
|
||||
}
|
||||
|
||||
// 减去 Gemini audio tokens
|
||||
if !dAudioTokens.IsZero() {
|
||||
audioInputPrice = operation_setting.GetGeminiInputAudioPricePerMillionTokens(modelName)
|
||||
if audioInputPrice > 0 {
|
||||
// 重新计算 base tokens
|
||||
baseTokens = baseTokens.Sub(dAudioTokens)
|
||||
audioInputQuota = decimal.NewFromFloat(audioInputPrice).Div(decimal.NewFromInt(1000000)).Mul(dAudioTokens).Mul(dGroupRatio).Mul(dQuotaPerUnit)
|
||||
extraContent += fmt.Sprintf("Audio Input 花费 %s", audioInputQuota.String())
|
||||
}
|
||||
}
|
||||
promptQuota := baseTokens.Add(cachedTokensWithRatio).Add(imageTokensWithRatio)
|
||||
|
||||
completionQuota := dCompletionTokens.Mul(dCompletionRatio)
|
||||
|
||||
quotaCalculateDecimal = promptQuota.Add(completionQuota).Mul(ratio)
|
||||
@@ -442,6 +464,8 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
|
||||
// 添加 responses tools call 调用的配额
|
||||
quotaCalculateDecimal = quotaCalculateDecimal.Add(dWebSearchQuota)
|
||||
quotaCalculateDecimal = quotaCalculateDecimal.Add(dFileSearchQuota)
|
||||
// 添加 audio input 独立计费
|
||||
quotaCalculateDecimal = quotaCalculateDecimal.Add(audioInputQuota)
|
||||
|
||||
quota := int(quotaCalculateDecimal.Round(0).IntPart())
|
||||
totalTokens := promptTokens + completionTokens
|
||||
@@ -512,6 +536,11 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
|
||||
other["file_search_price"] = fileSearchPrice
|
||||
}
|
||||
}
|
||||
if !audioInputQuota.IsZero() {
|
||||
other["audio_input_seperate_price"] = true
|
||||
other["audio_input_token_count"] = audioTokens
|
||||
other["audio_input_price"] = audioInputPrice
|
||||
}
|
||||
model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, promptTokens, completionTokens, logModel,
|
||||
tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream, relayInfo.Group, other)
|
||||
}
|
||||
|
||||
@@ -79,6 +79,14 @@ func SetRelayRouter(router *gin.Engine) {
|
||||
relaySunoRouter.GET("/fetch/:id", controller.RelayTask)
|
||||
}
|
||||
|
||||
relayGeminiRouter := router.Group("/v1beta")
|
||||
relayGeminiRouter.Use(middleware.TokenAuth())
|
||||
relayGeminiRouter.Use(middleware.ModelRequestRateLimit())
|
||||
relayGeminiRouter.Use(middleware.Distribute())
|
||||
{
|
||||
// Gemini API 路径格式: /v1beta/models/{model_name}:{action}
|
||||
relayGeminiRouter.POST("/models/*path", controller.Relay)
|
||||
}
|
||||
}
|
||||
|
||||
func registerMjRouterGroup(relayMjRouter *gin.RouterGroup) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseAudio(audioBase64 string, format string) (duration float64, err error) {
|
||||
@@ -29,3 +30,19 @@ func parseAudio(audioBase64 string, format string) (duration float64, err error)
|
||||
duration = float64(samplesCount) / float64(sampleRate)
|
||||
return duration, nil
|
||||
}
|
||||
|
||||
func DecodeBase64AudioData(audioBase64 string) (string, error) {
|
||||
// 检查并移除 data:audio/xxx;base64, 前缀
|
||||
idx := strings.Index(audioBase64, ",")
|
||||
if idx != -1 {
|
||||
audioBase64 = audioBase64[idx+1:]
|
||||
}
|
||||
|
||||
// 解码 Base64 数据
|
||||
_, err := base64.StdEncoding.DecodeString(audioBase64)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("base64 decode error: %v", err)
|
||||
}
|
||||
|
||||
return audioBase64, nil
|
||||
}
|
||||
|
||||
+40
-10
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"one-api/common"
|
||||
"one-api/dto"
|
||||
"one-api/relay/channel/openrouter"
|
||||
relaycommon "one-api/relay/common"
|
||||
"strings"
|
||||
)
|
||||
@@ -18,10 +19,24 @@ func ClaudeToOpenAIRequest(claudeRequest dto.ClaudeRequest, info *relaycommon.Re
|
||||
Stream: claudeRequest.Stream,
|
||||
}
|
||||
|
||||
isOpenRouter := info.ChannelType == common.ChannelTypeOpenRouter
|
||||
|
||||
if claudeRequest.Thinking != nil {
|
||||
if strings.HasSuffix(info.OriginModelName, "-thinking") &&
|
||||
!strings.HasSuffix(claudeRequest.Model, "-thinking") {
|
||||
openAIRequest.Model = openAIRequest.Model + "-thinking"
|
||||
if isOpenRouter {
|
||||
reasoning := openrouter.RequestReasoning{
|
||||
MaxTokens: claudeRequest.Thinking.BudgetTokens,
|
||||
}
|
||||
reasoningJSON, err := json.Marshal(reasoning)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal reasoning: %w", err)
|
||||
}
|
||||
openAIRequest.Reasoning = reasoningJSON
|
||||
} else {
|
||||
thinkingSuffix := "-thinking"
|
||||
if strings.HasSuffix(info.OriginModelName, thinkingSuffix) &&
|
||||
!strings.HasSuffix(openAIRequest.Model, thinkingSuffix) {
|
||||
openAIRequest.Model = openAIRequest.Model + thinkingSuffix
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,16 +77,30 @@ func ClaudeToOpenAIRequest(claudeRequest dto.ClaudeRequest, info *relaycommon.Re
|
||||
} else {
|
||||
systems := claudeRequest.ParseSystem()
|
||||
if len(systems) > 0 {
|
||||
systemStr := ""
|
||||
openAIMessage := dto.Message{
|
||||
Role: "system",
|
||||
}
|
||||
for _, system := range systems {
|
||||
if system.Text != nil {
|
||||
systemStr += *system.Text
|
||||
isOpenRouterClaude := isOpenRouter && strings.HasPrefix(info.UpstreamModelName, "anthropic/claude")
|
||||
if isOpenRouterClaude {
|
||||
systemMediaMessages := make([]dto.MediaContent, 0, len(systems))
|
||||
for _, system := range systems {
|
||||
message := dto.MediaContent{
|
||||
Type: "text",
|
||||
Text: system.GetText(),
|
||||
CacheControl: system.CacheControl,
|
||||
}
|
||||
systemMediaMessages = append(systemMediaMessages, message)
|
||||
}
|
||||
openAIMessage.SetMediaContent(systemMediaMessages)
|
||||
} else {
|
||||
systemStr := ""
|
||||
for _, system := range systems {
|
||||
if system.Text != nil {
|
||||
systemStr += *system.Text
|
||||
}
|
||||
}
|
||||
openAIMessage.SetStringContent(systemStr)
|
||||
}
|
||||
openAIMessage.SetStringContent(systemStr)
|
||||
openAIMessages = append(openAIMessages, openAIMessage)
|
||||
}
|
||||
}
|
||||
@@ -97,8 +126,9 @@ func ClaudeToOpenAIRequest(claudeRequest dto.ClaudeRequest, info *relaycommon.Re
|
||||
switch mediaMsg.Type {
|
||||
case "text":
|
||||
message := dto.MediaContent{
|
||||
Type: "text",
|
||||
Text: mediaMsg.GetText(),
|
||||
Type: "text",
|
||||
Text: mediaMsg.GetText(),
|
||||
CacheControl: mediaMsg.CacheControl,
|
||||
}
|
||||
mediaMessages = append(mediaMessages, message)
|
||||
case "image":
|
||||
|
||||
@@ -14,6 +14,13 @@ const (
|
||||
FileSearchPrice = 2.5
|
||||
)
|
||||
|
||||
const (
|
||||
// Gemini Audio Input Price
|
||||
Gemini25FlashPreviewInputAudioPrice = 1.00
|
||||
Gemini25FlashNativeAudioInputAudioPrice = 3.00
|
||||
Gemini20FlashInputAudioPrice = 0.70
|
||||
)
|
||||
|
||||
func GetWebSearchPricePerThousand(modelName string, contextSize string) float64 {
|
||||
// 确定模型类型
|
||||
// https://platform.openai.com/docs/pricing Web search 价格按模型类型和 search context size 收费
|
||||
@@ -55,3 +62,14 @@ func GetWebSearchPricePerThousand(modelName string, contextSize string) float64
|
||||
func GetFileSearchPricePerThousand() float64 {
|
||||
return FileSearchPrice
|
||||
}
|
||||
|
||||
func GetGeminiInputAudioPricePerMillionTokens(modelName string) float64 {
|
||||
if strings.HasPrefix(modelName, "gemini-2.5-flash-preview") {
|
||||
return Gemini25FlashPreviewInputAudioPrice
|
||||
} else if strings.HasPrefix(modelName, "gemini-2.5-flash-preview-native-audio") {
|
||||
return Gemini25FlashNativeAudioInputAudioPrice
|
||||
} else if strings.HasPrefix(modelName, "gemini-2.0-flash") {
|
||||
return Gemini20FlashInputAudioPrice
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 550 KiB After Width: | Height: | Size: 550 KiB |
+10
-12
@@ -1,15 +1,15 @@
|
||||
import React, { lazy, Suspense, useContext, useEffect } from 'react';
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import { Route, Routes, useLocation } from 'react-router-dom';
|
||||
import Loading from './components/Loading';
|
||||
import Loading from './components/common/Loading.js';
|
||||
import User from './pages/User';
|
||||
import { PrivateRoute } from './components/PrivateRoute';
|
||||
import RegisterForm from './components/RegisterForm';
|
||||
import LoginForm from './components/LoginForm';
|
||||
import { AuthRedirect, PrivateRoute } from './helpers';
|
||||
import RegisterForm from './components/auth/RegisterForm.js';
|
||||
import LoginForm from './components/auth/LoginForm.js';
|
||||
import NotFound from './pages/NotFound';
|
||||
import Setting from './pages/Setting';
|
||||
import EditUser from './pages/User/EditUser';
|
||||
import PasswordResetForm from './components/PasswordResetForm';
|
||||
import PasswordResetConfirm from './components/PasswordResetConfirm';
|
||||
import PasswordResetForm from './components/auth/PasswordResetForm.js';
|
||||
import PasswordResetConfirm from './components/auth/PasswordResetConfirm.js';
|
||||
import Channel from './pages/Channel';
|
||||
import Token from './pages/Token';
|
||||
import EditChannel from './pages/Channel/EditChannel';
|
||||
@@ -18,16 +18,14 @@ import TopUp from './pages/TopUp';
|
||||
import Log from './pages/Log';
|
||||
import Chat from './pages/Chat';
|
||||
import Chat2Link from './pages/Chat2Link';
|
||||
import { Layout } from '@douyinfe/semi-ui';
|
||||
import Midjourney from './pages/Midjourney';
|
||||
import Pricing from './pages/Pricing/index.js';
|
||||
import Task from './pages/Task/index.js';
|
||||
import Playground from './pages/Playground/index.js';
|
||||
import OAuth2Callback from './components/OAuth2Callback.js';
|
||||
import PersonalSetting from './components/PersonalSetting.js';
|
||||
import OAuth2Callback from './components/auth/OAuth2Callback.js';
|
||||
import PersonalSetting from './components/settings/PersonalSetting.js';
|
||||
import Setup from './pages/Setup/index.js';
|
||||
import SetupCheck from './components/SetupCheck';
|
||||
import AuthRedirect from './components/AuthRedirect';
|
||||
import SetupCheck from './components/layout/SetupCheck.js';
|
||||
|
||||
const Home = lazy(() => import('./pages/Home'));
|
||||
const Detail = lazy(() => import('./pages/Detail'));
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
const AuthRedirect = ({ children }) => {
|
||||
const user = localStorage.getItem('user');
|
||||
|
||||
if (user) {
|
||||
return <Navigate to="/console" replace />;
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
export default AuthRedirect;
|
||||
@@ -1,94 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Modal, Empty } from '@douyinfe/semi-ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { API, showError } from '../helpers';
|
||||
import { marked } from 'marked';
|
||||
import { IllustrationNoContent, IllustrationNoContentDark } from '@douyinfe/semi-illustrations';
|
||||
|
||||
const NoticeModal = ({ visible, onClose, isMobile }) => {
|
||||
const { t } = useTranslation();
|
||||
const [noticeContent, setNoticeContent] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleCloseTodayNotice = () => {
|
||||
const today = new Date().toDateString();
|
||||
localStorage.setItem('notice_close_date', today);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const displayNotice = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await API.get('/api/notice');
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
if (data !== '') {
|
||||
const htmlNotice = marked.parse(data);
|
||||
setNoticeContent(htmlNotice);
|
||||
} else {
|
||||
setNoticeContent('');
|
||||
}
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
displayNotice();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const renderContent = () => {
|
||||
if (loading) {
|
||||
return <div className="py-12"><Empty description={t('加载中...')} /></div>;
|
||||
}
|
||||
|
||||
if (!noticeContent) {
|
||||
return (
|
||||
<div className="py-12">
|
||||
<Empty
|
||||
image={<IllustrationNoContent style={{ width: 150, height: 150 }} />}
|
||||
darkModeImage={<IllustrationNoContentDark style={{ width: 150, height: 150 }} />}
|
||||
description={t('暂无公告')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: noticeContent }}
|
||||
className="max-h-[60vh] overflow-y-auto pr-2"
|
||||
style={{
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: 'var(--semi-color-tertiary) transparent'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('系统公告')}
|
||||
visible={visible}
|
||||
onCancel={onClose}
|
||||
footer={(
|
||||
<div className="flex justify-end">
|
||||
<Button type='secondary' className='!rounded-full' onClick={handleCloseTodayNotice}>{t('今日关闭')}</Button>
|
||||
<Button type="primary" className='!rounded-full' onClick={onClose}>{t('关闭公告')}</Button>
|
||||
</div>
|
||||
)}
|
||||
size={isMobile ? 'full-width' : 'large'}
|
||||
>
|
||||
{renderContent()}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoticeModal;
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
import { history } from '../helpers';
|
||||
|
||||
function PrivateRoute({ children }) {
|
||||
if (!localStorage.getItem('user')) {
|
||||
return <Navigate to='/login' state={{ from: history.location }} />;
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
export { PrivateRoute };
|
||||
@@ -1,519 +0,0 @@
|
||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { UserContext } from '../context/User';
|
||||
import { StatusContext } from '../context/Status';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
API,
|
||||
getLogo,
|
||||
getSystemName,
|
||||
isAdmin,
|
||||
isMobile,
|
||||
showError,
|
||||
} from '../helpers';
|
||||
import '../index.css';
|
||||
|
||||
import {
|
||||
IconCalendarClock,
|
||||
IconChecklistStroked,
|
||||
IconComment,
|
||||
IconTerminal,
|
||||
IconCreditCard,
|
||||
IconGift,
|
||||
IconHelpCircle,
|
||||
IconHistogram,
|
||||
IconHome,
|
||||
IconImage,
|
||||
IconKey,
|
||||
IconLayers,
|
||||
IconPriceTag,
|
||||
IconSetting,
|
||||
IconUser,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import {
|
||||
Avatar,
|
||||
Dropdown,
|
||||
Layout,
|
||||
Nav,
|
||||
Switch,
|
||||
Divider,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { setStatusData } from '../helpers/data.js';
|
||||
import { stringToColor } from '../helpers/render.js';
|
||||
import { useSetTheme, useTheme } from '../context/Theme/index.js';
|
||||
import { useStyle, styleActions } from '../context/Style/index.js';
|
||||
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
|
||||
|
||||
// 自定义侧边栏按钮样式
|
||||
const navItemStyle = {
|
||||
borderRadius: '6px',
|
||||
margin: '4px 8px',
|
||||
};
|
||||
|
||||
// 自定义侧边栏按钮悬停样式
|
||||
const navItemHoverStyle = {
|
||||
backgroundColor: 'var(--semi-color-primary-light-default)',
|
||||
color: 'var(--semi-color-primary)',
|
||||
};
|
||||
|
||||
// 自定义侧边栏按钮选中样式
|
||||
const navItemSelectedStyle = {
|
||||
backgroundColor: 'var(--semi-color-primary-light-default)',
|
||||
color: 'var(--semi-color-primary)',
|
||||
fontWeight: '600',
|
||||
};
|
||||
|
||||
// 自定义图标样式
|
||||
const iconStyle = (itemKey, selectedKeys) => {
|
||||
return {
|
||||
fontSize: '18px',
|
||||
color: selectedKeys.includes(itemKey)
|
||||
? 'var(--semi-color-primary)'
|
||||
: 'var(--semi-color-text-2)',
|
||||
};
|
||||
};
|
||||
|
||||
// Define routerMap as a constant outside the component
|
||||
const routerMap = {
|
||||
home: '/',
|
||||
channel: '/console/channel',
|
||||
token: '/console/token',
|
||||
redemption: '/console/redemption',
|
||||
topup: '/console/topup',
|
||||
user: '/console/user',
|
||||
log: '/console/log',
|
||||
midjourney: '/console/midjourney',
|
||||
setting: '/console/setting',
|
||||
about: '/about',
|
||||
detail: '/console',
|
||||
pricing: '/pricing',
|
||||
task: '/console/task',
|
||||
playground: '/console/playground',
|
||||
personal: '/console/personal',
|
||||
};
|
||||
|
||||
const SiderBar = () => {
|
||||
const { t } = useTranslation();
|
||||
const { state: styleState, dispatch: styleDispatch } = useStyle();
|
||||
const [statusState, statusDispatch] = useContext(StatusContext);
|
||||
|
||||
const [selectedKeys, setSelectedKeys] = useState(['home']);
|
||||
const [isCollapsed, setIsCollapsed] = useState(styleState.siderCollapsed);
|
||||
const [chatItems, setChatItems] = useState([]);
|
||||
const [openedKeys, setOpenedKeys] = useState([]);
|
||||
const theme = useTheme();
|
||||
const setTheme = useSetTheme();
|
||||
const location = useLocation();
|
||||
const [routerMapState, setRouterMapState] = useState(routerMap);
|
||||
|
||||
// 预先计算所有可能的图标样式
|
||||
const allItemKeys = useMemo(() => {
|
||||
const keys = [
|
||||
'home',
|
||||
'channel',
|
||||
'token',
|
||||
'redemption',
|
||||
'topup',
|
||||
'user',
|
||||
'log',
|
||||
'midjourney',
|
||||
'setting',
|
||||
'about',
|
||||
'chat',
|
||||
'detail',
|
||||
'pricing',
|
||||
'task',
|
||||
'playground',
|
||||
'personal',
|
||||
];
|
||||
// 添加聊天项的keys
|
||||
for (let i = 0; i < chatItems.length; i++) {
|
||||
keys.push('chat' + i);
|
||||
}
|
||||
return keys;
|
||||
}, [chatItems]);
|
||||
|
||||
// 使用useMemo一次性计算所有图标样式
|
||||
const iconStyles = useMemo(() => {
|
||||
const styles = {};
|
||||
allItemKeys.forEach((key) => {
|
||||
styles[key] = iconStyle(key, selectedKeys);
|
||||
});
|
||||
return styles;
|
||||
}, [allItemKeys, selectedKeys]);
|
||||
|
||||
const workspaceItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
text: t('数据看板'),
|
||||
itemKey: 'detail',
|
||||
to: '/detail',
|
||||
icon: <IconCalendarClock />,
|
||||
className:
|
||||
localStorage.getItem('enable_data_export') === 'true'
|
||||
? ''
|
||||
: 'tableHiddle',
|
||||
},
|
||||
{
|
||||
text: t('API令牌'),
|
||||
itemKey: 'token',
|
||||
to: '/token',
|
||||
icon: <IconKey />,
|
||||
},
|
||||
{
|
||||
text: t('使用日志'),
|
||||
itemKey: 'log',
|
||||
to: '/log',
|
||||
icon: <IconHistogram />,
|
||||
},
|
||||
{
|
||||
text: t('绘图日志'),
|
||||
itemKey: 'midjourney',
|
||||
to: '/midjourney',
|
||||
icon: <IconImage />,
|
||||
className:
|
||||
localStorage.getItem('enable_drawing') === 'true'
|
||||
? ''
|
||||
: 'tableHiddle',
|
||||
},
|
||||
{
|
||||
text: t('任务日志'),
|
||||
itemKey: 'task',
|
||||
to: '/task',
|
||||
icon: <IconChecklistStroked />,
|
||||
className:
|
||||
localStorage.getItem('enable_task') === 'true' ? '' : 'tableHiddle',
|
||||
},
|
||||
],
|
||||
[
|
||||
localStorage.getItem('enable_data_export'),
|
||||
localStorage.getItem('enable_drawing'),
|
||||
localStorage.getItem('enable_task'),
|
||||
t,
|
||||
],
|
||||
);
|
||||
|
||||
const financeItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
text: t('钱包'),
|
||||
itemKey: 'topup',
|
||||
to: '/topup',
|
||||
icon: <IconCreditCard />,
|
||||
},
|
||||
{
|
||||
text: t('个人设置'),
|
||||
itemKey: 'personal',
|
||||
to: '/personal',
|
||||
icon: <IconUser />,
|
||||
},
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
const adminItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
text: t('渠道'),
|
||||
itemKey: 'channel',
|
||||
to: '/channel',
|
||||
icon: <IconLayers />,
|
||||
className: isAdmin() ? '' : 'tableHiddle',
|
||||
},
|
||||
{
|
||||
text: t('兑换码'),
|
||||
itemKey: 'redemption',
|
||||
to: '/redemption',
|
||||
icon: <IconGift />,
|
||||
className: isAdmin() ? '' : 'tableHiddle',
|
||||
},
|
||||
{
|
||||
text: t('用户管理'),
|
||||
itemKey: 'user',
|
||||
to: '/user',
|
||||
icon: <IconUser />,
|
||||
},
|
||||
{
|
||||
text: t('系统设置'),
|
||||
itemKey: 'setting',
|
||||
to: '/setting',
|
||||
icon: <IconSetting />,
|
||||
},
|
||||
],
|
||||
[isAdmin(), t],
|
||||
);
|
||||
|
||||
const chatMenuItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
text: t('操练场'),
|
||||
itemKey: 'playground',
|
||||
to: '/playground',
|
||||
icon: <IconTerminal />,
|
||||
},
|
||||
{
|
||||
text: t('聊天'),
|
||||
itemKey: 'chat',
|
||||
items: chatItems,
|
||||
icon: <IconComment />,
|
||||
},
|
||||
],
|
||||
[chatItems, t],
|
||||
);
|
||||
|
||||
// Function to update router map with chat routes
|
||||
const updateRouterMapWithChats = (chats) => {
|
||||
const newRouterMap = { ...routerMap };
|
||||
|
||||
if (Array.isArray(chats) && chats.length > 0) {
|
||||
for (let i = 0; i < chats.length; i++) {
|
||||
newRouterMap['chat' + i] = '/console/chat/' + i;
|
||||
}
|
||||
}
|
||||
|
||||
setRouterMapState(newRouterMap);
|
||||
return newRouterMap;
|
||||
};
|
||||
|
||||
// Update the useEffect for chat items
|
||||
useEffect(() => {
|
||||
let chats = localStorage.getItem('chats');
|
||||
if (chats) {
|
||||
try {
|
||||
chats = JSON.parse(chats);
|
||||
if (Array.isArray(chats)) {
|
||||
let chatItems = [];
|
||||
for (let i = 0; i < chats.length; i++) {
|
||||
let chat = {};
|
||||
for (let key in chats[i]) {
|
||||
chat.text = key;
|
||||
chat.itemKey = 'chat' + i;
|
||||
chat.to = '/console/chat/' + i;
|
||||
}
|
||||
chatItems.push(chat);
|
||||
}
|
||||
setChatItems(chatItems);
|
||||
|
||||
// Update router map with chat routes
|
||||
updateRouterMapWithChats(chats);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showError('聊天数据解析失败');
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Update the useEffect for route selection
|
||||
useEffect(() => {
|
||||
const currentPath = location.pathname;
|
||||
let matchingKey = Object.keys(routerMapState).find(
|
||||
(key) => routerMapState[key] === currentPath,
|
||||
);
|
||||
|
||||
// Handle chat routes
|
||||
if (!matchingKey && currentPath.startsWith('/console/chat/')) {
|
||||
const chatIndex = currentPath.split('/').pop();
|
||||
if (!isNaN(chatIndex)) {
|
||||
matchingKey = 'chat' + chatIndex;
|
||||
} else {
|
||||
matchingKey = 'chat';
|
||||
}
|
||||
}
|
||||
|
||||
// If we found a matching key, update the selected keys
|
||||
if (matchingKey) {
|
||||
setSelectedKeys([matchingKey]);
|
||||
}
|
||||
}, [location.pathname, routerMapState]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsCollapsed(styleState.siderCollapsed);
|
||||
}, [styleState.siderCollapsed]);
|
||||
|
||||
// Custom divider style
|
||||
const dividerStyle = {
|
||||
margin: '8px 0',
|
||||
opacity: 0.6,
|
||||
};
|
||||
|
||||
// Custom group label style
|
||||
const groupLabelStyle = {
|
||||
padding: '8px 16px',
|
||||
color: 'var(--semi-color-text-2)',
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.5px',
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Nav
|
||||
className='custom-sidebar-nav'
|
||||
style={{
|
||||
width: isCollapsed ? '60px' : '200px',
|
||||
borderRight: '1px solid var(--semi-color-border)',
|
||||
background: 'var(--semi-color-bg-0)',
|
||||
borderRadius: styleState.isMobile ? '0' : '0 8px 8px 0',
|
||||
position: 'relative',
|
||||
zIndex: 95,
|
||||
height: '100%',
|
||||
overflowY: 'auto',
|
||||
WebkitOverflowScrolling: 'touch', // Improve scrolling on iOS devices
|
||||
}}
|
||||
defaultIsCollapsed={styleState.siderCollapsed}
|
||||
isCollapsed={isCollapsed}
|
||||
onCollapseChange={(collapsed) => {
|
||||
setIsCollapsed(collapsed);
|
||||
styleDispatch(styleActions.setSiderCollapsed(collapsed));
|
||||
|
||||
// 确保在收起侧边栏时有选中的项目,避免不必要的计算
|
||||
if (selectedKeys.length === 0) {
|
||||
const currentPath = location.pathname;
|
||||
const matchingKey = Object.keys(routerMapState).find(
|
||||
(key) => routerMapState[key] === currentPath,
|
||||
);
|
||||
|
||||
if (matchingKey) {
|
||||
setSelectedKeys([matchingKey]);
|
||||
} else if (currentPath.startsWith('/console/chat/')) {
|
||||
setSelectedKeys(['chat']);
|
||||
} else {
|
||||
setSelectedKeys(['detail']); // 默认选中首页
|
||||
}
|
||||
}
|
||||
}}
|
||||
selectedKeys={selectedKeys}
|
||||
itemStyle={navItemStyle}
|
||||
hoverStyle={navItemHoverStyle}
|
||||
selectedStyle={navItemSelectedStyle}
|
||||
renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
|
||||
return (
|
||||
<Link
|
||||
style={{ textDecoration: 'none' }}
|
||||
to={routerMapState[props.itemKey] || routerMap[props.itemKey]}
|
||||
>
|
||||
{itemElement}
|
||||
</Link>
|
||||
);
|
||||
}}
|
||||
onSelect={(key) => {
|
||||
// 如果点击的是已经展开的子菜单的父项,则收起子菜单
|
||||
if (openedKeys.includes(key.itemKey)) {
|
||||
setOpenedKeys(openedKeys.filter((k) => k !== key.itemKey));
|
||||
}
|
||||
|
||||
setSelectedKeys([key.itemKey]);
|
||||
}}
|
||||
openKeys={openedKeys}
|
||||
onOpenChange={(data) => {
|
||||
setOpenedKeys(data.openKeys);
|
||||
}}
|
||||
>
|
||||
{/* Chat Section - Only show if there are chat items */}
|
||||
{chatMenuItems.map((item) => {
|
||||
if (item.items && item.items.length > 0) {
|
||||
return (
|
||||
<Nav.Sub
|
||||
key={item.itemKey}
|
||||
itemKey={item.itemKey}
|
||||
text={item.text}
|
||||
icon={React.cloneElement(item.icon, {
|
||||
style: iconStyles[item.itemKey],
|
||||
})}
|
||||
>
|
||||
{item.items.map((subItem) => (
|
||||
<Nav.Item
|
||||
key={subItem.itemKey}
|
||||
itemKey={subItem.itemKey}
|
||||
text={subItem.text}
|
||||
/>
|
||||
))}
|
||||
</Nav.Sub>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Nav.Item
|
||||
key={item.itemKey}
|
||||
itemKey={item.itemKey}
|
||||
text={item.text}
|
||||
icon={React.cloneElement(item.icon, {
|
||||
style: iconStyles[item.itemKey],
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
|
||||
{/* Divider */}
|
||||
<Divider style={dividerStyle} />
|
||||
|
||||
{/* Workspace Section */}
|
||||
{!isCollapsed && <Text style={groupLabelStyle}>{t('控制台')}</Text>}
|
||||
{workspaceItems.map((item) => (
|
||||
<Nav.Item
|
||||
key={item.itemKey}
|
||||
itemKey={item.itemKey}
|
||||
text={item.text}
|
||||
icon={React.cloneElement(item.icon, {
|
||||
style: iconStyles[item.itemKey],
|
||||
})}
|
||||
className={item.className}
|
||||
/>
|
||||
))}
|
||||
|
||||
{isAdmin() && (
|
||||
<>
|
||||
{/* Divider */}
|
||||
<Divider style={dividerStyle} />
|
||||
|
||||
{/* Admin Section */}
|
||||
{!isCollapsed && <Text style={groupLabelStyle}>{t('管理员')}</Text>}
|
||||
{adminItems.map((item) => (
|
||||
<Nav.Item
|
||||
key={item.itemKey}
|
||||
itemKey={item.itemKey}
|
||||
text={item.text}
|
||||
icon={React.cloneElement(item.icon, {
|
||||
style: iconStyles[item.itemKey],
|
||||
})}
|
||||
className={item.className}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Divider */}
|
||||
<Divider style={dividerStyle} />
|
||||
|
||||
{/* Finance Management Section */}
|
||||
{!isCollapsed && <Text style={groupLabelStyle}>{t('个人中心')}</Text>}
|
||||
{financeItems.map((item) => (
|
||||
<Nav.Item
|
||||
key={item.itemKey}
|
||||
itemKey={item.itemKey}
|
||||
text={item.text}
|
||||
icon={React.cloneElement(item.icon, {
|
||||
style: iconStyles[item.itemKey],
|
||||
})}
|
||||
className={item.className}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Nav.Footer
|
||||
collapseButton={true}
|
||||
collapseText={(collapsed) => {
|
||||
if (collapsed) {
|
||||
return t('展开侧边栏');
|
||||
}
|
||||
return t('收起侧边栏');
|
||||
}}
|
||||
/>
|
||||
</Nav>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SiderBar;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { UserContext } from '../context/User';
|
||||
import { UserContext } from '../../context/User/index.js';
|
||||
import {
|
||||
API,
|
||||
getLogo,
|
||||
@@ -9,12 +9,11 @@ import {
|
||||
showSuccess,
|
||||
updateAPI,
|
||||
getSystemName,
|
||||
} from '../helpers';
|
||||
import {
|
||||
setUserData,
|
||||
onGitHubOAuthClicked,
|
||||
onOIDCClicked,
|
||||
onLinuxDOOAuthClicked,
|
||||
} from './utils';
|
||||
onLinuxDOOAuthClicked
|
||||
} from '../../helpers/index.js';
|
||||
import Turnstile from 'react-turnstile';
|
||||
import {
|
||||
Button,
|
||||
@@ -29,12 +28,11 @@ import Text from '@douyinfe/semi-ui/lib/es/typography/text';
|
||||
import TelegramLoginButton from 'react-telegram-login';
|
||||
|
||||
import { IconGithubLogo, IconMail, IconLock } from '@douyinfe/semi-icons';
|
||||
import OIDCIcon from './common/logo/OIDCIcon.js';
|
||||
import WeChatIcon from './common/logo/WeChatIcon.js';
|
||||
import { setUserData } from '../helpers/data.js';
|
||||
import LinuxDoIcon from './common/logo/LinuxDoIcon.js';
|
||||
import OIDCIcon from '../common/logo/OIDCIcon.js';
|
||||
import WeChatIcon from '../common/logo/WeChatIcon.js';
|
||||
import LinuxDoIcon from '../common/logo/LinuxDoIcon.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Background from '../images/example.png';
|
||||
import Background from '/example.png';
|
||||
|
||||
const LoginForm = () => {
|
||||
const [inputs, setInputs] = useState({
|
||||
@@ -296,7 +294,7 @@ const LoginForm = () => {
|
||||
theme='outline'
|
||||
className="w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors"
|
||||
type="tertiary"
|
||||
icon={<IconGithubLogo size="large" style={{ color: '#24292e' }} />}
|
||||
icon={<IconGithubLogo size="large" />}
|
||||
size="large"
|
||||
onClick={handleGitHubClick}
|
||||
loading={githubLoading}
|
||||
@@ -355,7 +353,7 @@ const LoginForm = () => {
|
||||
onClick={handleEmailLoginClick}
|
||||
loading={emailLoginLoading}
|
||||
>
|
||||
<span className="ml-3">{t('使用 邮箱 登录')}</span>
|
||||
<span className="ml-3">{t('使用 邮箱或用户名 登录')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -364,17 +362,6 @@ const LoginForm = () => {
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{turnstileEnabled && (
|
||||
<div className="flex justify-center mt-6">
|
||||
<Turnstile
|
||||
sitekey={turnstileSiteKey}
|
||||
onVerify={(token) => {
|
||||
setTurnstileToken(token);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -397,8 +384,8 @@ const LoginForm = () => {
|
||||
<Form className="space-y-3">
|
||||
<Form.Input
|
||||
field="username"
|
||||
label={t('邮箱')}
|
||||
placeholder={t('请输入您的邮箱地址')}
|
||||
label={t('用户名或邮箱')}
|
||||
placeholder={t('请输入您的用户名或邮箱地址')}
|
||||
name="username"
|
||||
size="large"
|
||||
className="!rounded-md"
|
||||
@@ -444,21 +431,29 @@ const LoginForm = () => {
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
<Divider margin='12px' align='center'>
|
||||
{t('或')}
|
||||
</Divider>
|
||||
{(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth) && (
|
||||
<>
|
||||
<Divider margin='12px' align='center'>
|
||||
{t('或')}
|
||||
</Divider>
|
||||
|
||||
<div className="mt-4 text-center">
|
||||
<Button
|
||||
theme="outline"
|
||||
type="tertiary"
|
||||
className="w-full !rounded-full"
|
||||
size="large"
|
||||
onClick={handleOtherLoginOptionsClick}
|
||||
loading={otherLoginOptionsLoading}
|
||||
>
|
||||
{t('其他登录选项')}
|
||||
</Button>
|
||||
<div className="mt-4 text-center">
|
||||
<Button
|
||||
theme="outline"
|
||||
type="tertiary"
|
||||
className="w-full !rounded-full"
|
||||
size="large"
|
||||
onClick={handleOtherLoginOptionsClick}
|
||||
loading={otherLoginOptionsLoading}
|
||||
>
|
||||
{t('其他登录选项')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="mt-6 text-center text-sm">
|
||||
<Text>{t('没有账户?')} <Link to="/register" className="text-blue-600 hover:text-blue-800 font-medium">{t('注册')}</Link></Text>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -522,6 +517,17 @@ const LoginForm = () => {
|
||||
? renderEmailLoginForm()
|
||||
: renderOAuthOptions()}
|
||||
{renderWeChatLoginModal()}
|
||||
|
||||
{turnstileEnabled && (
|
||||
<div className="flex justify-center mt-6">
|
||||
<Turnstile
|
||||
sitekey={turnstileSiteKey}
|
||||
onVerify={(token) => {
|
||||
setTurnstileToken(token);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Spin, Typography, Space } from '@douyinfe/semi-ui';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { API, showError, showSuccess, updateAPI } from '../helpers';
|
||||
import { UserContext } from '../context/User';
|
||||
import { setUserData } from '../helpers/data.js';
|
||||
import { API, showError, showSuccess, updateAPI, setUserData } from '../../helpers';
|
||||
import { UserContext } from '../../context/User';
|
||||
|
||||
const OAuth2Callback = (props) => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { API, copy, showError, showNotice, getLogo, getSystemName } from '../helpers';
|
||||
import { API, copy, showError, showNotice, getLogo, getSystemName } from '../../helpers';
|
||||
import { useSearchParams, Link } from 'react-router-dom';
|
||||
import { Button, Card, Form, Typography } from '@douyinfe/semi-ui';
|
||||
import { IconMail, IconLock } from '@douyinfe/semi-icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Background from '../images/example.png';
|
||||
import Background from '/example.png';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { API, getLogo, showError, showInfo, showSuccess, getSystemName } from '../helpers';
|
||||
import { API, getLogo, showError, showInfo, showSuccess, getSystemName } from '../../helpers';
|
||||
import Turnstile from 'react-turnstile';
|
||||
import { Button, Card, Form, Typography } from '@douyinfe/semi-ui';
|
||||
import { IconMail } from '@douyinfe/semi-icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Background from '../images/example.png';
|
||||
import Background from '/example.png';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
showSuccess,
|
||||
updateAPI,
|
||||
getSystemName,
|
||||
} from '../helpers';
|
||||
setUserData
|
||||
} from '../../helpers/index.js';
|
||||
import Turnstile from 'react-turnstile';
|
||||
import {
|
||||
Button,
|
||||
@@ -25,15 +26,14 @@ import {
|
||||
onGitHubOAuthClicked,
|
||||
onLinuxDOOAuthClicked,
|
||||
onOIDCClicked,
|
||||
} from './utils.js';
|
||||
import OIDCIcon from './common/logo/OIDCIcon.js';
|
||||
import LinuxDoIcon from './common/logo/LinuxDoIcon.js';
|
||||
import WeChatIcon from './common/logo/WeChatIcon.js';
|
||||
} from '../../helpers/index.js';
|
||||
import OIDCIcon from '../common/logo/OIDCIcon.js';
|
||||
import LinuxDoIcon from '../common/logo/LinuxDoIcon.js';
|
||||
import WeChatIcon from '../common/logo/WeChatIcon.js';
|
||||
import TelegramLoginButton from 'react-telegram-login/src';
|
||||
import { setUserData } from '../helpers/data.js';
|
||||
import { UserContext } from '../context/User/index.js';
|
||||
import { UserContext } from '../../context/User/index.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Background from '../images/example.png';
|
||||
import Background from '/example.png';
|
||||
|
||||
const RegisterForm = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -300,7 +300,7 @@ const RegisterForm = () => {
|
||||
theme='outline'
|
||||
className="w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors"
|
||||
type="tertiary"
|
||||
icon={<IconGithubLogo size="large" style={{ color: '#24292e' }} />}
|
||||
icon={<IconGithubLogo size="large" />}
|
||||
size="large"
|
||||
onClick={handleGitHubClick}
|
||||
loading={githubLoading}
|
||||
@@ -359,7 +359,7 @@ const RegisterForm = () => {
|
||||
onClick={handleEmailRegisterClick}
|
||||
loading={emailRegisterLoading}
|
||||
>
|
||||
<span className="ml-3">{t('使用 邮箱 注册')}</span>
|
||||
<span className="ml-3">{t('使用 用户名 注册')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -368,17 +368,6 @@ const RegisterForm = () => {
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{turnstileEnabled && (
|
||||
<div className="flex justify-center mt-6">
|
||||
<Turnstile
|
||||
sitekey={turnstileSiteKey}
|
||||
onVerify={(token) => {
|
||||
setTurnstileToken(token);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -485,22 +474,26 @@ const RegisterForm = () => {
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
<Divider margin='12px' align='center'>
|
||||
{t('或')}
|
||||
</Divider>
|
||||
{(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth) && (
|
||||
<>
|
||||
<Divider margin='12px' align='center'>
|
||||
{t('或')}
|
||||
</Divider>
|
||||
|
||||
<div className="mt-4 text-center">
|
||||
<Button
|
||||
theme="outline"
|
||||
type="tertiary"
|
||||
className="w-full !rounded-full"
|
||||
size="large"
|
||||
onClick={handleOtherRegisterOptionsClick}
|
||||
loading={otherRegisterOptionsLoading}
|
||||
>
|
||||
{t('其他注册选项')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 text-center">
|
||||
<Button
|
||||
theme="outline"
|
||||
type="tertiary"
|
||||
className="w-full !rounded-full"
|
||||
size="large"
|
||||
onClick={handleOtherRegisterOptionsClick}
|
||||
loading={otherRegisterOptionsLoading}
|
||||
>
|
||||
{t('其他注册选项')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="mt-6 text-center text-sm">
|
||||
<Text>{t('已有账户?')} <Link to="/login" className="text-blue-600 hover:text-blue-800 font-medium">{t('登录')}</Link></Text>
|
||||
@@ -564,6 +557,17 @@ const RegisterForm = () => {
|
||||
? renderEmailRegisterForm()
|
||||
: renderOAuthOptions()}
|
||||
{renderWeChatLoginModal()}
|
||||
|
||||
{turnstileEnabled && (
|
||||
<div className="flex justify-center mt-6">
|
||||
<Turnstile
|
||||
sitekey={turnstileSiteKey}
|
||||
onVerify={(token) => {
|
||||
setTurnstileToken(token);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1,6 +1,6 @@
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import 'highlight.js/styles/default.css';
|
||||
import 'highlight.js/styles/github.css';
|
||||
import './markdown.css';
|
||||
import RemarkMath from 'remark-math';
|
||||
import RemarkBreaks from 'remark-breaks';
|
||||
@@ -13,10 +13,9 @@ import React from 'react';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import clsx from 'clsx';
|
||||
import { Button, Tooltip, Toast } from '@douyinfe/semi-ui';
|
||||
import { copy } from '../../../helpers/utils';
|
||||
import { copy, rehypeSplitWordsIntoSpans } from '../../../helpers';
|
||||
import { IconCopy } from '@douyinfe/semi-icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { rehypeSplitWordsIntoSpans } from '../../../utils/rehypeSplitWordsIntoSpans';
|
||||
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
// src/hooks/useTokenKeys.js
|
||||
import { useEffect, useState } from 'react';
|
||||
import { API, showError } from '../helpers';
|
||||
|
||||
async function fetchTokenKeys() {
|
||||
try {
|
||||
const response = await API.get('/api/token/?p=0&size=100');
|
||||
const { success, data } = response.data;
|
||||
if (success) {
|
||||
const activeTokens = data.filter((token) => token.status === 1);
|
||||
return activeTokens.map((token) => token.key);
|
||||
} else {
|
||||
throw new Error('Failed to fetch token keys');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching token keys:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function getServerAddress() {
|
||||
let status = localStorage.getItem('status');
|
||||
let serverAddress = '';
|
||||
|
||||
if (status) {
|
||||
try {
|
||||
status = JSON.parse(status);
|
||||
serverAddress = status.server_address || '';
|
||||
} catch (error) {
|
||||
console.error('Failed to parse status from localStorage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!serverAddress) {
|
||||
serverAddress = window.location.origin;
|
||||
}
|
||||
|
||||
return serverAddress;
|
||||
}
|
||||
|
||||
export function useTokenKeys(id) {
|
||||
const [keys, setKeys] = useState([]);
|
||||
// const [chatLink, setChatLink] = useState('');
|
||||
const [serverAddress, setServerAddress] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const loadAllData = async () => {
|
||||
const fetchedKeys = await fetchTokenKeys();
|
||||
if (fetchedKeys.length === 0) {
|
||||
showError('当前没有可用的启用令牌,请确认是否有令牌处于启用状态!');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/token';
|
||||
}, 1500); // 延迟 1.5 秒后跳转
|
||||
}
|
||||
setKeys(fetchedKeys);
|
||||
setIsLoading(false);
|
||||
// setChatLink(link);
|
||||
|
||||
const address = getServerAddress();
|
||||
setServerAddress(address);
|
||||
};
|
||||
|
||||
loadAllData();
|
||||
}, []);
|
||||
|
||||
return { keys, serverAddress, isLoading };
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useEffect, useState, useMemo, useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Typography } from '@douyinfe/semi-ui';
|
||||
import { getFooterHTML, getLogo, getSystemName } from '../helpers';
|
||||
import { StatusContext } from '../context/Status';
|
||||
import { getFooterHTML, getLogo, getSystemName } from '../../helpers';
|
||||
import { StatusContext } from '../../context/Status';
|
||||
|
||||
const FooterBar = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -22,7 +22,7 @@ const FooterBar = () => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
const customFooter = useMemo(() => (
|
||||
<footer className="relative bg-gray-900 dark:bg-[#1C1F23] h-auto py-16 px-6 md:px-24 w-full flex flex-col items-center justify-between overflow-hidden">
|
||||
<footer className="relative bg-semi-color-bg-2 h-auto py-16 px-6 md:px-24 w-full flex flex-col items-center justify-between overflow-hidden">
|
||||
<div className="absolute hidden md:block top-[204px] left-[-100px] w-[151px] h-[151px] rounded-full bg-[#FFD166]"></div>
|
||||
<div className="absolute md:hidden bottom-[20px] left-[-50px] w-[80px] h-[80px] rounded-full bg-[#FFD166] opacity-60"></div>
|
||||
|
||||
@@ -38,38 +38,38 @@ const FooterBar = () => {
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-8 w-full">
|
||||
<div className="text-left">
|
||||
<p className="!text-[#d9dbe1] font-semibold mb-5">{t('关于我们')}</p>
|
||||
<p className="!text-semi-color-text-0 font-semibold mb-5">{t('关于我们')}</p>
|
||||
<div className="flex flex-col gap-4">
|
||||
<a href="https://docs.newapi.pro/wiki/project-introduction/" target="_blank" rel="noopener noreferrer" className="!text-[#d9dbe1]">{t('关于项目')}</a>
|
||||
<a href="https://docs.newapi.pro/support/community-interaction/" target="_blank" rel="noopener noreferrer" className="!text-[#d9dbe1]">{t('联系我们')}</a>
|
||||
<a href="https://docs.newapi.pro/wiki/features-introduction/" target="_blank" rel="noopener noreferrer" className="!text-[#d9dbe1]">{t('功能特性')}</a>
|
||||
<a href="https://docs.newapi.pro/wiki/project-introduction/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1 hover:!text-semi-color-primary transition-colors">{t('关于项目')}</a>
|
||||
<a href="https://docs.newapi.pro/support/community-interaction/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1 hover:!text-semi-color-primary transition-colors">{t('联系我们')}</a>
|
||||
<a href="https://docs.newapi.pro/wiki/features-introduction/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1 hover:!text-semi-color-primary transition-colors">{t('功能特性')}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-left">
|
||||
<p className="!text-[#d9dbe1] font-semibold mb-5">{t('文档')}</p>
|
||||
<p className="!text-semi-color-text-0 font-semibold mb-5">{t('文档')}</p>
|
||||
<div className="flex flex-col gap-4">
|
||||
<a href="https://docs.newapi.pro/getting-started/" target="_blank" rel="noopener noreferrer" className="!text-[#d9dbe1]">{t('快速开始')}</a>
|
||||
<a href="https://docs.newapi.pro/installation/" target="_blank" rel="noopener noreferrer" className="!text-[#d9dbe1]">{t('安装指南')}</a>
|
||||
<a href="https://docs.newapi.pro/api/" target="_blank" rel="noopener noreferrer" className="!text-[#d9dbe1]">{t('API 文档')}</a>
|
||||
<a href="https://docs.newapi.pro/getting-started/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1 hover:!text-semi-color-primary transition-colors">{t('快速开始')}</a>
|
||||
<a href="https://docs.newapi.pro/installation/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1 hover:!text-semi-color-primary transition-colors">{t('安装指南')}</a>
|
||||
<a href="https://docs.newapi.pro/api/" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1 hover:!text-semi-color-primary transition-colors">{t('API 文档')}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-left">
|
||||
<p className="!text-[#d9dbe1] font-semibold mb-5">{t('相关项目')}</p>
|
||||
<p className="!text-semi-color-text-0 font-semibold mb-5">{t('相关项目')}</p>
|
||||
<div className="flex flex-col gap-4">
|
||||
<a href="https://github.com/songquanpeng/one-api" target="_blank" rel="noopener noreferrer" className="!text-[#d9dbe1]">One API</a>
|
||||
<a href="https://github.com/novicezk/midjourney-proxy" target="_blank" rel="noopener noreferrer" className="!text-[#d9dbe1]">Midjourney-Proxy</a>
|
||||
<a href="https://github.com/Deeptrain-Community/chatnio" target="_blank" rel="noopener noreferrer" className="!text-[#d9dbe1]">chatnio</a>
|
||||
<a href="https://github.com/Calcium-Ion/neko-api-key-tool" target="_blank" rel="noopener noreferrer" className="!text-[#d9dbe1]">neko-api-key-tool</a>
|
||||
<a href="https://github.com/songquanpeng/one-api" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1 hover:!text-semi-color-primary transition-colors">One API</a>
|
||||
<a href="https://github.com/novicezk/midjourney-proxy" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1 hover:!text-semi-color-primary transition-colors">Midjourney-Proxy</a>
|
||||
<a href="https://github.com/Deeptrain-Community/chatnio" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1 hover:!text-semi-color-primary transition-colors">chatnio</a>
|
||||
<a href="https://github.com/Calcium-Ion/neko-api-key-tool" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1 hover:!text-semi-color-primary transition-colors">neko-api-key-tool</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-left">
|
||||
<p className="!text-[#d9dbe1] font-semibold mb-5">{t('基于New API的项目')}</p>
|
||||
<p className="!text-semi-color-text-0 font-semibold mb-5">{t('基于New API的项目')}</p>
|
||||
<div className="flex flex-col gap-4">
|
||||
<a href="https://github.com/Calcium-Ion/new-api-horizon" target="_blank" rel="noopener noreferrer" className="!text-[#d9dbe1]">new-api-horizon</a>
|
||||
{/* <a href="https://github.com/VoAPI/VoAPI" target="_blank" rel="noopener noreferrer" className="!text-[#d9dbe1]">VoAPI</a> */}
|
||||
<a href="https://github.com/Calcium-Ion/new-api-horizon" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1 hover:!text-semi-color-primary transition-colors">new-api-horizon</a>
|
||||
{/* <a href="https://github.com/VoAPI/VoAPI" target="_blank" rel="noopener noreferrer" className="!text-semi-color-text-1 hover:!text-semi-color-primary transition-colors">VoAPI</a> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -78,15 +78,15 @@ const FooterBar = () => {
|
||||
|
||||
<div className="flex flex-col md:flex-row items-center justify-between w-full max-w-[1110px] gap-6">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Typography.Text className="text-sm !text-[#d9dbe1]">© {currentYear} {systemName}. {t('版权所有')}</Typography.Text>
|
||||
<Typography.Text className="text-sm !text-semi-color-text-1">© {currentYear} {systemName}. {t('版权所有')}</Typography.Text>
|
||||
</div>
|
||||
|
||||
{isDemoSiteMode && (
|
||||
<div className="text-sm">
|
||||
<span className="!text-[#d9dbe1]">{t('设计与开发由')} </span>
|
||||
<span className="!text-[#01ffc3]">Douyin FE</span>
|
||||
<span className="!text-[#d9dbe1]"> & </span>
|
||||
<a href="https://github.com/QuantumNous" target="_blank" rel="noreferrer" className="!text-[#01ffc3] hover:!text-[#01ffc3]">QuantumNous</a>
|
||||
<span className="!text-semi-color-text-1">{t('设计与开发由')} </span>
|
||||
<span className="!text-semi-color-primary">Douyin FE</span>
|
||||
<span className="!text-semi-color-text-1"> & </span>
|
||||
<a href="https://github.com/QuantumNous" target="_blank" rel="noreferrer" className="!text-semi-color-primary hover:!text-semi-color-primary-hover transition-colors">QuantumNous</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { UserContext } from '../context/User';
|
||||
import { useSetTheme, useTheme } from '../context/Theme';
|
||||
import { UserContext } from '../../context/User/index.js';
|
||||
import { useSetTheme, useTheme } from '../../context/Theme/index.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { API, getLogo, getSystemName, showSuccess } from '../helpers';
|
||||
import { API, getLogo, getSystemName, showSuccess, stringToColor } from '../../helpers/index.js';
|
||||
import fireworks from 'react-fireworks';
|
||||
import { CN, GB } from 'country-flag-icons/react/3x2';
|
||||
import NoticeModal from './NoticeModal';
|
||||
import NoticeModal from './NoticeModal.js';
|
||||
|
||||
import {
|
||||
IconClose,
|
||||
@@ -29,9 +29,8 @@ import {
|
||||
Typography,
|
||||
Skeleton,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { stringToColor } from '../helpers/render';
|
||||
import { StatusContext } from '../context/Status/index.js';
|
||||
import { useStyle, styleActions } from '../context/Style/index.js';
|
||||
import { StatusContext } from '../../context/Status/index.js';
|
||||
import { useStyle, styleActions } from '../../context/Style/index.js';
|
||||
|
||||
const HeaderBar = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
@@ -469,7 +468,7 @@ const HeaderBar = () => {
|
||||
onClick={() => setNoticeVisible(true)}
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full bg-semi-color-fill-0 dark:bg-semi-color-fill-1 hover:bg-semi-color-fill-1 dark:hover:bg-semi-color-fill-2"
|
||||
className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2"
|
||||
/>
|
||||
|
||||
<Button
|
||||
@@ -478,7 +477,7 @@ const HeaderBar = () => {
|
||||
onClick={() => setTheme(theme === 'dark' ? false : true)}
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full bg-semi-color-fill-0 dark:bg-semi-color-fill-1 hover:bg-semi-color-fill-1 dark:hover:bg-semi-color-fill-2"
|
||||
className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2"
|
||||
/>
|
||||
|
||||
<Dropdown
|
||||
@@ -507,7 +506,7 @@ const HeaderBar = () => {
|
||||
aria-label={t('切换语言')}
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full bg-semi-color-fill-0 dark:bg-semi-color-fill-1 hover:bg-semi-color-fill-1 dark:hover:bg-semi-color-fill-2"
|
||||
className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2"
|
||||
/>
|
||||
</Dropdown>
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Modal, Empty } from '@douyinfe/semi-ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { API, showError } from '../../helpers';
|
||||
import { marked } from 'marked';
|
||||
import { IllustrationNoContent, IllustrationNoContentDark } from '@douyinfe/semi-illustrations';
|
||||
|
||||
const NoticeModal = ({ visible, onClose, isMobile }) => {
|
||||
const { t } = useTranslation();
|
||||
const [noticeContent, setNoticeContent] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleCloseTodayNotice = () => {
|
||||
const today = new Date().toDateString();
|
||||
localStorage.setItem('notice_close_date', today);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const displayNotice = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await API.get('/api/notice');
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
if (data !== '') {
|
||||
const htmlNotice = marked.parse(data);
|
||||
setNoticeContent(htmlNotice);
|
||||
} else {
|
||||
setNoticeContent('');
|
||||
}
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
displayNotice();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const renderContent = () => {
|
||||
if (loading) {
|
||||
return <div className="py-12"><Empty description={t('加载中...')} /></div>;
|
||||
}
|
||||
|
||||
if (!noticeContent) {
|
||||
return (
|
||||
<div className="py-12">
|
||||
<Empty
|
||||
image={<IllustrationNoContent style={{ width: 150, height: 150 }} />}
|
||||
darkModeImage={<IllustrationNoContentDark style={{ width: 150, height: 150 }} />}
|
||||
description={t('暂无公告')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: noticeContent }}
|
||||
className="max-h-[60vh] overflow-y-auto pr-2"
|
||||
style={{
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: 'var(--semi-color-tertiary) transparent'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('系统公告')}
|
||||
visible={visible}
|
||||
onCancel={onClose}
|
||||
footer={(
|
||||
<div className="flex justify-end">
|
||||
<Button type='secondary' className='!rounded-full' onClick={handleCloseTodayNotice}>{t('今日关闭')}</Button>
|
||||
<Button type="primary" className='!rounded-full' onClick={onClose}>{t('关闭公告')}</Button>
|
||||
</div>
|
||||
)}
|
||||
size={isMobile ? 'full-width' : 'large'}
|
||||
>
|
||||
{renderContent()}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoticeModal;
|
||||
@@ -1,16 +1,15 @@
|
||||
import HeaderBar from './HeaderBar.js';
|
||||
import { Layout } from '@douyinfe/semi-ui';
|
||||
import SiderBar from './SiderBar.js';
|
||||
import App from '../App.js';
|
||||
import App from '../../App.js';
|
||||
import FooterBar from './Footer.js';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { useStyle } from '../context/Style/index.js';
|
||||
import { useStyle } from '../../context/Style/index.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { API, getLogo, getSystemName, showError } from '../helpers/index.js';
|
||||
import { setStatusData } from '../helpers/data.js';
|
||||
import { UserContext } from '../context/User/index.js';
|
||||
import { StatusContext } from '../context/Status/index.js';
|
||||
import { API, getLogo, getSystemName, showError, setStatusData } from '../../helpers/index.js';
|
||||
import { UserContext } from '../../context/User/index.js';
|
||||
import { StatusContext } from '../../context/Status/index.js';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
const { Sider, Content, Header, Footer } = Layout;
|
||||
|
||||
@@ -89,7 +88,6 @@ const PageLayout = () => {
|
||||
width: '100%',
|
||||
top: 0,
|
||||
zIndex: 100,
|
||||
borderBottom: '1px solid var(--semi-color-border)',
|
||||
}}
|
||||
>
|
||||
<HeaderBar />
|
||||
@@ -125,7 +123,7 @@ const PageLayout = () => {
|
||||
: styleState.showSider
|
||||
? styleState.siderCollapsed
|
||||
? '60px'
|
||||
: '200px'
|
||||
: '180px'
|
||||
: '0',
|
||||
transition: 'margin-left 0.3s ease',
|
||||
flex: '1 1 auto',
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
import { StatusContext } from '../context/Status';
|
||||
import { StatusContext } from '../../context/Status';
|
||||
|
||||
const SetupCheck = ({ children }) => {
|
||||
const [statusState] = useContext(StatusContext);
|
||||
@@ -0,0 +1,448 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getLucideIcon, sidebarIconColors } from '../../helpers/render.js';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
import { useStyle, styleActions } from '../../context/Style/index.js';
|
||||
import {
|
||||
isAdmin,
|
||||
isRoot,
|
||||
showError
|
||||
} from '../../helpers/index.js';
|
||||
|
||||
import {
|
||||
Nav,
|
||||
Divider,
|
||||
Tooltip,
|
||||
} from '@douyinfe/semi-ui';
|
||||
|
||||
const routerMap = {
|
||||
home: '/',
|
||||
channel: '/console/channel',
|
||||
token: '/console/token',
|
||||
redemption: '/console/redemption',
|
||||
topup: '/console/topup',
|
||||
user: '/console/user',
|
||||
log: '/console/log',
|
||||
midjourney: '/console/midjourney',
|
||||
setting: '/console/setting',
|
||||
about: '/about',
|
||||
detail: '/console',
|
||||
pricing: '/pricing',
|
||||
task: '/console/task',
|
||||
playground: '/console/playground',
|
||||
personal: '/console/personal',
|
||||
};
|
||||
|
||||
const SiderBar = () => {
|
||||
const { t } = useTranslation();
|
||||
const { state: styleState, dispatch: styleDispatch } = useStyle();
|
||||
|
||||
const [selectedKeys, setSelectedKeys] = useState(['home']);
|
||||
const [isCollapsed, setIsCollapsed] = useState(styleState.siderCollapsed);
|
||||
const [chatItems, setChatItems] = useState([]);
|
||||
const [openedKeys, setOpenedKeys] = useState([]);
|
||||
const location = useLocation();
|
||||
const [routerMapState, setRouterMapState] = useState(routerMap);
|
||||
|
||||
const workspaceItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
text: t('数据看板'),
|
||||
itemKey: 'detail',
|
||||
to: '/detail',
|
||||
className:
|
||||
localStorage.getItem('enable_data_export') === 'true'
|
||||
? ''
|
||||
: 'tableHiddle',
|
||||
},
|
||||
{
|
||||
text: t('API令牌'),
|
||||
itemKey: 'token',
|
||||
to: '/token',
|
||||
},
|
||||
{
|
||||
text: t('使用日志'),
|
||||
itemKey: 'log',
|
||||
to: '/log',
|
||||
},
|
||||
{
|
||||
text: t('绘图日志'),
|
||||
itemKey: 'midjourney',
|
||||
to: '/midjourney',
|
||||
className:
|
||||
localStorage.getItem('enable_drawing') === 'true'
|
||||
? ''
|
||||
: 'tableHiddle',
|
||||
},
|
||||
{
|
||||
text: t('任务日志'),
|
||||
itemKey: 'task',
|
||||
to: '/task',
|
||||
className:
|
||||
localStorage.getItem('enable_task') === 'true' ? '' : 'tableHiddle',
|
||||
},
|
||||
],
|
||||
[
|
||||
localStorage.getItem('enable_data_export'),
|
||||
localStorage.getItem('enable_drawing'),
|
||||
localStorage.getItem('enable_task'),
|
||||
t,
|
||||
],
|
||||
);
|
||||
|
||||
const financeItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
text: t('钱包'),
|
||||
itemKey: 'topup',
|
||||
to: '/topup',
|
||||
},
|
||||
{
|
||||
text: t('个人设置'),
|
||||
itemKey: 'personal',
|
||||
to: '/personal',
|
||||
},
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
const adminItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
text: t('渠道'),
|
||||
itemKey: 'channel',
|
||||
to: '/channel',
|
||||
className: isAdmin() ? '' : 'tableHiddle',
|
||||
},
|
||||
{
|
||||
text: t('兑换码'),
|
||||
itemKey: 'redemption',
|
||||
to: '/redemption',
|
||||
className: isAdmin() ? '' : 'tableHiddle',
|
||||
},
|
||||
{
|
||||
text: t('用户管理'),
|
||||
itemKey: 'user',
|
||||
to: '/user',
|
||||
className: isAdmin() ? '' : 'tableHiddle',
|
||||
},
|
||||
{
|
||||
text: t('系统设置'),
|
||||
itemKey: 'setting',
|
||||
to: '/setting',
|
||||
className: isRoot() ? '' : 'tableHiddle',
|
||||
},
|
||||
],
|
||||
[isAdmin(), isRoot(), t],
|
||||
);
|
||||
|
||||
const chatMenuItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
text: t('操练场'),
|
||||
itemKey: 'playground',
|
||||
to: '/playground',
|
||||
},
|
||||
{
|
||||
text: t('聊天'),
|
||||
itemKey: 'chat',
|
||||
items: chatItems,
|
||||
},
|
||||
],
|
||||
[chatItems, t],
|
||||
);
|
||||
|
||||
// 更新路由映射,添加聊天路由
|
||||
const updateRouterMapWithChats = (chats) => {
|
||||
const newRouterMap = { ...routerMap };
|
||||
|
||||
if (Array.isArray(chats) && chats.length > 0) {
|
||||
for (let i = 0; i < chats.length; i++) {
|
||||
newRouterMap['chat' + i] = '/console/chat/' + i;
|
||||
}
|
||||
}
|
||||
|
||||
setRouterMapState(newRouterMap);
|
||||
return newRouterMap;
|
||||
};
|
||||
|
||||
// 加载聊天项
|
||||
useEffect(() => {
|
||||
let chats = localStorage.getItem('chats');
|
||||
if (chats) {
|
||||
try {
|
||||
chats = JSON.parse(chats);
|
||||
if (Array.isArray(chats)) {
|
||||
let chatItems = [];
|
||||
for (let i = 0; i < chats.length; i++) {
|
||||
let chat = {};
|
||||
for (let key in chats[i]) {
|
||||
chat.text = key;
|
||||
chat.itemKey = 'chat' + i;
|
||||
chat.to = '/console/chat/' + i;
|
||||
}
|
||||
chatItems.push(chat);
|
||||
}
|
||||
setChatItems(chatItems);
|
||||
updateRouterMapWithChats(chats);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showError('聊天数据解析失败');
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 根据当前路径设置选中的菜单项
|
||||
useEffect(() => {
|
||||
const currentPath = location.pathname;
|
||||
let matchingKey = Object.keys(routerMapState).find(
|
||||
(key) => routerMapState[key] === currentPath,
|
||||
);
|
||||
|
||||
// 处理聊天路由
|
||||
if (!matchingKey && currentPath.startsWith('/console/chat/')) {
|
||||
const chatIndex = currentPath.split('/').pop();
|
||||
if (!isNaN(chatIndex)) {
|
||||
matchingKey = 'chat' + chatIndex;
|
||||
} else {
|
||||
matchingKey = 'chat';
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找到匹配的键,更新选中的键
|
||||
if (matchingKey) {
|
||||
setSelectedKeys([matchingKey]);
|
||||
}
|
||||
}, [location.pathname, routerMapState]);
|
||||
|
||||
// 同步折叠状态
|
||||
useEffect(() => {
|
||||
setIsCollapsed(styleState.siderCollapsed);
|
||||
}, [styleState.siderCollapsed]);
|
||||
|
||||
// 获取菜单项对应的颜色
|
||||
const getItemColor = (itemKey) => {
|
||||
switch (itemKey) {
|
||||
case 'detail': return sidebarIconColors.dashboard;
|
||||
case 'playground': return sidebarIconColors.terminal;
|
||||
case 'chat': return sidebarIconColors.message;
|
||||
case 'token': return sidebarIconColors.key;
|
||||
case 'log': return sidebarIconColors.chart;
|
||||
case 'midjourney': return sidebarIconColors.image;
|
||||
case 'task': return sidebarIconColors.check;
|
||||
case 'topup': return sidebarIconColors.credit;
|
||||
case 'channel': return sidebarIconColors.layers;
|
||||
case 'redemption': return sidebarIconColors.gift;
|
||||
case 'user':
|
||||
case 'personal': return sidebarIconColors.user;
|
||||
case 'setting': return sidebarIconColors.settings;
|
||||
default:
|
||||
// 处理聊天项
|
||||
if (itemKey && itemKey.startsWith('chat')) return sidebarIconColors.message;
|
||||
return 'currentColor';
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染自定义菜单项
|
||||
const renderNavItem = (item) => {
|
||||
// 跳过隐藏的项目
|
||||
if (item.className === 'tableHiddle') return null;
|
||||
|
||||
const isSelected = selectedKeys.includes(item.itemKey);
|
||||
const textColor = isSelected ? getItemColor(item.itemKey) : 'inherit';
|
||||
|
||||
return (
|
||||
<Nav.Item
|
||||
key={item.itemKey}
|
||||
itemKey={item.itemKey}
|
||||
text={
|
||||
<div className="flex items-center">
|
||||
<span className="truncate font-medium text-sm" style={{ color: textColor }}>
|
||||
{item.text}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
icon={
|
||||
<div className="sidebar-icon-container flex-shrink-0">
|
||||
{getLucideIcon(item.itemKey, isSelected)}
|
||||
</div>
|
||||
}
|
||||
className={item.className}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染子菜单项
|
||||
const renderSubItem = (item) => {
|
||||
if (item.items && item.items.length > 0) {
|
||||
const isSelected = selectedKeys.includes(item.itemKey);
|
||||
const textColor = isSelected ? getItemColor(item.itemKey) : 'inherit';
|
||||
|
||||
return (
|
||||
<Nav.Sub
|
||||
key={item.itemKey}
|
||||
itemKey={item.itemKey}
|
||||
text={
|
||||
<div className="flex items-center">
|
||||
<span className="truncate font-medium text-sm" style={{ color: textColor }}>
|
||||
{item.text}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
icon={
|
||||
<div className="sidebar-icon-container flex-shrink-0">
|
||||
{getLucideIcon(item.itemKey, isSelected)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{item.items.map((subItem) => {
|
||||
const isSubSelected = selectedKeys.includes(subItem.itemKey);
|
||||
const subTextColor = isSubSelected ? getItemColor(subItem.itemKey) : 'inherit';
|
||||
|
||||
return (
|
||||
<Nav.Item
|
||||
key={subItem.itemKey}
|
||||
itemKey={subItem.itemKey}
|
||||
text={
|
||||
<span className="truncate font-medium text-sm" style={{ color: subTextColor }}>
|
||||
{subItem.text}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Nav.Sub>
|
||||
);
|
||||
} else {
|
||||
return renderNavItem(item);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="sidebar-container"
|
||||
style={{ width: isCollapsed ? '60px' : '180px' }}
|
||||
>
|
||||
<Nav
|
||||
className="sidebar-nav custom-sidebar-nav"
|
||||
defaultIsCollapsed={styleState.siderCollapsed}
|
||||
isCollapsed={isCollapsed}
|
||||
onCollapseChange={(collapsed) => {
|
||||
setIsCollapsed(collapsed);
|
||||
styleDispatch(styleActions.setSiderCollapsed(collapsed));
|
||||
|
||||
// 确保在收起侧边栏时有选中的项目
|
||||
if (selectedKeys.length === 0) {
|
||||
const currentPath = location.pathname;
|
||||
const matchingKey = Object.keys(routerMapState).find(
|
||||
(key) => routerMapState[key] === currentPath,
|
||||
);
|
||||
|
||||
if (matchingKey) {
|
||||
setSelectedKeys([matchingKey]);
|
||||
} else if (currentPath.startsWith('/console/chat/')) {
|
||||
setSelectedKeys(['chat']);
|
||||
} else {
|
||||
setSelectedKeys(['detail']); // 默认选中首页
|
||||
}
|
||||
}
|
||||
}}
|
||||
selectedKeys={selectedKeys}
|
||||
itemStyle="sidebar-nav-item"
|
||||
hoverStyle="sidebar-nav-item:hover"
|
||||
selectedStyle="sidebar-nav-item-selected"
|
||||
renderWrapper={({ itemElement, props }) => {
|
||||
const to = routerMapState[props.itemKey] || routerMap[props.itemKey];
|
||||
|
||||
// 如果没有路由,直接返回元素
|
||||
if (!to) return itemElement;
|
||||
|
||||
return (
|
||||
<Link
|
||||
style={{ textDecoration: 'none' }}
|
||||
to={to}
|
||||
>
|
||||
{itemElement}
|
||||
</Link>
|
||||
);
|
||||
}}
|
||||
onSelect={(key) => {
|
||||
// 如果点击的是已经展开的子菜单的父项,则收起子菜单
|
||||
if (openedKeys.includes(key.itemKey)) {
|
||||
setOpenedKeys(openedKeys.filter((k) => k !== key.itemKey));
|
||||
}
|
||||
|
||||
setSelectedKeys([key.itemKey]);
|
||||
}}
|
||||
openKeys={openedKeys}
|
||||
onOpenChange={(data) => {
|
||||
setOpenedKeys(data.openKeys);
|
||||
}}
|
||||
>
|
||||
{/* 聊天区域 */}
|
||||
<div className="sidebar-section">
|
||||
{!isCollapsed && (
|
||||
<div className="sidebar-group-label">{t('聊天')}</div>
|
||||
)}
|
||||
{chatMenuItems.map((item) => renderSubItem(item))}
|
||||
</div>
|
||||
|
||||
{/* 控制台区域 */}
|
||||
<Divider className="sidebar-divider" />
|
||||
<div>
|
||||
{!isCollapsed && (
|
||||
<div className="sidebar-group-label">{t('控制台')}</div>
|
||||
)}
|
||||
{workspaceItems.map((item) => renderNavItem(item))}
|
||||
</div>
|
||||
|
||||
{/* 管理员区域 - 只在管理员时显示 */}
|
||||
{isAdmin() && (
|
||||
<>
|
||||
<Divider className="sidebar-divider" />
|
||||
<div>
|
||||
{!isCollapsed && (
|
||||
<div className="sidebar-group-label">{t('管理员')}</div>
|
||||
)}
|
||||
{adminItems.map((item) => renderNavItem(item))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 个人中心区域 */}
|
||||
<Divider className="sidebar-divider" />
|
||||
<div>
|
||||
{!isCollapsed && (
|
||||
<div className="sidebar-group-label">{t('个人中心')}</div>
|
||||
)}
|
||||
{financeItems.map((item) => renderNavItem(item))}
|
||||
</div>
|
||||
</Nav>
|
||||
|
||||
{/* 底部折叠按钮 */}
|
||||
<div
|
||||
className="sidebar-collapse-button"
|
||||
onClick={() => {
|
||||
const newCollapsed = !isCollapsed;
|
||||
setIsCollapsed(newCollapsed);
|
||||
styleDispatch(styleActions.setSiderCollapsed(newCollapsed));
|
||||
}}
|
||||
>
|
||||
<Tooltip content={isCollapsed ? t('展开侧边栏') : t('收起侧边栏')} position="right">
|
||||
<div className="sidebar-collapse-button-inner">
|
||||
<span
|
||||
className="sidebar-collapse-icon-container"
|
||||
style={{ transform: isCollapsed ? 'rotate(180deg)' : 'rotate(0deg)' }}
|
||||
>
|
||||
<ChevronLeft size={16} strokeWidth={2.5} color="var(--semi-color-text-2)" />
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SiderBar;
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useMemo, useCallback } from 'react';
|
||||
import { Button, Tooltip, Toast } from '@douyinfe/semi-ui';
|
||||
import { Copy, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { copy } from '../../helpers/utils';
|
||||
import { copy } from '../../helpers';
|
||||
|
||||
const PERFORMANCE_CONFIG = {
|
||||
MAX_DISPLAY_LENGTH: 50000, // 最大显示字符数
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
Settings,
|
||||
} from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { renderGroupOption } from '../../helpers/render.js';
|
||||
import { renderGroupOption } from '../../helpers';
|
||||
import ParameterControl from './ParameterControl';
|
||||
import ImageUrlInput from './ImageUrlInput';
|
||||
import ConfigManager from './ConfigManager';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { STORAGE_KEYS, DEFAULT_CONFIG } from '../../utils/constants';
|
||||
import { STORAGE_KEYS, DEFAULT_CONFIG } from '../../constants/playground.constants';
|
||||
|
||||
const MESSAGES_STORAGE_KEY = 'playground_messages';
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, Spin, Tabs } from '@douyinfe/semi-ui';
|
||||
|
||||
import { API, showError, showSuccess } from '../helpers';
|
||||
import { API, showError, showSuccess } from '../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SettingGeminiModel from '../pages/Setting/Model/SettingGeminiModel.js';
|
||||
import SettingClaudeModel from '../pages/Setting/Model/SettingClaudeModel.js';
|
||||
import SettingGlobalModel from '../pages/Setting/Model/SettingGlobalModel.js';
|
||||
import SettingGeminiModel from '../../pages/Setting/Model/SettingGeminiModel.js';
|
||||
import SettingClaudeModel from '../../pages/Setting/Model/SettingClaudeModel.js';
|
||||
import SettingGlobalModel from '../../pages/Setting/Model/SettingGlobalModel.js';
|
||||
|
||||
const ModelSetting = () => {
|
||||
const { t } = useTranslation();
|
||||
+13
-13
@@ -1,20 +1,20 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, Spin, Tabs } from '@douyinfe/semi-ui';
|
||||
import SettingsGeneral from '../pages/Setting/Operation/SettingsGeneral.js';
|
||||
import SettingsDrawing from '../pages/Setting/Operation/SettingsDrawing.js';
|
||||
import SettingsSensitiveWords from '../pages/Setting/Operation/SettingsSensitiveWords.js';
|
||||
import SettingsLog from '../pages/Setting/Operation/SettingsLog.js';
|
||||
import SettingsDataDashboard from '../pages/Setting/Operation/SettingsDataDashboard.js';
|
||||
import SettingsMonitoring from '../pages/Setting/Operation/SettingsMonitoring.js';
|
||||
import SettingsCreditLimit from '../pages/Setting/Operation/SettingsCreditLimit.js';
|
||||
import ModelSettingsVisualEditor from '../pages/Setting/Operation/ModelSettingsVisualEditor.js';
|
||||
import GroupRatioSettings from '../pages/Setting/Operation/GroupRatioSettings.js';
|
||||
import ModelRatioSettings from '../pages/Setting/Operation/ModelRatioSettings.js';
|
||||
import SettingsGeneral from '../../pages/Setting/Operation/SettingsGeneral.js';
|
||||
import SettingsDrawing from '../../pages/Setting/Operation/SettingsDrawing.js';
|
||||
import SettingsSensitiveWords from '../../pages/Setting/Operation/SettingsSensitiveWords.js';
|
||||
import SettingsLog from '../../pages/Setting/Operation/SettingsLog.js';
|
||||
import SettingsDataDashboard from '../../pages/Setting/Operation/SettingsDataDashboard.js';
|
||||
import SettingsMonitoring from '../../pages/Setting/Operation/SettingsMonitoring.js';
|
||||
import SettingsCreditLimit from '../../pages/Setting/Operation/SettingsCreditLimit.js';
|
||||
import ModelSettingsVisualEditor from '../../pages/Setting/Operation/ModelSettingsVisualEditor.js';
|
||||
import GroupRatioSettings from '../../pages/Setting/Operation/GroupRatioSettings.js';
|
||||
import ModelRatioSettings from '../../pages/Setting/Operation/ModelRatioSettings.js';
|
||||
|
||||
import { API, showError, showSuccess } from '../helpers';
|
||||
import SettingsChats from '../pages/Setting/Operation/SettingsChats.js';
|
||||
import { API, showError, showSuccess } from '../../helpers';
|
||||
import SettingsChats from '../../pages/Setting/Operation/SettingsChats.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ModelRatioNotSetEditor from '../pages/Setting/Operation/ModelRationNotSetEditor.js';
|
||||
import ModelRatioNotSetEditor from '../../pages/Setting/Operation/ModelRationNotSetEditor.js';
|
||||
|
||||
const OperationSetting = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
Space,
|
||||
Card,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { API, showError, showSuccess, timestamp2string } from '../helpers';
|
||||
import { API, showError, showSuccess, timestamp2string } from '../../helpers';
|
||||
import { marked } from 'marked';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StatusContext } from '../context/Status/index.js';
|
||||
import { StatusContext } from '../../context/Status/index.js';
|
||||
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
|
||||
|
||||
const OtherSetting = () => {
|
||||
+198
-249
@@ -8,24 +8,28 @@ import {
|
||||
showError,
|
||||
showInfo,
|
||||
showSuccess,
|
||||
} from '../helpers';
|
||||
import Turnstile from 'react-turnstile';
|
||||
import { UserContext } from '../context/User';
|
||||
import {
|
||||
renderQuota,
|
||||
renderQuotaWithPrompt,
|
||||
stringToColor,
|
||||
onGitHubOAuthClicked,
|
||||
onOIDCClicked,
|
||||
onLinuxDOOAuthClicked,
|
||||
} from './utils';
|
||||
renderModelTag,
|
||||
getModelCategories
|
||||
} from '../../helpers';
|
||||
import Turnstile from 'react-turnstile';
|
||||
import { UserContext } from '../../context/User';
|
||||
import {
|
||||
Avatar,
|
||||
Banner,
|
||||
Button,
|
||||
Card,
|
||||
Empty,
|
||||
Image,
|
||||
Input,
|
||||
InputNumber,
|
||||
Layout,
|
||||
Modal,
|
||||
Skeleton,
|
||||
Space,
|
||||
Tag,
|
||||
Typography,
|
||||
@@ -37,6 +41,7 @@ import {
|
||||
Tabs,
|
||||
TabPane,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { IllustrationNoContent, IllustrationNoContentDark } from '@douyinfe/semi-illustrations';
|
||||
import {
|
||||
IconMail,
|
||||
IconLock,
|
||||
@@ -46,20 +51,12 @@ import {
|
||||
IconBell,
|
||||
IconGithubLogo,
|
||||
IconKey,
|
||||
IconCreditCard,
|
||||
IconLink,
|
||||
IconDelete,
|
||||
IconChevronDown,
|
||||
IconChevronUp,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { SiTelegram, SiWechat, SiLinux } from 'react-icons/si';
|
||||
import { Bell, Shield, Webhook, Globe, Settings, UserPlus, ShieldCheck } from 'lucide-react';
|
||||
import {
|
||||
getQuotaPerUnit,
|
||||
renderQuota,
|
||||
renderQuotaWithPrompt,
|
||||
stringToColor,
|
||||
} from '../helpers/render';
|
||||
import TelegramLoginButton from 'react-telegram-login';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -88,16 +85,14 @@ const PersonalSetting = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [disableButton, setDisableButton] = useState(false);
|
||||
const [countdown, setCountdown] = useState(30);
|
||||
const [affLink, setAffLink] = useState('');
|
||||
const [systemToken, setSystemToken] = useState('');
|
||||
const [models, setModels] = useState([]);
|
||||
const [openTransfer, setOpenTransfer] = useState(false);
|
||||
const [transferAmount, setTransferAmount] = useState(0);
|
||||
const [isModelsExpanded, setIsModelsExpanded] = useState(() => {
|
||||
// Initialize from localStorage if available
|
||||
const savedState = localStorage.getItem('modelsExpanded');
|
||||
return savedState ? JSON.parse(savedState) : false;
|
||||
});
|
||||
const [activeModelCategory, setActiveModelCategory] = useState('all');
|
||||
const MODELS_DISPLAY_COUNT = 25; // 默认显示的模型数量
|
||||
const [notificationSettings, setNotificationSettings] = useState({
|
||||
warningType: 'email',
|
||||
@@ -107,6 +102,7 @@ const PersonalSetting = () => {
|
||||
notificationEmail: '',
|
||||
acceptUnsetModelRatioModel: false,
|
||||
});
|
||||
const [modelsLoading, setModelsLoading] = useState(true);
|
||||
const [showWebhookDocs, setShowWebhookDocs] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -123,8 +119,6 @@ const PersonalSetting = () => {
|
||||
console.log(userState);
|
||||
});
|
||||
loadModels().then();
|
||||
getAffLink().then();
|
||||
setTransferAmount(getQuotaPerUnit());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -176,17 +170,6 @@ const PersonalSetting = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const getAffLink = async () => {
|
||||
const res = await API.get('/api/user/aff');
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
let link = `${window.location.origin}/register?aff=${data}`;
|
||||
setAffLink(link);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
const getUserData = async () => {
|
||||
let res = await API.get(`/api/user/self`);
|
||||
const { success, message, data } = res.data;
|
||||
@@ -198,21 +181,24 @@ const PersonalSetting = () => {
|
||||
};
|
||||
|
||||
const loadModels = async () => {
|
||||
let res = await API.get(`/api/user/models`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
if (data != null) {
|
||||
setModels(data);
|
||||
}
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
setModelsLoading(true);
|
||||
|
||||
const handleAffLinkClick = async (e) => {
|
||||
e.target.select();
|
||||
await copy(e.target.value);
|
||||
showSuccess(t('邀请链接已复制到剪切板'));
|
||||
try {
|
||||
let res = await API.get(`/api/user/models`);
|
||||
const { success, message, data } = res.data;
|
||||
|
||||
if (success) {
|
||||
if (data != null) {
|
||||
setModels(data);
|
||||
}
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('加载模型列表失败'));
|
||||
} finally {
|
||||
setModelsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSystemTokenClick = async (e) => {
|
||||
@@ -286,24 +272,6 @@ const PersonalSetting = () => {
|
||||
setShowChangePasswordModal(false);
|
||||
};
|
||||
|
||||
const transfer = async () => {
|
||||
if (transferAmount < getQuotaPerUnit()) {
|
||||
showError(t('划转金额最低为') + ' ' + renderQuota(getQuotaPerUnit()));
|
||||
return;
|
||||
}
|
||||
const res = await API.post(`/api/user/aff_transfer`, {
|
||||
quota: transferAmount,
|
||||
});
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess(message);
|
||||
setOpenTransfer(false);
|
||||
getUserData().then();
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
const sendVerificationCode = async () => {
|
||||
if (inputs.email === '') {
|
||||
showError(t('请输入邮箱!'));
|
||||
@@ -364,10 +332,6 @@ const PersonalSetting = () => {
|
||||
return 'NA';
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setOpenTransfer(false);
|
||||
};
|
||||
|
||||
const copyText = async (text) => {
|
||||
if (await copy(text)) {
|
||||
showSuccess(t('已复制:') + text);
|
||||
@@ -410,55 +374,12 @@ const PersonalSetting = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="bg-gray-50">
|
||||
<Layout>
|
||||
<Layout.Content>
|
||||
{/* 划转模态框 */}
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<IconCreditCard className="mr-2" />
|
||||
{t('请输入要划转的数量')}
|
||||
</div>
|
||||
}
|
||||
visible={openTransfer}
|
||||
onOk={transfer}
|
||||
onCancel={handleCancel}
|
||||
maskClosable={false}
|
||||
size={'small'}
|
||||
centered={true}
|
||||
>
|
||||
<div className="space-y-4 py-4">
|
||||
<div>
|
||||
<Typography.Text strong className="block mb-2">
|
||||
{t('可用额度')} {renderQuotaWithPrompt(userState?.user?.aff_quota)}
|
||||
</Typography.Text>
|
||||
<Input
|
||||
value={userState?.user?.aff_quota}
|
||||
disabled={true}
|
||||
size="large"
|
||||
className="!rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text strong className="block mb-2">
|
||||
{t('划转额度')} {renderQuotaWithPrompt(transferAmount)}{' '}
|
||||
{t('最低') + renderQuota(getQuotaPerUnit())}
|
||||
</Typography.Text>
|
||||
<InputNumber
|
||||
min={0}
|
||||
value={transferAmount}
|
||||
onChange={(value) => setTransferAmount(value)}
|
||||
disabled={false}
|
||||
size="large"
|
||||
className="!rounded-lg w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<div className="w-full max-w-6xl">
|
||||
<div className="w-full">
|
||||
{/* 主卡片容器 */}
|
||||
<Card className="!rounded-2xl shadow-lg border-0">
|
||||
{/* 顶部用户信息区域 */}
|
||||
@@ -604,17 +525,17 @@ const PersonalSetting = () => {
|
||||
{/* 主内容区域 - 使用Tabs组织不同功能模块 */}
|
||||
<div className="p-4">
|
||||
<Tabs type='line' defaultActiveKey='models' className="modern-tabs">
|
||||
{/* 模型与邀请Tab */}
|
||||
{/* 可用模型Tab */}
|
||||
<TabPane
|
||||
tab={
|
||||
<div className="flex items-center">
|
||||
<Settings size={16} className="mr-2" />
|
||||
{t('模型与邀请')}
|
||||
{t('可用模型')}
|
||||
</div>
|
||||
}
|
||||
itemKey='models'
|
||||
>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 py-4">
|
||||
<div className="gap-6 py-4">
|
||||
{/* 可用模型部分 */}
|
||||
<div className="bg-gray-50 rounded-xl">
|
||||
<div className="flex items-center mb-4">
|
||||
@@ -622,148 +543,176 @@ const PersonalSetting = () => {
|
||||
<Settings size={20} className="text-purple-500" />
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Title heading={6} className="mb-0">{t('可用模型')}</Typography.Title>
|
||||
<Typography.Title heading={6} className="mb-0">{t('模型列表')}</Typography.Title>
|
||||
<div className="text-gray-500 text-sm">{t('点击模型名称可复制')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg p-3">
|
||||
{models.length <= MODELS_DISPLAY_COUNT ? (
|
||||
<Space wrap>
|
||||
{models.map((model) => (
|
||||
<Tag
|
||||
key={model}
|
||||
color={stringToColor(model)}
|
||||
onClick={() => copyText(model)}
|
||||
className="cursor-pointer hover:opacity-80 transition-opacity !rounded-lg"
|
||||
>
|
||||
{model}
|
||||
</Tag>
|
||||
{modelsLoading ? (
|
||||
// 骨架屏加载状态 - 模拟实际加载后的布局
|
||||
<div className="space-y-4">
|
||||
{/* 模拟分类标签 */}
|
||||
<div className="mb-4" style={{ borderBottom: '1px solid var(--semi-color-border)' }}>
|
||||
<div className="flex overflow-x-auto py-2 gap-2">
|
||||
{Array.from({ length: 8 }).map((_, index) => (
|
||||
<Skeleton.Button key={`cat-${index}`} style={{
|
||||
width: index === 0 ? 130 : 100 + Math.random() * 50,
|
||||
height: 36,
|
||||
borderRadius: 8
|
||||
}} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 模拟模型标签列表 */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Array.from({ length: 20 }).map((_, index) => (
|
||||
<Skeleton.Button
|
||||
key={`model-${index}`}
|
||||
style={{
|
||||
width: 100 + Math.random() * 100,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
margin: '4px'
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Space>
|
||||
) : (
|
||||
<>
|
||||
<Collapsible isOpen={isModelsExpanded}>
|
||||
<Space wrap>
|
||||
{models.map((model) => (
|
||||
<Tag
|
||||
key={model}
|
||||
color={stringToColor(model)}
|
||||
onClick={() => copyText(model)}
|
||||
className="cursor-pointer hover:opacity-80 transition-opacity !rounded-lg"
|
||||
>
|
||||
{model}
|
||||
</Tag>
|
||||
))}
|
||||
<Tag
|
||||
color='grey'
|
||||
type='light'
|
||||
className="cursor-pointer !rounded-lg"
|
||||
onClick={() => setIsModelsExpanded(false)}
|
||||
icon={<IconChevronUp />}
|
||||
>
|
||||
{t('收起')}
|
||||
</Tag>
|
||||
</Space>
|
||||
</Collapsible>
|
||||
{!isModelsExpanded && (
|
||||
<Space wrap>
|
||||
{models
|
||||
.slice(0, MODELS_DISPLAY_COUNT)
|
||||
.map((model) => (
|
||||
<Tag
|
||||
key={model}
|
||||
color={stringToColor(model)}
|
||||
onClick={() => copyText(model)}
|
||||
className="cursor-pointer hover:opacity-80 transition-opacity !rounded-lg"
|
||||
>
|
||||
{model}
|
||||
</Tag>
|
||||
))}
|
||||
<Tag
|
||||
color='grey'
|
||||
type='light'
|
||||
className="cursor-pointer !rounded-lg"
|
||||
onClick={() => setIsModelsExpanded(true)}
|
||||
icon={<IconChevronDown />}
|
||||
>
|
||||
{t('更多')} {models.length - MODELS_DISPLAY_COUNT} {t('个模型')}
|
||||
</Tag>
|
||||
</Space>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 邀请信息部分 */}
|
||||
<div className="bg-gray-50 rounded-xl">
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="w-10 h-10 rounded-full bg-orange-50 flex items-center justify-center mr-3">
|
||||
<IconLink size={20} className="text-orange-500" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Title heading={6} className="mb-0">{t('邀请信息')}</Typography.Title>
|
||||
<div className="text-gray-500 text-sm">{t('管理您的邀请链接和收益')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<Card
|
||||
className="!rounded-2xl text-center"
|
||||
bodyStyle={{ padding: '16px' }}
|
||||
shadows='hover'
|
||||
>
|
||||
<div className="text-gray-600 text-xs font-medium">{t('待使用收益')}</div>
|
||||
<div className="text-gray-900 text-lg font-bold mt-1">
|
||||
{renderQuota(userState?.user?.aff_quota)}
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
theme="solid"
|
||||
onClick={() => setOpenTransfer(true)}
|
||||
size="small"
|
||||
className="!rounded-lg !bg-blue-500 hover:!bg-blue-600 mt-2 w-full"
|
||||
icon={<IconCreditCard />}
|
||||
>
|
||||
{t('划转')}
|
||||
</Button>
|
||||
</Card>
|
||||
<Card
|
||||
className="!rounded-2xl text-center"
|
||||
bodyStyle={{ padding: '16px' }}
|
||||
shadows='hover'
|
||||
>
|
||||
<div className="text-gray-600 text-xs font-medium">{t('总收益')}</div>
|
||||
<div className="text-gray-900 text-lg font-bold mt-1">
|
||||
{renderQuota(userState?.user?.aff_history_quota)}
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
className="!rounded-2xl text-center"
|
||||
bodyStyle={{ padding: '16px' }}
|
||||
shadows='hover'
|
||||
>
|
||||
<div className="text-gray-600 text-xs font-medium">{t('邀请人数')}</div>
|
||||
<div className="text-gray-900 text-lg font-bold mt-1">
|
||||
{userState?.user?.aff_count || 0}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg p-3">
|
||||
<Typography.Text strong className="block mb-2 text-sm">{t('邀请链接')}</Typography.Text>
|
||||
<Input
|
||||
value={affLink}
|
||||
onClick={handleAffLinkClick}
|
||||
readOnly
|
||||
size="large"
|
||||
className="!rounded-lg"
|
||||
prefix={<IconLink />}
|
||||
) : models.length === 0 ? (
|
||||
<div className="py-8">
|
||||
<Empty
|
||||
image={<IllustrationNoContent style={{ width: 150, height: 150 }} />}
|
||||
darkModeImage={<IllustrationNoContentDark style={{ width: 150, height: 150 }} />}
|
||||
description={t('没有可用模型')}
|
||||
style={{ padding: '24px 0' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 模型分类标签页 */}
|
||||
<div className="mb-4">
|
||||
<Tabs
|
||||
type="card"
|
||||
activeKey={activeModelCategory}
|
||||
onChange={key => setActiveModelCategory(key)}
|
||||
className="mt-2"
|
||||
>
|
||||
{Object.entries(getModelCategories(t)).map(([key, category]) => {
|
||||
// 计算该分类下的模型数量
|
||||
const modelCount = key === 'all'
|
||||
? models.length
|
||||
: models.filter(model => category.filter({ model_name: model })).length;
|
||||
|
||||
if (modelCount === 0 && key !== 'all') return null;
|
||||
|
||||
return (
|
||||
<TabPane
|
||||
tab={
|
||||
<span className="flex items-center gap-2">
|
||||
{category.icon && <span className="w-4 h-4">{category.icon}</span>}
|
||||
{category.label}
|
||||
<Tag
|
||||
color={activeModelCategory === key ? 'red' : 'grey'}
|
||||
size='small'
|
||||
shape='circle'
|
||||
>
|
||||
{modelCount}
|
||||
</Tag>
|
||||
</span>
|
||||
}
|
||||
itemKey={key}
|
||||
key={key}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg p-3">
|
||||
{(() => {
|
||||
// 根据当前选中的分类过滤模型
|
||||
const categories = getModelCategories(t);
|
||||
const filteredModels = activeModelCategory === 'all'
|
||||
? models
|
||||
: models.filter(model => categories[activeModelCategory].filter({ model_name: model }));
|
||||
|
||||
// 如果过滤后没有模型,显示空状态
|
||||
if (filteredModels.length === 0) {
|
||||
return (
|
||||
<Empty
|
||||
image={<IllustrationNoContent style={{ width: 120, height: 120 }} />}
|
||||
darkModeImage={<IllustrationNoContentDark style={{ width: 120, height: 120 }} />}
|
||||
description={t('该分类下没有可用模型')}
|
||||
style={{ padding: '16px 0' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (filteredModels.length <= MODELS_DISPLAY_COUNT) {
|
||||
return (
|
||||
<Space wrap>
|
||||
{filteredModels.map((model) => (
|
||||
renderModelTag(model, {
|
||||
size: 'large',
|
||||
shape: 'circle',
|
||||
onClick: () => copyText(model),
|
||||
})
|
||||
))}
|
||||
</Space>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<Collapsible isOpen={isModelsExpanded}>
|
||||
<Space wrap>
|
||||
{filteredModels.map((model) => (
|
||||
renderModelTag(model, {
|
||||
size: 'large',
|
||||
shape: 'circle',
|
||||
onClick: () => copyText(model),
|
||||
})
|
||||
))}
|
||||
<Tag
|
||||
color='grey'
|
||||
type='light'
|
||||
className="cursor-pointer !rounded-lg"
|
||||
onClick={() => setIsModelsExpanded(false)}
|
||||
icon={<IconChevronUp />}
|
||||
>
|
||||
{t('收起')}
|
||||
</Tag>
|
||||
</Space>
|
||||
</Collapsible>
|
||||
{!isModelsExpanded && (
|
||||
<Space wrap>
|
||||
{filteredModels
|
||||
.slice(0, MODELS_DISPLAY_COUNT)
|
||||
.map((model) => (
|
||||
renderModelTag(model, {
|
||||
size: 'large',
|
||||
shape: 'circle',
|
||||
onClick: () => copyText(model),
|
||||
})
|
||||
))}
|
||||
<Tag
|
||||
color='grey'
|
||||
type='light'
|
||||
className="cursor-pointer !rounded-lg"
|
||||
onClick={() => setIsModelsExpanded(true)}
|
||||
icon={<IconChevronDown />}
|
||||
>
|
||||
{t('更多')} {filteredModels.length - MODELS_DISPLAY_COUNT} {t('个模型')}
|
||||
</Tag>
|
||||
</Space>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabPane>
|
||||
@@ -809,7 +758,7 @@ const PersonalSetting = () => {
|
||||
>
|
||||
{userState.user && userState.user.email !== ''
|
||||
? t('修改绑定')
|
||||
: t('绑定邮箱')}
|
||||
: t('绑定')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
+3
-3
@@ -1,10 +1,10 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, Spin, Tabs } from '@douyinfe/semi-ui';
|
||||
|
||||
import { API, showError, showSuccess } from '../helpers';
|
||||
import SettingsChats from '../pages/Setting/Operation/SettingsChats.js';
|
||||
import { API, showError, showSuccess } from '../../helpers/index.js';
|
||||
import SettingsChats from '../../pages/Setting/Operation/SettingsChats.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import RequestRateLimit from '../pages/Setting/RateLimit/SettingsRequestRateLimit.js';
|
||||
import RequestRateLimit from '../../pages/Setting/RateLimit/SettingsRequestRateLimit.js';
|
||||
|
||||
const RateLimitSetting = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -13,12 +13,12 @@ import {
|
||||
} from '@douyinfe/semi-ui';
|
||||
const { Text } = Typography;
|
||||
import {
|
||||
API,
|
||||
removeTrailingSlash,
|
||||
showError,
|
||||
showSuccess,
|
||||
verifyJSON,
|
||||
} from '../helpers/utils';
|
||||
import { API } from '../helpers/api';
|
||||
verifyJSON
|
||||
} from '../../helpers';
|
||||
import axios from 'axios';
|
||||
|
||||
const SystemSetting = () => {
|
||||
@@ -5,14 +5,12 @@ import {
|
||||
showInfo,
|
||||
showSuccess,
|
||||
timestamp2string,
|
||||
} from '../helpers';
|
||||
|
||||
import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants';
|
||||
import {
|
||||
renderGroup,
|
||||
renderNumberWithPoint,
|
||||
renderQuota,
|
||||
} from '../helpers/render';
|
||||
renderQuota
|
||||
} from '../../helpers/index.js';
|
||||
|
||||
import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../../constants/index.js';
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
@@ -29,9 +27,9 @@ import {
|
||||
Typography,
|
||||
Checkbox,
|
||||
Card,
|
||||
Select,
|
||||
Select
|
||||
} from '@douyinfe/semi-ui';
|
||||
import EditChannel from '../pages/Channel/EditChannel';
|
||||
import EditChannel from '../../pages/Channel/EditChannel.js';
|
||||
import {
|
||||
IconList,
|
||||
IconTreeTriangleDown,
|
||||
@@ -48,8 +46,8 @@ import {
|
||||
IconCopy,
|
||||
IconSmallTriangleRight
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { loadChannelModels } from './utils.js';
|
||||
import EditTagModal from '../pages/Channel/EditTagModal.js';
|
||||
import { loadChannelModels } from '../../helpers/index.js';
|
||||
import EditTagModal from '../../pages/Channel/EditTagModal.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const ChannelsTable = () => {
|
||||
@@ -245,19 +243,16 @@ const ChannelsTable = () => {
|
||||
key: COLUMN_KEYS.ID,
|
||||
title: t('ID'),
|
||||
dataIndex: 'id',
|
||||
width: 50,
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.NAME,
|
||||
title: t('名称'),
|
||||
dataIndex: 'name',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.GROUP,
|
||||
title: t('分组'),
|
||||
dataIndex: 'group',
|
||||
width: 180,
|
||||
render: (text, record, index) => (
|
||||
<div>
|
||||
<Space spacing={2}>
|
||||
@@ -277,7 +272,6 @@ const ChannelsTable = () => {
|
||||
key: COLUMN_KEYS.TYPE,
|
||||
title: t('类型'),
|
||||
dataIndex: 'type',
|
||||
width: 120,
|
||||
render: (text, record, index) => {
|
||||
if (record.children === undefined) {
|
||||
return <>{renderType(text)}</>;
|
||||
@@ -290,7 +284,6 @@ const ChannelsTable = () => {
|
||||
key: COLUMN_KEYS.STATUS,
|
||||
title: t('状态'),
|
||||
dataIndex: 'status',
|
||||
width: 120,
|
||||
render: (text, record, index) => {
|
||||
if (text === 3) {
|
||||
if (record.other_info === '') {
|
||||
@@ -317,7 +310,6 @@ const ChannelsTable = () => {
|
||||
key: COLUMN_KEYS.RESPONSE_TIME,
|
||||
title: t('响应时间'),
|
||||
dataIndex: 'response_time',
|
||||
width: 120,
|
||||
render: (text, record, index) => (
|
||||
<div>{renderResponseTime(text)}</div>
|
||||
),
|
||||
@@ -326,7 +318,6 @@ const ChannelsTable = () => {
|
||||
key: COLUMN_KEYS.BALANCE,
|
||||
title: t('已用/剩余'),
|
||||
dataIndex: 'expired_time',
|
||||
width: 120,
|
||||
render: (text, record, index) => {
|
||||
if (record.children === undefined) {
|
||||
return (
|
||||
@@ -366,7 +357,6 @@ const ChannelsTable = () => {
|
||||
key: COLUMN_KEYS.PRIORITY,
|
||||
title: t('优先级'),
|
||||
dataIndex: 'priority',
|
||||
width: 100,
|
||||
render: (text, record, index) => {
|
||||
if (record.children === undefined) {
|
||||
return (
|
||||
@@ -419,7 +409,6 @@ const ChannelsTable = () => {
|
||||
key: COLUMN_KEYS.WEIGHT,
|
||||
title: t('权重'),
|
||||
dataIndex: 'weight',
|
||||
width: 100,
|
||||
render: (text, record, index) => {
|
||||
if (record.children === undefined) {
|
||||
return (
|
||||
@@ -472,7 +461,7 @@ const ChannelsTable = () => {
|
||||
key: COLUMN_KEYS.OPERATE,
|
||||
title: '',
|
||||
dataIndex: 'operate',
|
||||
width: 350,
|
||||
fixed: 'right',
|
||||
render: (text, record, index) => {
|
||||
if (record.children === undefined) {
|
||||
// 创建更多操作的下拉菜单项
|
||||
@@ -880,11 +869,14 @@ const ChannelsTable = () => {
|
||||
};
|
||||
|
||||
const copySelectedChannel = async (record) => {
|
||||
const channelToCopy = record;
|
||||
const channelToCopy = { ...record };
|
||||
channelToCopy.name += t('_复制');
|
||||
channelToCopy.created_time = null;
|
||||
channelToCopy.balance = 0;
|
||||
channelToCopy.used_quota = 0;
|
||||
// 删除可能导致类型不匹配的字段
|
||||
delete channelToCopy.test_time;
|
||||
delete channelToCopy.response_time;
|
||||
if (!channelToCopy) {
|
||||
showError(t('渠道未找到,请刷新页面后重试。'));
|
||||
return;
|
||||
@@ -1635,13 +1627,15 @@ const ChannelsTable = () => {
|
||||
/>
|
||||
|
||||
<Card
|
||||
className="!rounded-2xl overflow-hidden"
|
||||
className="!rounded-2xl"
|
||||
title={renderHeader()}
|
||||
shadows='hover'
|
||||
shadows='always'
|
||||
bordered={false}
|
||||
>
|
||||
<Table
|
||||
columns={getVisibleColumns()}
|
||||
dataSource={pageData}
|
||||
scroll={{ x: 'max-content' }}
|
||||
pagination={{
|
||||
currentPage: activePage,
|
||||
pageSize: pageSize,
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
API,
|
||||
@@ -8,7 +8,20 @@ import {
|
||||
showError,
|
||||
showSuccess,
|
||||
timestamp2string,
|
||||
} from '../helpers';
|
||||
renderAudioModelPrice,
|
||||
renderClaudeLogContent,
|
||||
renderClaudeModelPrice,
|
||||
renderClaudeModelPriceSimple,
|
||||
renderGroup,
|
||||
renderLogContent,
|
||||
renderModelPrice,
|
||||
renderModelPriceSimple,
|
||||
renderNumber,
|
||||
renderQuota,
|
||||
stringToColor,
|
||||
getLogOther,
|
||||
renderModelTag,
|
||||
} from '../../helpers';
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
@@ -29,31 +42,9 @@ import {
|
||||
Input,
|
||||
DatePicker,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { ITEMS_PER_PAGE } from '../constants';
|
||||
import {
|
||||
renderAudioModelPrice,
|
||||
renderClaudeLogContent,
|
||||
renderClaudeModelPrice,
|
||||
renderClaudeModelPriceSimple,
|
||||
renderGroup,
|
||||
renderLogContent,
|
||||
renderModelPrice,
|
||||
renderModelPriceSimple,
|
||||
renderNumber,
|
||||
renderQuota,
|
||||
stringToColor,
|
||||
} from '../helpers/render';
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
|
||||
import { getLogOther } from '../helpers/other.js';
|
||||
import {
|
||||
IconRefresh,
|
||||
IconSetting,
|
||||
IconEyeOpened,
|
||||
IconSearch,
|
||||
IconCoinMoneyStroked,
|
||||
IconPulse,
|
||||
IconTypograph,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { IconSetting, IconSearch, IconForward } from '@douyinfe/semi-icons';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -204,19 +195,11 @@ const LogsTable = () => {
|
||||
other?.upstream_model_name &&
|
||||
other?.upstream_model_name !== '';
|
||||
if (!modelMapped) {
|
||||
return (
|
||||
<Tag
|
||||
color={stringToColor(record.model_name)}
|
||||
size='large'
|
||||
shape='circle'
|
||||
onClick={(event) => {
|
||||
copyText(event, record.model_name).then((r) => { });
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
{record.model_name}{' '}
|
||||
</Tag>
|
||||
);
|
||||
return renderModelTag(record.model_name, {
|
||||
onClick: (event) => {
|
||||
copyText(event, record.model_name).then((r) => { });
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
@@ -225,48 +208,42 @@ const LogsTable = () => {
|
||||
content={
|
||||
<div style={{ padding: 10 }}>
|
||||
<Space vertical align={'start'}>
|
||||
<Tag
|
||||
color={stringToColor(record.model_name)}
|
||||
size='large'
|
||||
shape='circle'
|
||||
onClick={(event) => {
|
||||
copyText(event, record.model_name).then((r) => { });
|
||||
}}
|
||||
>
|
||||
{t('请求并计费模型')} {record.model_name}{' '}
|
||||
</Tag>
|
||||
<Tag
|
||||
color={stringToColor(other.upstream_model_name)}
|
||||
size='large'
|
||||
shape='circle'
|
||||
onClick={(event) => {
|
||||
copyText(event, other.upstream_model_name).then(
|
||||
(r) => { },
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t('实际模型')} {other.upstream_model_name}{' '}
|
||||
</Tag>
|
||||
<div className='flex items-center'>
|
||||
<Text strong style={{ marginRight: 8 }}>
|
||||
{t('请求并计费模型')}:
|
||||
</Text>
|
||||
{renderModelTag(record.model_name, {
|
||||
onClick: (event) => {
|
||||
copyText(event, record.model_name).then((r) => { });
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
<Text strong style={{ marginRight: 8 }}>
|
||||
{t('实际模型')}:
|
||||
</Text>
|
||||
{renderModelTag(other.upstream_model_name, {
|
||||
onClick: (event) => {
|
||||
copyText(event, other.upstream_model_name).then(
|
||||
(r) => { },
|
||||
);
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Tag
|
||||
color={stringToColor(record.model_name)}
|
||||
size='large'
|
||||
shape='circle'
|
||||
onClick={(event) => {
|
||||
{renderModelTag(record.model_name, {
|
||||
onClick: (event) => {
|
||||
copyText(event, record.model_name).then((r) => { });
|
||||
}}
|
||||
suffixIcon={
|
||||
<IconRefresh
|
||||
style={{ width: '0.8em', height: '0.8em', opacity: 0.6 }}
|
||||
},
|
||||
suffixIcon: (
|
||||
<IconForward
|
||||
style={{ width: '0.9em', height: '0.9em', opacity: 0.75 }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{' '}
|
||||
{record.model_name}{' '}
|
||||
</Tag>
|
||||
),
|
||||
})}
|
||||
</Popover>
|
||||
</Space>
|
||||
</>
|
||||
@@ -374,13 +351,11 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.TIME,
|
||||
title: t('时间'),
|
||||
dataIndex: 'timestamp2string',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
key: COLUMN_KEYS.CHANNEL,
|
||||
title: t('渠道'),
|
||||
dataIndex: 'channel',
|
||||
width: 80,
|
||||
className: isAdmin() ? 'tableShow' : 'tableHiddle',
|
||||
render: (text, record, index) => {
|
||||
return isAdminUser ? (
|
||||
@@ -411,7 +386,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.USERNAME,
|
||||
title: t('用户'),
|
||||
dataIndex: 'username',
|
||||
width: 150,
|
||||
className: isAdmin() ? 'tableShow' : 'tableHiddle',
|
||||
render: (text, record, index) => {
|
||||
return isAdminUser ? (
|
||||
@@ -438,7 +412,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.TOKEN,
|
||||
title: t('令牌'),
|
||||
dataIndex: 'token_name',
|
||||
width: 160,
|
||||
render: (text, record, index) => {
|
||||
return record.type === 0 || record.type === 2 || record.type === 5 ? (
|
||||
<div>
|
||||
@@ -464,7 +437,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.GROUP,
|
||||
title: t('分组'),
|
||||
dataIndex: 'group',
|
||||
width: 120,
|
||||
render: (text, record, index) => {
|
||||
if (record.type === 0 || record.type === 2 || record.type === 5) {
|
||||
if (record.group) {
|
||||
@@ -497,7 +469,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.TYPE,
|
||||
title: t('类型'),
|
||||
dataIndex: 'type',
|
||||
width: 100,
|
||||
render: (text, record, index) => {
|
||||
return <>{renderType(text)}</>;
|
||||
},
|
||||
@@ -506,7 +477,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.MODEL,
|
||||
title: t('模型'),
|
||||
dataIndex: 'model_name',
|
||||
width: 160,
|
||||
render: (text, record, index) => {
|
||||
return record.type === 0 || record.type === 2 || record.type === 5 ? (
|
||||
<>{renderModelName(record)}</>
|
||||
@@ -519,7 +489,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.USE_TIME,
|
||||
title: t('用时/首字'),
|
||||
dataIndex: 'use_time',
|
||||
width: 160,
|
||||
render: (text, record, index) => {
|
||||
if (record.is_stream) {
|
||||
let other = getLogOther(record.other);
|
||||
@@ -548,7 +517,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.PROMPT,
|
||||
title: t('提示'),
|
||||
dataIndex: 'prompt_tokens',
|
||||
width: 100,
|
||||
render: (text, record, index) => {
|
||||
return record.type === 0 || record.type === 2 || record.type === 5 ? (
|
||||
<>{<span> {text} </span>}</>
|
||||
@@ -561,7 +529,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.COMPLETION,
|
||||
title: t('补全'),
|
||||
dataIndex: 'completion_tokens',
|
||||
width: 100,
|
||||
render: (text, record, index) => {
|
||||
return parseInt(text) > 0 &&
|
||||
(record.type === 0 || record.type === 2 || record.type === 5) ? (
|
||||
@@ -575,7 +542,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.COST,
|
||||
title: t('花费'),
|
||||
dataIndex: 'quota',
|
||||
width: 120,
|
||||
render: (text, record, index) => {
|
||||
return record.type === 0 || record.type === 2 || record.type === 5 ? (
|
||||
<>{renderQuota(text, 6)}</>
|
||||
@@ -588,7 +554,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.RETRY,
|
||||
title: t('重试'),
|
||||
dataIndex: 'retry',
|
||||
width: 160,
|
||||
className: isAdmin() ? 'tableShow' : 'tableHiddle',
|
||||
render: (text, record, index) => {
|
||||
let content = t('渠道') + `:${record.channel}`;
|
||||
@@ -617,7 +582,7 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.DETAILS,
|
||||
title: t('详情'),
|
||||
dataIndex: 'content',
|
||||
width: 200,
|
||||
fixed: 'right',
|
||||
render: (text, record, index) => {
|
||||
let other = getLogOther(record.other);
|
||||
if (other == null || record.type !== 2) {
|
||||
@@ -691,25 +656,25 @@ const LogsTable = () => {
|
||||
visible={showColumnSelector}
|
||||
onCancel={() => setShowColumnSelector(false)}
|
||||
footer={
|
||||
<div className="flex justify-end">
|
||||
<div className='flex justify-end'>
|
||||
<Button
|
||||
theme="light"
|
||||
theme='light'
|
||||
onClick={() => initDefaultColumns()}
|
||||
className="!rounded-full"
|
||||
className='!rounded-full'
|
||||
>
|
||||
{t('重置')}
|
||||
</Button>
|
||||
<Button
|
||||
theme="light"
|
||||
theme='light'
|
||||
onClick={() => setShowColumnSelector(false)}
|
||||
className="!rounded-full"
|
||||
className='!rounded-full'
|
||||
>
|
||||
{t('取消')}
|
||||
</Button>
|
||||
<Button
|
||||
type='primary'
|
||||
onClick={() => setShowColumnSelector(false)}
|
||||
className="!rounded-full"
|
||||
className='!rounded-full'
|
||||
>
|
||||
{t('确定')}
|
||||
</Button>
|
||||
@@ -729,7 +694,7 @@ const LogsTable = () => {
|
||||
</Checkbox>
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-wrap max-h-96 overflow-y-auto rounded-lg p-4"
|
||||
className='flex flex-wrap max-h-96 overflow-y-auto rounded-lg p-4'
|
||||
style={{ border: '1px solid var(--semi-color-border)' }}
|
||||
>
|
||||
{allColumns.map((column) => {
|
||||
@@ -744,10 +709,7 @@ const LogsTable = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={column.key}
|
||||
className="w-1/2 mb-4 pr-2"
|
||||
>
|
||||
<div key={column.key} className='w-1/2 mb-4 pr-2'>
|
||||
<Checkbox
|
||||
checked={!!visibleColumns[column.key]}
|
||||
onChange={(e) =>
|
||||
@@ -1031,6 +993,9 @@ const LogsTable = () => {
|
||||
other?.file_search || false,
|
||||
other?.file_search_call_count || 0,
|
||||
other?.file_search_price || 0,
|
||||
other?.audio_input_seperate_price || false,
|
||||
other?.audio_input_token_count || 0,
|
||||
other?.audio_input_price || 0,
|
||||
);
|
||||
}
|
||||
expandDataLocal.push({
|
||||
@@ -1125,13 +1090,20 @@ const LogsTable = () => {
|
||||
return <Descriptions data={expandData[record.key]} />;
|
||||
};
|
||||
|
||||
// 检查是否有任何记录有展开内容
|
||||
const hasExpandableRows = () => {
|
||||
return logs.some(
|
||||
(log) => expandData[log.key] && expandData[log.key].length > 0,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderColumnSelector()}
|
||||
<Card
|
||||
className="!rounded-2xl overflow-hidden mb-4"
|
||||
className='!rounded-2xl mb-4'
|
||||
title={
|
||||
<div className="flex flex-col w-full">
|
||||
<div className='flex flex-col w-full'>
|
||||
<Spin spinning={loadingStat}>
|
||||
<Space>
|
||||
<Tag
|
||||
@@ -1174,15 +1146,15 @@ const LogsTable = () => {
|
||||
</Space>
|
||||
</Spin>
|
||||
|
||||
<Divider margin="12px" />
|
||||
<Divider margin='12px' />
|
||||
|
||||
{/* 搜索表单区域 */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4'>
|
||||
{/* 时间选择器 */}
|
||||
<div className="col-span-1 lg:col-span-2">
|
||||
<div className='col-span-1 lg:col-span-2'>
|
||||
<DatePicker
|
||||
className="w-full"
|
||||
className='w-full'
|
||||
value={[start_timestamp, end_timestamp]}
|
||||
type='dateTimeRange'
|
||||
onChange={(value) => {
|
||||
@@ -1198,7 +1170,7 @@ const LogsTable = () => {
|
||||
<Select
|
||||
value={logType.toString()}
|
||||
placeholder={t('日志类型')}
|
||||
className="!rounded-full"
|
||||
className='!rounded-full'
|
||||
onChange={(value) => {
|
||||
setLogType(parseInt(value));
|
||||
loadLogs(0, pageSize, parseInt(value));
|
||||
@@ -1218,7 +1190,7 @@ const LogsTable = () => {
|
||||
placeholder={t('令牌名称')}
|
||||
value={token_name}
|
||||
onChange={(value) => handleInputChange(value, 'token_name')}
|
||||
className="!rounded-full"
|
||||
className='!rounded-full'
|
||||
showClear
|
||||
/>
|
||||
|
||||
@@ -1227,7 +1199,7 @@ const LogsTable = () => {
|
||||
placeholder={t('模型名称')}
|
||||
value={model_name}
|
||||
onChange={(value) => handleInputChange(value, 'model_name')}
|
||||
className="!rounded-full"
|
||||
className='!rounded-full'
|
||||
showClear
|
||||
/>
|
||||
|
||||
@@ -1236,7 +1208,7 @@ const LogsTable = () => {
|
||||
placeholder={t('分组')}
|
||||
value={group}
|
||||
onChange={(value) => handleInputChange(value, 'group')}
|
||||
className="!rounded-full"
|
||||
className='!rounded-full'
|
||||
showClear
|
||||
/>
|
||||
|
||||
@@ -1247,7 +1219,7 @@ const LogsTable = () => {
|
||||
placeholder={t('渠道 ID')}
|
||||
value={channel}
|
||||
onChange={(value) => handleInputChange(value, 'channel')}
|
||||
className="!rounded-full"
|
||||
className='!rounded-full'
|
||||
showClear
|
||||
/>
|
||||
<Input
|
||||
@@ -1255,7 +1227,7 @@ const LogsTable = () => {
|
||||
placeholder={t('用户名称')}
|
||||
value={username}
|
||||
onChange={(value) => handleInputChange(value, 'username')}
|
||||
className="!rounded-full"
|
||||
className='!rounded-full'
|
||||
showClear
|
||||
/>
|
||||
</>
|
||||
@@ -1263,14 +1235,14 @@ const LogsTable = () => {
|
||||
</div>
|
||||
|
||||
{/* 操作按钮区域 */}
|
||||
<div className="flex justify-between items-center pt-2">
|
||||
<div className='flex justify-between items-center pt-2'>
|
||||
<div></div>
|
||||
<div className="flex gap-2">
|
||||
<div className='flex gap-2'>
|
||||
<Button
|
||||
type='primary'
|
||||
onClick={refresh}
|
||||
loading={loading}
|
||||
className="!rounded-full"
|
||||
className='!rounded-full'
|
||||
>
|
||||
{t('查询')}
|
||||
</Button>
|
||||
@@ -1279,7 +1251,7 @@ const LogsTable = () => {
|
||||
type='tertiary'
|
||||
icon={<IconSetting />}
|
||||
onClick={() => setShowColumnSelector(true)}
|
||||
className="!rounded-full"
|
||||
className='!rounded-full'
|
||||
>
|
||||
{t('列设置')}
|
||||
</Button>
|
||||
@@ -1288,17 +1260,22 @@ const LogsTable = () => {
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
shadows='hover'
|
||||
shadows='always'
|
||||
bordered={false}
|
||||
>
|
||||
<Table
|
||||
columns={getVisibleColumns()}
|
||||
expandedRowRender={expandRowRender}
|
||||
expandRowByClick={true}
|
||||
{...(hasExpandableRows() && {
|
||||
expandedRowRender: expandRowRender,
|
||||
expandRowByClick: true,
|
||||
rowExpandable: (record) => expandData[record.key] && expandData[record.key].length > 0
|
||||
})}
|
||||
dataSource={logs}
|
||||
rowKey='key'
|
||||
loading={loading}
|
||||
className="rounded-xl overflow-hidden"
|
||||
size="middle"
|
||||
scroll={{ x: 'max-content' }}
|
||||
className='rounded-xl overflow-hidden'
|
||||
size='middle'
|
||||
pagination={{
|
||||
formatPageText: (page) =>
|
||||
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
showError,
|
||||
showSuccess,
|
||||
timestamp2string,
|
||||
} from '../helpers';
|
||||
} from '../../helpers';
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
Tag,
|
||||
Typography,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { ITEMS_PER_PAGE } from '../constants';
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import {
|
||||
IconEyeOpened,
|
||||
IconSearch,
|
||||
@@ -374,7 +374,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.SUBMIT_TIME,
|
||||
title: t('提交时间'),
|
||||
dataIndex: 'submit_time',
|
||||
width: 180,
|
||||
render: (text, record, index) => {
|
||||
return <div>{renderTimestamp(text / 1000)}</div>;
|
||||
},
|
||||
@@ -383,7 +382,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.DURATION,
|
||||
title: t('花费时间'),
|
||||
dataIndex: 'finish_time',
|
||||
width: 120,
|
||||
render: (finish, record) => {
|
||||
return renderDuration(record.submit_time, finish);
|
||||
},
|
||||
@@ -392,7 +390,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.CHANNEL,
|
||||
title: t('渠道'),
|
||||
dataIndex: 'channel_id',
|
||||
width: 100,
|
||||
className: isAdmin() ? 'tableShow' : 'tableHiddle',
|
||||
render: (text, record, index) => {
|
||||
return isAdminUser ? (
|
||||
@@ -418,7 +415,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.TYPE,
|
||||
title: t('类型'),
|
||||
dataIndex: 'action',
|
||||
width: 120,
|
||||
render: (text, record, index) => {
|
||||
return <div>{renderType(text)}</div>;
|
||||
},
|
||||
@@ -427,7 +423,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.TASK_ID,
|
||||
title: t('任务ID'),
|
||||
dataIndex: 'mj_id',
|
||||
width: 200,
|
||||
render: (text, record, index) => {
|
||||
return <div>{text}</div>;
|
||||
},
|
||||
@@ -436,7 +431,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.SUBMIT_RESULT,
|
||||
title: t('提交结果'),
|
||||
dataIndex: 'code',
|
||||
width: 120,
|
||||
className: isAdmin() ? 'tableShow' : 'tableHiddle',
|
||||
render: (text, record, index) => {
|
||||
return isAdminUser ? <div>{renderCode(text)}</div> : <></>;
|
||||
@@ -446,7 +440,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.TASK_STATUS,
|
||||
title: t('任务状态'),
|
||||
dataIndex: 'status',
|
||||
width: 120,
|
||||
className: isAdmin() ? 'tableShow' : 'tableHiddle',
|
||||
render: (text, record, index) => {
|
||||
return <div>{renderStatus(text)}</div>;
|
||||
@@ -456,7 +449,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.PROGRESS,
|
||||
title: t('进度'),
|
||||
dataIndex: 'progress',
|
||||
width: 160,
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
@@ -470,6 +462,7 @@ const LogsTable = () => {
|
||||
percent={text ? parseInt(text.replace('%', '')) : 0}
|
||||
showInfo={true}
|
||||
aria-label='drawing progress'
|
||||
style={{ minWidth: '200px' }}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
@@ -480,7 +473,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.IMAGE,
|
||||
title: t('结果图片'),
|
||||
dataIndex: 'image_url',
|
||||
width: 120,
|
||||
render: (text, record, index) => {
|
||||
if (!text) {
|
||||
return t('无');
|
||||
@@ -501,7 +493,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.PROMPT,
|
||||
title: 'Prompt',
|
||||
dataIndex: 'prompt',
|
||||
width: 200,
|
||||
render: (text, record, index) => {
|
||||
if (!text) {
|
||||
return t('无');
|
||||
@@ -525,7 +516,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.PROMPT_EN,
|
||||
title: 'PromptEn',
|
||||
dataIndex: 'prompt_en',
|
||||
width: 200,
|
||||
render: (text, record, index) => {
|
||||
if (!text) {
|
||||
return t('无');
|
||||
@@ -549,7 +539,7 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.FAIL_REASON,
|
||||
title: t('失败原因'),
|
||||
dataIndex: 'fail_reason',
|
||||
width: 160,
|
||||
fixed: 'right',
|
||||
render: (text, record, index) => {
|
||||
if (!text) {
|
||||
return t('无');
|
||||
@@ -771,7 +761,7 @@ const LogsTable = () => {
|
||||
{renderColumnSelector()}
|
||||
<Layout>
|
||||
<Card
|
||||
className="!rounded-2xl overflow-hidden mb-4"
|
||||
className="!rounded-2xl mb-4"
|
||||
title={
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex flex-col md:flex-row justify-between items-center">
|
||||
@@ -864,13 +854,15 @@ const LogsTable = () => {
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
shadows='hover'
|
||||
shadows='always'
|
||||
bordered={false}
|
||||
>
|
||||
<Table
|
||||
columns={getVisibleColumns()}
|
||||
dataSource={pageData}
|
||||
rowKey='key'
|
||||
loading={loading}
|
||||
scroll={{ x: 'max-content' }}
|
||||
className="rounded-xl overflow-hidden"
|
||||
size="middle"
|
||||
pagination={{
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useContext, useEffect, useRef, useMemo, useState } from 'react';
|
||||
import { API, copy, showError, showInfo, showSuccess } from '../helpers';
|
||||
import { API, copy, showError, showInfo, showSuccess, getModelCategories, renderModelTag } from '../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
@@ -24,11 +24,10 @@ import {
|
||||
IconSearch,
|
||||
IconCopy,
|
||||
IconInfoCircle,
|
||||
IconCrown,
|
||||
IconLayers,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { UserContext } from '../context/User/index.js';
|
||||
import { UserContext } from '../../context/User/index.js';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import { MODEL_CATEGORIES } from '../constants';
|
||||
|
||||
const ModelPricing = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -116,29 +115,20 @@ const ModelPricing = () => {
|
||||
return Number(aAvailable) - Number(bAvailable);
|
||||
},
|
||||
defaultSortOrder: 'descend',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: t('模型名称'),
|
||||
dataIndex: 'model_name',
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Tag
|
||||
color='green'
|
||||
size='large'
|
||||
shape='circle'
|
||||
onClick={() => {
|
||||
copyText(text);
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</Tag>
|
||||
);
|
||||
return renderModelTag(text, {
|
||||
onClick: () => {
|
||||
copyText(text);
|
||||
}
|
||||
});
|
||||
},
|
||||
onFilter: (value, record) =>
|
||||
record.model_name.toLowerCase().includes(value.toLowerCase()),
|
||||
filteredValue,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: t('计费类型'),
|
||||
@@ -147,7 +137,6 @@ const ModelPricing = () => {
|
||||
return renderQuotaType(parseInt(text));
|
||||
},
|
||||
sorter: (a, b) => a.quota_type - b.quota_type,
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: t('可用分组'),
|
||||
@@ -224,7 +213,6 @@ const ModelPricing = () => {
|
||||
);
|
||||
return content;
|
||||
},
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: t('模型价格'),
|
||||
@@ -253,13 +241,12 @@ const ModelPricing = () => {
|
||||
let price = parseFloat(text) * groupRatio[selectedGroup];
|
||||
content = (
|
||||
<div className="text-gray-700">
|
||||
${t('模型价格')}:${price.toFixed(3)}
|
||||
{t('模型价格')}:${price.toFixed(3)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return content;
|
||||
},
|
||||
width: 250,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -326,7 +313,21 @@ const ModelPricing = () => {
|
||||
refresh().then();
|
||||
}, []);
|
||||
|
||||
const modelCategories = MODEL_CATEGORIES(t);
|
||||
const modelCategories = getModelCategories(t);
|
||||
|
||||
const categoryCounts = useMemo(() => {
|
||||
const counts = {};
|
||||
if (models.length > 0) {
|
||||
counts['all'] = models.length;
|
||||
|
||||
Object.entries(modelCategories).forEach(([key, category]) => {
|
||||
if (key !== 'all') {
|
||||
counts[key] = models.filter(model => category.filter(model)).length;
|
||||
}
|
||||
});
|
||||
}
|
||||
return counts;
|
||||
}, [models, modelCategories]);
|
||||
|
||||
const renderArrow = (items, pos, handleArrowClick) => {
|
||||
const style = {
|
||||
@@ -345,15 +346,29 @@ const ModelPricing = () => {
|
||||
<Dropdown
|
||||
render={
|
||||
<Dropdown.Menu>
|
||||
{items.map(item => (
|
||||
<Dropdown.Item
|
||||
key={item.itemKey}
|
||||
onClick={() => setActiveKey(item.itemKey)}
|
||||
icon={modelCategories[item.itemKey]?.icon}
|
||||
>
|
||||
{modelCategories[item.itemKey]?.label || item.itemKey}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
{items.map(item => {
|
||||
const key = item.itemKey;
|
||||
const modelCount = categoryCounts[key] || 0;
|
||||
|
||||
return (
|
||||
<Dropdown.Item
|
||||
key={item.itemKey}
|
||||
onClick={() => setActiveKey(item.itemKey)}
|
||||
icon={modelCategories[item.itemKey]?.icon}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{modelCategories[item.itemKey]?.label || item.itemKey}
|
||||
<Tag
|
||||
color={activeKey === item.itemKey ? 'red' : 'grey'}
|
||||
size='small'
|
||||
shape='circle'
|
||||
>
|
||||
{modelCount}
|
||||
</Tag>
|
||||
</div>
|
||||
</Dropdown.Item>
|
||||
);
|
||||
})}
|
||||
</Dropdown.Menu>
|
||||
}
|
||||
>
|
||||
@@ -387,18 +402,29 @@ const ModelPricing = () => {
|
||||
>
|
||||
{Object.entries(modelCategories)
|
||||
.filter(([key]) => availableCategories.includes(key))
|
||||
.map(([key, category]) => (
|
||||
<TabPane
|
||||
tab={
|
||||
<span className="flex items-center gap-2">
|
||||
{category.icon && <span className="w-4 h-4">{category.icon}</span>}
|
||||
{category.label}
|
||||
</span>
|
||||
}
|
||||
itemKey={key}
|
||||
key={key}
|
||||
/>
|
||||
))}
|
||||
.map(([key, category]) => {
|
||||
const modelCount = categoryCounts[key] || 0;
|
||||
|
||||
return (
|
||||
<TabPane
|
||||
tab={
|
||||
<span className="flex items-center gap-2">
|
||||
{category.icon && <span className="w-4 h-4">{category.icon}</span>}
|
||||
{category.label}
|
||||
<Tag
|
||||
color={activeKey === key ? 'red' : 'grey'}
|
||||
size='small'
|
||||
shape='circle'
|
||||
>
|
||||
{modelCount}
|
||||
</Tag>
|
||||
</span>
|
||||
}
|
||||
itemKey={key}
|
||||
key={key}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
@@ -425,7 +451,7 @@ const ModelPricing = () => {
|
||||
|
||||
// 搜索和操作区组件
|
||||
const SearchAndActions = useMemo(() => (
|
||||
<Card className="!rounded-xl mb-6" shadows='hover'>
|
||||
<Card className="!rounded-xl mb-6" bordered={false}>
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<Input
|
||||
@@ -456,7 +482,7 @@ const ModelPricing = () => {
|
||||
|
||||
// 表格组件
|
||||
const ModelTable = useMemo(() => (
|
||||
<Card className="!rounded-xl overflow-hidden" shadows='hover'>
|
||||
<Card className="!rounded-xl overflow-hidden" bordered={false}>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={filteredModels}
|
||||
@@ -481,7 +507,7 @@ const ModelPricing = () => {
|
||||
), [filteredModels, loading, columns, rowSelection, pageSize, t]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="bg-gray-50">
|
||||
<Layout>
|
||||
<Layout.Content>
|
||||
<div className="flex justify-center p-4 sm:p-6 md:p-8">
|
||||
@@ -508,7 +534,7 @@ const ModelPricing = () => {
|
||||
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-4 lg:gap-6">
|
||||
<div className="flex items-start">
|
||||
<div className="w-10 h-10 sm:w-12 sm:h-12 rounded-xl bg-white/10 flex items-center justify-center mr-3 sm:mr-4">
|
||||
<IconCrown size="large" className="text-white" />
|
||||
<IconLayers size="extra-large" className="text-white" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-base sm:text-lg font-semibold mb-1 sm:mb-2">
|
||||
+9
-13
@@ -5,10 +5,10 @@ import {
|
||||
showError,
|
||||
showSuccess,
|
||||
timestamp2string,
|
||||
} from '../helpers';
|
||||
renderQuota
|
||||
} from '../../helpers';
|
||||
|
||||
import { ITEMS_PER_PAGE } from '../constants';
|
||||
import { renderQuota } from '../helpers/render';
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
IconPlay,
|
||||
IconMore,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import EditRedemption from '../pages/Redemption/EditRedemption';
|
||||
import EditRedemption from '../../pages/Redemption/EditRedemption';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const { Text } = Typography;
|
||||
@@ -78,18 +78,15 @@ const RedemptionsTable = () => {
|
||||
{
|
||||
title: t('ID'),
|
||||
dataIndex: 'id',
|
||||
width: 50,
|
||||
},
|
||||
{
|
||||
title: t('名称'),
|
||||
dataIndex: 'name',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: t('状态'),
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
render: (text, record, index) => {
|
||||
return <div>{renderStatus(text)}</div>;
|
||||
},
|
||||
@@ -97,7 +94,6 @@ const RedemptionsTable = () => {
|
||||
{
|
||||
title: t('额度'),
|
||||
dataIndex: 'quota',
|
||||
width: 100,
|
||||
render: (text, record, index) => {
|
||||
return <div>{renderQuota(parseInt(text))}</div>;
|
||||
},
|
||||
@@ -105,7 +101,6 @@ const RedemptionsTable = () => {
|
||||
{
|
||||
title: t('创建时间'),
|
||||
dataIndex: 'created_time',
|
||||
width: 180,
|
||||
render: (text, record, index) => {
|
||||
return <div>{renderTimestamp(text)}</div>;
|
||||
},
|
||||
@@ -113,7 +108,6 @@ const RedemptionsTable = () => {
|
||||
{
|
||||
title: t('兑换人ID'),
|
||||
dataIndex: 'used_user_id',
|
||||
width: 100,
|
||||
render: (text, record, index) => {
|
||||
return <div>{text === 0 ? t('无') : text}</div>;
|
||||
},
|
||||
@@ -121,7 +115,7 @@ const RedemptionsTable = () => {
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'operate',
|
||||
width: 300,
|
||||
fixed: 'right',
|
||||
render: (text, record, index) => {
|
||||
// 创建更多操作的下拉菜单项
|
||||
const moreMenuItems = [
|
||||
@@ -499,13 +493,15 @@ const RedemptionsTable = () => {
|
||||
></EditRedemption>
|
||||
|
||||
<Card
|
||||
className="!rounded-2xl overflow-hidden"
|
||||
className="!rounded-2xl"
|
||||
title={renderHeader()}
|
||||
shadows='hover'
|
||||
shadows='always'
|
||||
bordered={false}
|
||||
>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={pageData}
|
||||
scroll={{ x: 'max-content' }}
|
||||
pagination={{
|
||||
currentPage: activePage,
|
||||
pageSize: pageSize,
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
showError,
|
||||
showSuccess,
|
||||
timestamp2string,
|
||||
} from '../helpers';
|
||||
} from '../../helpers';
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
Tag,
|
||||
Typography,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { ITEMS_PER_PAGE } from '../constants';
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import {
|
||||
IconEyeOpened,
|
||||
IconSearch,
|
||||
@@ -289,7 +289,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.SUBMIT_TIME,
|
||||
title: t('提交时间'),
|
||||
dataIndex: 'submit_time',
|
||||
width: 180,
|
||||
render: (text, record, index) => {
|
||||
return <div>{text ? renderTimestamp(text) : '-'}</div>;
|
||||
},
|
||||
@@ -298,7 +297,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.FINISH_TIME,
|
||||
title: t('结束时间'),
|
||||
dataIndex: 'finish_time',
|
||||
width: 180,
|
||||
render: (text, record, index) => {
|
||||
return <div>{text ? renderTimestamp(text) : '-'}</div>;
|
||||
},
|
||||
@@ -307,7 +305,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.DURATION,
|
||||
title: t('花费时间'),
|
||||
dataIndex: 'finish_time',
|
||||
width: 120,
|
||||
render: (finish, record) => {
|
||||
return <>{finish ? renderDuration(record.submit_time, finish) : '-'}</>;
|
||||
},
|
||||
@@ -316,7 +313,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.CHANNEL,
|
||||
title: t('渠道'),
|
||||
dataIndex: 'channel_id',
|
||||
width: 100,
|
||||
className: isAdminUser ? 'tableShow' : 'tableHiddle',
|
||||
render: (text, record, index) => {
|
||||
return isAdminUser ? (
|
||||
@@ -341,7 +337,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.PLATFORM,
|
||||
title: t('平台'),
|
||||
dataIndex: 'platform',
|
||||
width: 120,
|
||||
render: (text, record, index) => {
|
||||
return <div>{renderPlatform(text)}</div>;
|
||||
},
|
||||
@@ -350,7 +345,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.TYPE,
|
||||
title: t('类型'),
|
||||
dataIndex: 'action',
|
||||
width: 120,
|
||||
render: (text, record, index) => {
|
||||
return <div>{renderType(text)}</div>;
|
||||
},
|
||||
@@ -359,7 +353,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.TASK_ID,
|
||||
title: t('任务ID'),
|
||||
dataIndex: 'task_id',
|
||||
width: 200,
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Typography.Text
|
||||
@@ -378,7 +371,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.TASK_STATUS,
|
||||
title: t('任务状态'),
|
||||
dataIndex: 'status',
|
||||
width: 120,
|
||||
render: (text, record, index) => {
|
||||
return <div>{renderStatus(text)}</div>;
|
||||
},
|
||||
@@ -387,7 +379,6 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.PROGRESS,
|
||||
title: t('进度'),
|
||||
dataIndex: 'progress',
|
||||
width: 160,
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
@@ -404,6 +395,7 @@ const LogsTable = () => {
|
||||
percent={text ? parseInt(text.replace('%', '')) : 0}
|
||||
showInfo={true}
|
||||
aria-label='task progress'
|
||||
style={{ minWidth: '200px' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -415,7 +407,7 @@ const LogsTable = () => {
|
||||
key: COLUMN_KEYS.FAIL_REASON,
|
||||
title: t('失败原因'),
|
||||
dataIndex: 'fail_reason',
|
||||
width: 160,
|
||||
fixed: 'right',
|
||||
render: (text, record, index) => {
|
||||
if (!text) {
|
||||
return t('无');
|
||||
@@ -613,7 +605,7 @@ const LogsTable = () => {
|
||||
{renderColumnSelector()}
|
||||
<Layout>
|
||||
<Card
|
||||
className="!rounded-2xl overflow-hidden mb-4"
|
||||
className="!rounded-2xl mb-4"
|
||||
title={
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex flex-col md:flex-row justify-between items-center">
|
||||
@@ -702,13 +694,15 @@ const LogsTable = () => {
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
shadows='hover'
|
||||
shadows='always'
|
||||
bordered={false}
|
||||
>
|
||||
<Table
|
||||
columns={getVisibleColumns()}
|
||||
dataSource={pageData}
|
||||
rowKey='key'
|
||||
loading={loading}
|
||||
scroll={{ x: 'max-content' }}
|
||||
className="rounded-xl overflow-hidden"
|
||||
size="middle"
|
||||
pagination={{
|
||||
@@ -1,15 +1,15 @@
|
||||
import React, { useEffect, useState, useContext } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
API,
|
||||
copy,
|
||||
showError,
|
||||
showSuccess,
|
||||
timestamp2string,
|
||||
} from '../helpers';
|
||||
renderGroup,
|
||||
renderQuota
|
||||
} from '../../helpers';
|
||||
|
||||
import { ITEMS_PER_PAGE } from '../constants';
|
||||
import { renderGroup, renderQuota } from '../helpers/render';
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -20,8 +20,6 @@ import {
|
||||
Table,
|
||||
Tag,
|
||||
Input,
|
||||
Divider,
|
||||
Avatar,
|
||||
} from '@douyinfe/semi-ui';
|
||||
|
||||
import {
|
||||
@@ -35,13 +33,9 @@ import {
|
||||
IconStop,
|
||||
IconPlay,
|
||||
IconMore,
|
||||
IconMoneyExchangeStroked,
|
||||
IconHistogram,
|
||||
IconRotate,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import EditToken from '../pages/Token/EditToken';
|
||||
import EditToken from '../../pages/Token/EditToken';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { UserContext } from '../context/User';
|
||||
|
||||
function renderTimestamp(timestamp) {
|
||||
return <>{timestamp2string(timestamp)}</>;
|
||||
@@ -49,8 +43,6 @@ function renderTimestamp(timestamp) {
|
||||
|
||||
const TokensTable = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
|
||||
const renderStatus = (status, model_limits_enabled = false) => {
|
||||
switch (status) {
|
||||
@@ -99,13 +91,11 @@ const TokensTable = () => {
|
||||
{
|
||||
title: t('名称'),
|
||||
dataIndex: 'name',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: t('状态'),
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 200,
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
@@ -120,7 +110,6 @@ const TokensTable = () => {
|
||||
{
|
||||
title: t('已用额度'),
|
||||
dataIndex: 'used_quota',
|
||||
width: 120,
|
||||
render: (text, record, index) => {
|
||||
return <div>{renderQuota(parseInt(text))}</div>;
|
||||
},
|
||||
@@ -128,7 +117,6 @@ const TokensTable = () => {
|
||||
{
|
||||
title: t('剩余额度'),
|
||||
dataIndex: 'remain_quota',
|
||||
width: 120,
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
@@ -148,7 +136,6 @@ const TokensTable = () => {
|
||||
{
|
||||
title: t('创建时间'),
|
||||
dataIndex: 'created_time',
|
||||
width: 180,
|
||||
render: (text, record, index) => {
|
||||
return <div>{renderTimestamp(text)}</div>;
|
||||
},
|
||||
@@ -156,7 +143,6 @@ const TokensTable = () => {
|
||||
{
|
||||
title: t('过期时间'),
|
||||
dataIndex: 'expired_time',
|
||||
width: 180,
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
@@ -168,7 +154,7 @@ const TokensTable = () => {
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'operate',
|
||||
width: 320,
|
||||
fixed: 'right',
|
||||
render: (text, record, index) => {
|
||||
let chats = localStorage.getItem('chats');
|
||||
let chatsArray = [];
|
||||
@@ -430,26 +416,9 @@ const TokensTable = () => {
|
||||
window.open(url, '_blank');
|
||||
};
|
||||
|
||||
// 获取用户数据
|
||||
const getUserData = async () => {
|
||||
try {
|
||||
const res = await API.get(`/api/user/self`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
userDispatch({ type: 'login', payload: data });
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户数据失败:', error);
|
||||
showError(t('获取用户数据失败'));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
// 获取用户数据以确保显示正确的余额和使用量
|
||||
getUserData();
|
||||
|
||||
loadTokens(0)
|
||||
.then()
|
||||
.catch((reason) => {
|
||||
@@ -573,71 +542,6 @@ const TokensTable = () => {
|
||||
|
||||
const renderHeader = () => (
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Card
|
||||
shadows='hover'
|
||||
className="bg-blue-50 border-0 !rounded-2xl w-full"
|
||||
headerLine={false}
|
||||
onClick={() => navigate('/console/topup')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Avatar
|
||||
className="mr-3"
|
||||
size="medium"
|
||||
color="blue"
|
||||
>
|
||||
<IconMoneyExchangeStroked size="large" />
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="text-sm text-gray-500">{t('当前余额')}</div>
|
||||
<div className="text-xl font-semibold">{renderQuota(userState?.user?.quota)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
shadows='hover'
|
||||
className="bg-purple-50 border-0 !rounded-2xl w-full"
|
||||
headerLine={false}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Avatar
|
||||
className="mr-3"
|
||||
size="medium"
|
||||
color="purple"
|
||||
>
|
||||
<IconHistogram size="large" />
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="text-sm text-gray-500">{t('累计消费')}</div>
|
||||
<div className="text-xl font-semibold">{renderQuota(userState?.user?.used_quota)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
shadows='hover'
|
||||
className="bg-green-50 border-0 !rounded-2xl w-full"
|
||||
headerLine={false}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Avatar
|
||||
className="mr-3"
|
||||
size="medium"
|
||||
color="green"
|
||||
>
|
||||
<IconRotate size="large" />
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="text-sm text-gray-500">{t('请求次数')}</div>
|
||||
<div className="text-xl font-semibold">{userState?.user?.request_count || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Divider margin="12px" />
|
||||
|
||||
<div className="flex flex-col md:flex-row justify-between items-center gap-4 w-full">
|
||||
<div className="flex gap-2 w-full md:w-auto order-2 md:order-1">
|
||||
<Button
|
||||
@@ -672,7 +576,7 @@ const TokensTable = () => {
|
||||
await copyText(keys);
|
||||
}}
|
||||
>
|
||||
{t('复制所选兑换码到剪贴板')}
|
||||
{t('复制所选令牌到剪贴板')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -720,13 +624,15 @@ const TokensTable = () => {
|
||||
></EditToken>
|
||||
|
||||
<Card
|
||||
className="!rounded-2xl overflow-hidden"
|
||||
className="!rounded-2xl"
|
||||
title={renderHeader()}
|
||||
shadows='hover'
|
||||
shadows='always'
|
||||
bordered={false}
|
||||
>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={pageData}
|
||||
scroll={{ x: 'max-content' }}
|
||||
pagination={{
|
||||
currentPage: activePage,
|
||||
pageSize: pageSize,
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { API, showError, showSuccess } from '../helpers';
|
||||
import { API, showError, showSuccess, renderGroup, renderNumber, renderQuota } from '../../helpers';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -25,10 +25,9 @@ import {
|
||||
IconArrowUp,
|
||||
IconArrowDown,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { ITEMS_PER_PAGE } from '../constants';
|
||||
import { renderGroup, renderNumber, renderQuota } from '../helpers/render';
|
||||
import AddUser from '../pages/User/AddUser';
|
||||
import EditUser from '../pages/User/EditUser';
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import AddUser from '../../pages/User/AddUser';
|
||||
import EditUser from '../../pages/User/EditUser';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const { Text } = Typography;
|
||||
@@ -88,17 +87,14 @@ const UsersTable = () => {
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
width: 50,
|
||||
},
|
||||
{
|
||||
title: t('用户名'),
|
||||
dataIndex: 'username',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: t('分组'),
|
||||
dataIndex: 'group',
|
||||
width: 100,
|
||||
render: (text, record, index) => {
|
||||
return <div>{renderGroup(text)}</div>;
|
||||
},
|
||||
@@ -106,7 +102,6 @@ const UsersTable = () => {
|
||||
{
|
||||
title: t('统计信息'),
|
||||
dataIndex: 'info',
|
||||
width: 280,
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
@@ -128,7 +123,6 @@ const UsersTable = () => {
|
||||
{
|
||||
title: t('邀请信息'),
|
||||
dataIndex: 'invite',
|
||||
width: 250,
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
@@ -150,7 +144,6 @@ const UsersTable = () => {
|
||||
{
|
||||
title: t('角色'),
|
||||
dataIndex: 'role',
|
||||
width: 120,
|
||||
render: (text, record, index) => {
|
||||
return <div>{renderRole(text)}</div>;
|
||||
},
|
||||
@@ -158,7 +151,6 @@ const UsersTable = () => {
|
||||
{
|
||||
title: t('状态'),
|
||||
dataIndex: 'status',
|
||||
width: 100,
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
@@ -174,7 +166,7 @@ const UsersTable = () => {
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'operate',
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
render: (text, record, index) => {
|
||||
if (record.DeletedAt !== null) {
|
||||
return <></>;
|
||||
@@ -550,13 +542,15 @@ const UsersTable = () => {
|
||||
></EditUser>
|
||||
|
||||
<Card
|
||||
className="!rounded-2xl overflow-hidden"
|
||||
className="!rounded-2xl"
|
||||
title={renderHeader()}
|
||||
shadows='hover'
|
||||
shadows='always'
|
||||
bordered={false}
|
||||
>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={users}
|
||||
scroll={{ x: 'max-content' }}
|
||||
pagination={{
|
||||
formatPageText: (page) =>
|
||||
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
|
||||
@@ -1,76 +0,0 @@
|
||||
import { API, showError } from '../helpers';
|
||||
|
||||
export async function getOAuthState() {
|
||||
let path = '/api/oauth/state';
|
||||
let affCode = localStorage.getItem('aff');
|
||||
if (affCode && affCode.length > 0) {
|
||||
path += `?aff=${affCode}`;
|
||||
}
|
||||
const res = await API.get(path);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
return data;
|
||||
} else {
|
||||
showError(message);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export async function onOIDCClicked(auth_url, client_id, openInNewTab = false) {
|
||||
const state = await getOAuthState();
|
||||
if (!state) return;
|
||||
const redirect_uri = `${window.location.origin}/oauth/oidc`;
|
||||
const response_type = 'code';
|
||||
const scope = 'openid profile email';
|
||||
const url = `${auth_url}?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&state=${state}`;
|
||||
if (openInNewTab) {
|
||||
window.open(url);
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
|
||||
export async function onGitHubOAuthClicked(github_client_id) {
|
||||
const state = await getOAuthState();
|
||||
if (!state) return;
|
||||
window.open(
|
||||
`https://github.com/login/oauth/authorize?client_id=${github_client_id}&state=${state}&scope=user:email`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function onLinuxDOOAuthClicked(linuxdo_client_id) {
|
||||
const state = await getOAuthState();
|
||||
if (!state) return;
|
||||
window.open(
|
||||
`https://connect.linux.do/oauth2/authorize?response_type=code&client_id=${linuxdo_client_id}&state=${state}`,
|
||||
);
|
||||
}
|
||||
|
||||
let channelModels = undefined;
|
||||
export async function loadChannelModels() {
|
||||
const res = await API.get('/api/models');
|
||||
const { success, data } = res.data;
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
channelModels = data;
|
||||
localStorage.setItem('channel_models', JSON.stringify(data));
|
||||
}
|
||||
|
||||
export function getChannelModels(type) {
|
||||
if (channelModels !== undefined && type in channelModels) {
|
||||
if (!channelModels[type]) {
|
||||
return [];
|
||||
}
|
||||
return channelModels[type];
|
||||
}
|
||||
let models = localStorage.getItem('channel_models');
|
||||
if (!models) {
|
||||
return [];
|
||||
}
|
||||
channelModels = JSON.parse(models);
|
||||
if (type in channelModels) {
|
||||
return channelModels[type];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
@@ -2,4 +2,4 @@ export * from './channel.constants';
|
||||
export * from './user.constants';
|
||||
export * from './toast.constants';
|
||||
export * from './common.constant';
|
||||
export * from './model.constants';
|
||||
export * from './playground.constants';
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
import {
|
||||
OpenAI,
|
||||
Claude,
|
||||
Gemini,
|
||||
Moonshot,
|
||||
Zhipu,
|
||||
Qwen,
|
||||
DeepSeek,
|
||||
Minimax,
|
||||
Wenxin,
|
||||
Spark,
|
||||
Midjourney,
|
||||
Hunyuan,
|
||||
Cohere,
|
||||
Cloudflare,
|
||||
Ai360,
|
||||
Yi,
|
||||
Jina,
|
||||
Mistral,
|
||||
XAI,
|
||||
Ollama,
|
||||
Doubao,
|
||||
} from '@lobehub/icons';
|
||||
|
||||
export const MODEL_CATEGORIES = (t) => ({
|
||||
all: {
|
||||
label: t('全部模型'),
|
||||
icon: null,
|
||||
filter: () => true
|
||||
},
|
||||
openai: {
|
||||
label: 'OpenAI',
|
||||
icon: <OpenAI />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('gpt') ||
|
||||
model.model_name.toLowerCase().includes('dall-e') ||
|
||||
model.model_name.toLowerCase().includes('whisper') ||
|
||||
model.model_name.toLowerCase().includes('tts') ||
|
||||
model.model_name.toLowerCase().includes('text-') ||
|
||||
model.model_name.toLowerCase().includes('babbage') ||
|
||||
model.model_name.toLowerCase().includes('davinci') ||
|
||||
model.model_name.toLowerCase().includes('curie') ||
|
||||
model.model_name.toLowerCase().includes('ada')
|
||||
},
|
||||
anthropic: {
|
||||
label: 'Anthropic',
|
||||
icon: <Claude.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('claude')
|
||||
},
|
||||
gemini: {
|
||||
label: 'Gemini',
|
||||
icon: <Gemini.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('gemini')
|
||||
},
|
||||
moonshot: {
|
||||
label: 'Moonshot',
|
||||
icon: <Moonshot />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('moonshot')
|
||||
},
|
||||
zhipu: {
|
||||
label: t('智谱'),
|
||||
icon: <Zhipu.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('chatglm') ||
|
||||
model.model_name.toLowerCase().includes('glm-')
|
||||
},
|
||||
qwen: {
|
||||
label: t('通义千问'),
|
||||
icon: <Qwen.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('qwen')
|
||||
},
|
||||
deepseek: {
|
||||
label: 'DeepSeek',
|
||||
icon: <DeepSeek.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('deepseek')
|
||||
},
|
||||
minimax: {
|
||||
label: 'MiniMax',
|
||||
icon: <Minimax.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('abab')
|
||||
},
|
||||
baidu: {
|
||||
label: t('文心一言'),
|
||||
icon: <Wenxin.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('ernie')
|
||||
},
|
||||
xunfei: {
|
||||
label: t('讯飞星火'),
|
||||
icon: <Spark.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('spark')
|
||||
},
|
||||
midjourney: {
|
||||
label: 'Midjourney',
|
||||
icon: <Midjourney />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('mj_')
|
||||
},
|
||||
tencent: {
|
||||
label: t('腾讯混元'),
|
||||
icon: <Hunyuan.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('hunyuan')
|
||||
},
|
||||
cohere: {
|
||||
label: 'Cohere',
|
||||
icon: <Cohere.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('command')
|
||||
},
|
||||
cloudflare: {
|
||||
label: 'Cloudflare',
|
||||
icon: <Cloudflare.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('@cf/')
|
||||
},
|
||||
ai360: {
|
||||
label: t('360智脑'),
|
||||
icon: <Ai360.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('360')
|
||||
},
|
||||
yi: {
|
||||
label: t('零一万物'),
|
||||
icon: <Yi.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('yi')
|
||||
},
|
||||
jina: {
|
||||
label: 'Jina',
|
||||
icon: <Jina />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('jina')
|
||||
},
|
||||
mistral: {
|
||||
label: 'Mistral AI',
|
||||
icon: <Mistral.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('mistral')
|
||||
},
|
||||
xai: {
|
||||
label: 'xAI',
|
||||
icon: <XAI />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('grok')
|
||||
},
|
||||
llama: {
|
||||
label: 'Llama',
|
||||
icon: <Ollama />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('llama')
|
||||
},
|
||||
doubao: {
|
||||
label: t('豆包'),
|
||||
icon: <Doubao.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('doubao')
|
||||
}
|
||||
});
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import React, { useReducer, useEffect, useMemo, createContext } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { isMobile as getIsMobile } from '../../helpers/index.js';
|
||||
import { isMobile as getIsMobile } from '../../helpers';
|
||||
|
||||
// Action Types
|
||||
const ACTION_TYPES = {
|
||||
|
||||
+184
-1
@@ -1,5 +1,6 @@
|
||||
import { getUserIdFromLocalStorage, showError } from './utils';
|
||||
import { getUserIdFromLocalStorage, showError, formatMessageForAPI, isValidMessage } from './utils';
|
||||
import axios from 'axios';
|
||||
import { MESSAGE_ROLES } from '../constants/playground.constants';
|
||||
|
||||
export let API = axios.create({
|
||||
baseURL: import.meta.env.VITE_REACT_APP_SERVER_URL
|
||||
@@ -29,3 +30,185 @@ API.interceptors.response.use(
|
||||
showError(error);
|
||||
},
|
||||
);
|
||||
|
||||
// playground
|
||||
|
||||
// 构建API请求负载
|
||||
export const buildApiPayload = (messages, systemPrompt, inputs, parameterEnabled) => {
|
||||
const processedMessages = messages
|
||||
.filter(isValidMessage)
|
||||
.map(formatMessageForAPI)
|
||||
.filter(Boolean);
|
||||
|
||||
// 如果有系统提示,插入到消息开头
|
||||
if (systemPrompt && systemPrompt.trim()) {
|
||||
processedMessages.unshift({
|
||||
role: MESSAGE_ROLES.SYSTEM,
|
||||
content: systemPrompt.trim()
|
||||
});
|
||||
}
|
||||
|
||||
const payload = {
|
||||
model: inputs.model,
|
||||
messages: processedMessages,
|
||||
stream: inputs.stream,
|
||||
};
|
||||
|
||||
// 添加启用的参数
|
||||
const parameterMappings = {
|
||||
temperature: 'temperature',
|
||||
top_p: 'top_p',
|
||||
max_tokens: 'max_tokens',
|
||||
frequency_penalty: 'frequency_penalty',
|
||||
presence_penalty: 'presence_penalty',
|
||||
seed: 'seed'
|
||||
};
|
||||
|
||||
Object.entries(parameterMappings).forEach(([key, param]) => {
|
||||
if (parameterEnabled[key] && inputs[param] !== undefined && inputs[param] !== null) {
|
||||
payload[param] = inputs[param];
|
||||
}
|
||||
});
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
// 处理API错误响应
|
||||
export const handleApiError = (error, response = null) => {
|
||||
const errorInfo = {
|
||||
error: error.message || '未知错误',
|
||||
timestamp: new Date().toISOString(),
|
||||
stack: error.stack
|
||||
};
|
||||
|
||||
if (response) {
|
||||
errorInfo.status = response.status;
|
||||
errorInfo.statusText = response.statusText;
|
||||
}
|
||||
|
||||
if (error.message.includes('HTTP error')) {
|
||||
errorInfo.details = '服务器返回了错误状态码';
|
||||
} else if (error.message.includes('Failed to fetch')) {
|
||||
errorInfo.details = '网络连接失败或服务器无响应';
|
||||
}
|
||||
|
||||
return errorInfo;
|
||||
};
|
||||
|
||||
// 处理模型数据
|
||||
export const processModelsData = (data, currentModel) => {
|
||||
const modelOptions = data.map(model => ({
|
||||
label: model,
|
||||
value: model,
|
||||
}));
|
||||
|
||||
const hasCurrentModel = modelOptions.some(option => option.value === currentModel);
|
||||
const selectedModel = hasCurrentModel && modelOptions.length > 0
|
||||
? currentModel
|
||||
: modelOptions[0]?.value;
|
||||
|
||||
return { modelOptions, selectedModel };
|
||||
};
|
||||
|
||||
// 处理分组数据
|
||||
export const processGroupsData = (data, userGroup) => {
|
||||
let groupOptions = Object.entries(data).map(([group, info]) => ({
|
||||
label: info.desc.length > 20 ? info.desc.substring(0, 20) + '...' : info.desc,
|
||||
value: group,
|
||||
ratio: info.ratio,
|
||||
fullLabel: info.desc,
|
||||
}));
|
||||
|
||||
if (groupOptions.length === 0) {
|
||||
groupOptions = [{
|
||||
label: '用户分组',
|
||||
value: '',
|
||||
ratio: 1,
|
||||
}];
|
||||
} else if (userGroup) {
|
||||
const userGroupIndex = groupOptions.findIndex(g => g.value === userGroup);
|
||||
if (userGroupIndex > -1) {
|
||||
const userGroupOption = groupOptions.splice(userGroupIndex, 1)[0];
|
||||
groupOptions.unshift(userGroupOption);
|
||||
}
|
||||
}
|
||||
|
||||
return groupOptions;
|
||||
};
|
||||
|
||||
// 原来components中的utils.js
|
||||
|
||||
export async function getOAuthState() {
|
||||
let path = '/api/oauth/state';
|
||||
let affCode = localStorage.getItem('aff');
|
||||
if (affCode && affCode.length > 0) {
|
||||
path += `?aff=${affCode}`;
|
||||
}
|
||||
const res = await API.get(path);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
return data;
|
||||
} else {
|
||||
showError(message);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export async function onOIDCClicked(auth_url, client_id, openInNewTab = false) {
|
||||
const state = await getOAuthState();
|
||||
if (!state) return;
|
||||
const redirect_uri = `${window.location.origin}/oauth/oidc`;
|
||||
const response_type = 'code';
|
||||
const scope = 'openid profile email';
|
||||
const url = `${auth_url}?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&state=${state}`;
|
||||
if (openInNewTab) {
|
||||
window.open(url);
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
|
||||
export async function onGitHubOAuthClicked(github_client_id) {
|
||||
const state = await getOAuthState();
|
||||
if (!state) return;
|
||||
window.open(
|
||||
`https://github.com/login/oauth/authorize?client_id=${github_client_id}&state=${state}&scope=user:email`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function onLinuxDOOAuthClicked(linuxdo_client_id) {
|
||||
const state = await getOAuthState();
|
||||
if (!state) return;
|
||||
window.open(
|
||||
`https://connect.linux.do/oauth2/authorize?response_type=code&client_id=${linuxdo_client_id}&state=${state}`,
|
||||
);
|
||||
}
|
||||
|
||||
let channelModels = undefined;
|
||||
export async function loadChannelModels() {
|
||||
const res = await API.get('/api/models');
|
||||
const { success, data } = res.data;
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
channelModels = data;
|
||||
localStorage.setItem('channel_models', JSON.stringify(data));
|
||||
}
|
||||
|
||||
export function getChannelModels(type) {
|
||||
if (channelModels !== undefined && type in channelModels) {
|
||||
if (!channelModels[type]) {
|
||||
return [];
|
||||
}
|
||||
return channelModels[type];
|
||||
}
|
||||
let models = localStorage.getItem('channel_models');
|
||||
if (!models) {
|
||||
return [];
|
||||
}
|
||||
channelModels = JSON.parse(models);
|
||||
if (type in channelModels) {
|
||||
return channelModels[type];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
export function authHeader() {
|
||||
// return authorization header with jwt token
|
||||
let user = JSON.parse(localStorage.getItem('user'));
|
||||
|
||||
if (user && user.token) {
|
||||
return { Authorization: 'Bearer ' + user.token };
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { history } from './history';
|
||||
|
||||
export function authHeader() {
|
||||
// return authorization header with jwt token
|
||||
let user = JSON.parse(localStorage.getItem('user'));
|
||||
|
||||
if (user && user.token) {
|
||||
return { Authorization: 'Bearer ' + user.token };
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export const AuthRedirect = ({ children }) => {
|
||||
const user = localStorage.getItem('user');
|
||||
|
||||
if (user) {
|
||||
return <Navigate to="/console" replace />;
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
function PrivateRoute({ children }) {
|
||||
if (!localStorage.getItem('user')) {
|
||||
return <Navigate to='/login' state={{ from: history.location }} />;
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
export { PrivateRoute };
|
||||
@@ -1,4 +1,8 @@
|
||||
export * from './history';
|
||||
export * from './auth-header';
|
||||
export * from './auth';
|
||||
export * from './utils';
|
||||
export * from './api';
|
||||
export * from './render';
|
||||
export * from './log';
|
||||
export * from './data';
|
||||
export * from './token';
|
||||
|
||||
@@ -4,4 +4,4 @@ export function getLogOther(otherStr) {
|
||||
}
|
||||
let other = JSON.parse(otherStr);
|
||||
return other;
|
||||
}
|
||||
}
|
||||
+642
-214
@@ -1,6 +1,479 @@
|
||||
import i18next from 'i18next';
|
||||
import { Modal, Tag, Typography } from '@douyinfe/semi-ui';
|
||||
import { copy, isMobile, showSuccess } from './utils.js';
|
||||
import { copy, isMobile, showSuccess } from './utils';
|
||||
import { visit } from 'unist-util-visit';
|
||||
import {
|
||||
OpenAI,
|
||||
Claude,
|
||||
Gemini,
|
||||
Moonshot,
|
||||
Zhipu,
|
||||
Qwen,
|
||||
DeepSeek,
|
||||
Minimax,
|
||||
Wenxin,
|
||||
Spark,
|
||||
Midjourney,
|
||||
Hunyuan,
|
||||
Cohere,
|
||||
Cloudflare,
|
||||
Ai360,
|
||||
Yi,
|
||||
Jina,
|
||||
Mistral,
|
||||
XAI,
|
||||
Ollama,
|
||||
Doubao,
|
||||
} from '@lobehub/icons';
|
||||
|
||||
import {
|
||||
LayoutDashboard,
|
||||
TerminalSquare,
|
||||
MessageSquare,
|
||||
Key,
|
||||
BarChart3,
|
||||
Image as ImageIcon,
|
||||
CheckSquare,
|
||||
CreditCard,
|
||||
Layers,
|
||||
Gift,
|
||||
User,
|
||||
Settings,
|
||||
CircleUser,
|
||||
} from 'lucide-react';
|
||||
|
||||
// 侧边栏图标颜色映射
|
||||
export const sidebarIconColors = {
|
||||
dashboard: '#4F46E5', // 紫蓝色
|
||||
terminal: '#10B981', // 绿色
|
||||
message: '#06B6D4', // 青色
|
||||
key: '#3B82F6', // 蓝色
|
||||
chart: '#8B5CF6', // 紫色
|
||||
image: '#EC4899', // 粉色
|
||||
check: '#F59E0B', // 琥珀色
|
||||
credit: '#F97316', // 橙色
|
||||
layers: '#EF4444', // 红色
|
||||
gift: '#F43F5E', // 玫红色
|
||||
user: '#6366F1', // 靛蓝色
|
||||
settings: '#6B7280', // 灰色
|
||||
};
|
||||
|
||||
// 获取侧边栏Lucide图标组件
|
||||
export function getLucideIcon(key, selected = false) {
|
||||
const size = 16;
|
||||
const strokeWidth = 2;
|
||||
const commonProps = {
|
||||
size,
|
||||
strokeWidth,
|
||||
className: `transition-colors duration-200 ${selected ? 'transition-transform duration-200 scale-105' : ''}`,
|
||||
};
|
||||
|
||||
// 根据不同的key返回不同的图标
|
||||
switch (key) {
|
||||
case 'detail':
|
||||
return (
|
||||
<LayoutDashboard
|
||||
{...commonProps}
|
||||
color={selected ? sidebarIconColors.dashboard : 'currentColor'}
|
||||
/>
|
||||
);
|
||||
case 'playground':
|
||||
return (
|
||||
<TerminalSquare
|
||||
{...commonProps}
|
||||
color={selected ? sidebarIconColors.terminal : 'currentColor'}
|
||||
/>
|
||||
);
|
||||
case 'chat':
|
||||
return (
|
||||
<MessageSquare
|
||||
{...commonProps}
|
||||
color={selected ? sidebarIconColors.message : 'currentColor'}
|
||||
/>
|
||||
);
|
||||
case 'token':
|
||||
return (
|
||||
<Key
|
||||
{...commonProps}
|
||||
color={selected ? sidebarIconColors.key : 'currentColor'}
|
||||
/>
|
||||
);
|
||||
case 'log':
|
||||
return (
|
||||
<BarChart3
|
||||
{...commonProps}
|
||||
color={selected ? sidebarIconColors.chart : 'currentColor'}
|
||||
/>
|
||||
);
|
||||
case 'midjourney':
|
||||
return (
|
||||
<ImageIcon
|
||||
{...commonProps}
|
||||
color={selected ? sidebarIconColors.image : 'currentColor'}
|
||||
/>
|
||||
);
|
||||
case 'task':
|
||||
return (
|
||||
<CheckSquare
|
||||
{...commonProps}
|
||||
color={selected ? sidebarIconColors.check : 'currentColor'}
|
||||
/>
|
||||
);
|
||||
case 'topup':
|
||||
return (
|
||||
<CreditCard
|
||||
{...commonProps}
|
||||
color={selected ? sidebarIconColors.credit : 'currentColor'}
|
||||
/>
|
||||
);
|
||||
case 'channel':
|
||||
return (
|
||||
<Layers
|
||||
{...commonProps}
|
||||
color={selected ? sidebarIconColors.layers : 'currentColor'}
|
||||
/>
|
||||
);
|
||||
case 'redemption':
|
||||
return (
|
||||
<Gift
|
||||
{...commonProps}
|
||||
color={selected ? sidebarIconColors.gift : 'currentColor'}
|
||||
/>
|
||||
);
|
||||
case 'user':
|
||||
case 'personal':
|
||||
return (
|
||||
<User
|
||||
{...commonProps}
|
||||
color={selected ? sidebarIconColors.user : 'currentColor'}
|
||||
/>
|
||||
);
|
||||
case 'setting':
|
||||
return (
|
||||
<Settings
|
||||
{...commonProps}
|
||||
color={selected ? sidebarIconColors.settings : 'currentColor'}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<CircleUser
|
||||
{...commonProps}
|
||||
color={selected ? sidebarIconColors.user : 'currentColor'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取模型分类
|
||||
export const getModelCategories = (() => {
|
||||
let categoriesCache = null;
|
||||
let lastLocale = null;
|
||||
|
||||
return (t) => {
|
||||
const currentLocale = i18next.language;
|
||||
if (categoriesCache && lastLocale === currentLocale) {
|
||||
return categoriesCache;
|
||||
}
|
||||
|
||||
categoriesCache = {
|
||||
all: {
|
||||
label: t('全部模型'),
|
||||
icon: null,
|
||||
filter: () => true,
|
||||
},
|
||||
openai: {
|
||||
label: 'OpenAI',
|
||||
icon: <OpenAI />,
|
||||
filter: (model) =>
|
||||
model.model_name.toLowerCase().includes('gpt') ||
|
||||
model.model_name.toLowerCase().includes('dall-e') ||
|
||||
model.model_name.toLowerCase().includes('whisper') ||
|
||||
model.model_name.toLowerCase().includes('tts') ||
|
||||
model.model_name.toLowerCase().includes('text-') ||
|
||||
model.model_name.toLowerCase().includes('babbage') ||
|
||||
model.model_name.toLowerCase().includes('davinci') ||
|
||||
model.model_name.toLowerCase().includes('curie') ||
|
||||
model.model_name.toLowerCase().includes('ada') ||
|
||||
model.model_name.toLowerCase().includes('o1') ||
|
||||
model.model_name.toLowerCase().includes('o3') ||
|
||||
model.model_name.toLowerCase().includes('o4'),
|
||||
},
|
||||
anthropic: {
|
||||
label: 'Anthropic',
|
||||
icon: <Claude.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('claude'),
|
||||
},
|
||||
gemini: {
|
||||
label: 'Gemini',
|
||||
icon: <Gemini.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('gemini'),
|
||||
},
|
||||
moonshot: {
|
||||
label: 'Moonshot',
|
||||
icon: <Moonshot />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('moonshot'),
|
||||
},
|
||||
zhipu: {
|
||||
label: t('智谱'),
|
||||
icon: <Zhipu.Color />,
|
||||
filter: (model) =>
|
||||
model.model_name.toLowerCase().includes('chatglm') ||
|
||||
model.model_name.toLowerCase().includes('glm-'),
|
||||
},
|
||||
qwen: {
|
||||
label: t('通义千问'),
|
||||
icon: <Qwen.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('qwen'),
|
||||
},
|
||||
deepseek: {
|
||||
label: 'DeepSeek',
|
||||
icon: <DeepSeek.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('deepseek'),
|
||||
},
|
||||
minimax: {
|
||||
label: 'MiniMax',
|
||||
icon: <Minimax.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('abab'),
|
||||
},
|
||||
baidu: {
|
||||
label: t('文心一言'),
|
||||
icon: <Wenxin.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('ernie'),
|
||||
},
|
||||
xunfei: {
|
||||
label: t('讯飞星火'),
|
||||
icon: <Spark.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('spark'),
|
||||
},
|
||||
midjourney: {
|
||||
label: 'Midjourney',
|
||||
icon: <Midjourney />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('mj_'),
|
||||
},
|
||||
tencent: {
|
||||
label: t('腾讯混元'),
|
||||
icon: <Hunyuan.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('hunyuan'),
|
||||
},
|
||||
cohere: {
|
||||
label: 'Cohere',
|
||||
icon: <Cohere.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('command'),
|
||||
},
|
||||
cloudflare: {
|
||||
label: 'Cloudflare',
|
||||
icon: <Cloudflare.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('@cf/'),
|
||||
},
|
||||
ai360: {
|
||||
label: t('360智脑'),
|
||||
icon: <Ai360.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('360'),
|
||||
},
|
||||
yi: {
|
||||
label: t('零一万物'),
|
||||
icon: <Yi.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('yi'),
|
||||
},
|
||||
jina: {
|
||||
label: 'Jina',
|
||||
icon: <Jina />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('jina'),
|
||||
},
|
||||
mistral: {
|
||||
label: 'Mistral AI',
|
||||
icon: <Mistral.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('mistral'),
|
||||
},
|
||||
xai: {
|
||||
label: 'xAI',
|
||||
icon: <XAI />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('grok'),
|
||||
},
|
||||
llama: {
|
||||
label: 'Llama',
|
||||
icon: <Ollama />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('llama'),
|
||||
},
|
||||
doubao: {
|
||||
label: t('豆包'),
|
||||
icon: <Doubao.Color />,
|
||||
filter: (model) => model.model_name.toLowerCase().includes('doubao'),
|
||||
},
|
||||
};
|
||||
|
||||
lastLocale = currentLocale;
|
||||
return categoriesCache;
|
||||
};
|
||||
})();
|
||||
|
||||
// 颜色列表
|
||||
const colors = [
|
||||
'amber',
|
||||
'blue',
|
||||
'cyan',
|
||||
'green',
|
||||
'grey',
|
||||
'indigo',
|
||||
'light-blue',
|
||||
'lime',
|
||||
'orange',
|
||||
'pink',
|
||||
'purple',
|
||||
'red',
|
||||
'teal',
|
||||
'violet',
|
||||
'yellow',
|
||||
];
|
||||
|
||||
// 基础10色色板 (N ≤ 10)
|
||||
const baseColors = [
|
||||
'#1664FF', // 主色
|
||||
'#1AC6FF',
|
||||
'#FF8A00',
|
||||
'#3CC780',
|
||||
'#7442D4',
|
||||
'#FFC400',
|
||||
'#304D77',
|
||||
'#B48DEB',
|
||||
'#009488',
|
||||
'#FF7DDA',
|
||||
];
|
||||
|
||||
// 扩展20色色板 (10 < N ≤ 20)
|
||||
const extendedColors = [
|
||||
'#1664FF',
|
||||
'#B2CFFF',
|
||||
'#1AC6FF',
|
||||
'#94EFFF',
|
||||
'#FF8A00',
|
||||
'#FFCE7A',
|
||||
'#3CC780',
|
||||
'#B9EDCD',
|
||||
'#7442D4',
|
||||
'#DDC5FA',
|
||||
'#FFC400',
|
||||
'#FAE878',
|
||||
'#304D77',
|
||||
'#8B959E',
|
||||
'#B48DEB',
|
||||
'#EFE3FF',
|
||||
'#009488',
|
||||
'#59BAA8',
|
||||
'#FF7DDA',
|
||||
'#FFCFEE',
|
||||
];
|
||||
|
||||
// 模型颜色映射
|
||||
export const modelColorMap = {
|
||||
'dall-e': 'rgb(147,112,219)', // 深紫色
|
||||
// 'dall-e-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调
|
||||
'dall-e-3': 'rgb(153,50,204)', // 介于紫罗兰和洋红之间的色调
|
||||
'gpt-3.5-turbo': 'rgb(184,227,167)', // 浅绿色
|
||||
// 'gpt-3.5-turbo-0301': 'rgb(131,220,131)', // 亮绿色
|
||||
'gpt-3.5-turbo-0613': 'rgb(60,179,113)', // 海洋绿
|
||||
'gpt-3.5-turbo-1106': 'rgb(32,178,170)', // 浅海洋绿
|
||||
'gpt-3.5-turbo-16k': 'rgb(149,252,206)', // 淡橙色
|
||||
'gpt-3.5-turbo-16k-0613': 'rgb(119,255,214)', // 淡桃
|
||||
'gpt-3.5-turbo-instruct': 'rgb(175,238,238)', // 粉蓝色
|
||||
'gpt-4': 'rgb(135,206,235)', // 天蓝色
|
||||
// 'gpt-4-0314': 'rgb(70,130,180)', // 钢蓝色
|
||||
'gpt-4-0613': 'rgb(100,149,237)', // 矢车菊蓝
|
||||
'gpt-4-1106-preview': 'rgb(30,144,255)', // 道奇蓝
|
||||
'gpt-4-0125-preview': 'rgb(2,177,236)', // 深天蓝
|
||||
'gpt-4-turbo-preview': 'rgb(2,177,255)', // 深天蓝
|
||||
'gpt-4-32k': 'rgb(104,111,238)', // 中紫色
|
||||
// 'gpt-4-32k-0314': 'rgb(90,105,205)', // 暗灰蓝色
|
||||
'gpt-4-32k-0613': 'rgb(61,71,139)', // 暗蓝灰色
|
||||
'gpt-4-all': 'rgb(65,105,225)', // 皇家蓝
|
||||
'gpt-4-gizmo-*': 'rgb(0,0,255)', // 纯蓝色
|
||||
'gpt-4-vision-preview': 'rgb(25,25,112)', // 午夜蓝
|
||||
'text-ada-001': 'rgb(255,192,203)', // 粉红色
|
||||
'text-babbage-001': 'rgb(255,160,122)', // 浅珊瑚色
|
||||
'text-curie-001': 'rgb(219,112,147)', // 苍紫罗兰色
|
||||
// 'text-davinci-002': 'rgb(199,21,133)', // 中紫罗兰红色
|
||||
'text-davinci-003': 'rgb(219,112,147)', // 苍紫罗兰色(与Curie相同,表示同一个系列)
|
||||
'text-davinci-edit-001': 'rgb(255,105,180)', // 热粉色
|
||||
'text-embedding-ada-002': 'rgb(255,182,193)', // 浅粉红
|
||||
'text-embedding-v1': 'rgb(255,174,185)', // 浅粉红色(略有区别)
|
||||
'text-moderation-latest': 'rgb(255,130,171)', // 强粉色
|
||||
'text-moderation-stable': 'rgb(255,160,122)', // 浅珊瑚色(与Babbage相同,表示同一类功能)
|
||||
'tts-1': 'rgb(255,140,0)', // 深橙色
|
||||
'tts-1-1106': 'rgb(255,165,0)', // 橙色
|
||||
'tts-1-hd': 'rgb(255,215,0)', // 金色
|
||||
'tts-1-hd-1106': 'rgb(255,223,0)', // 金黄色(略有区别)
|
||||
'whisper-1': 'rgb(245,245,220)', // 米色
|
||||
'claude-3-opus-20240229': 'rgb(255,132,31)', // 橙红色
|
||||
'claude-3-sonnet-20240229': 'rgb(253,135,93)', // 橙色
|
||||
'claude-3-haiku-20240307': 'rgb(255,175,146)', // 浅橙色
|
||||
'claude-2.1': 'rgb(255,209,190)', // 浅橙色(略有区别)
|
||||
};
|
||||
|
||||
export function modelToColor(modelName) {
|
||||
// 1. 如果模型在预定义的 modelColorMap 中,使用预定义颜色
|
||||
if (modelColorMap[modelName]) {
|
||||
return modelColorMap[modelName];
|
||||
}
|
||||
|
||||
// 2. 生成一个稳定的数字作为索引
|
||||
let hash = 0;
|
||||
for (let i = 0; i < modelName.length; i++) {
|
||||
hash = (hash << 5) - hash + modelName.charCodeAt(i);
|
||||
hash = hash & hash; // Convert to 32-bit integer
|
||||
}
|
||||
hash = Math.abs(hash);
|
||||
|
||||
// 3. 根据模型名称长度选择不同的色板
|
||||
const colorPalette = modelName.length > 10 ? extendedColors : baseColors;
|
||||
|
||||
// 4. 使用hash值选择颜色
|
||||
const index = hash % colorPalette.length;
|
||||
return colorPalette[index];
|
||||
}
|
||||
|
||||
export function stringToColor(str) {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
sum += str.charCodeAt(i);
|
||||
}
|
||||
let i = sum % colors.length;
|
||||
return colors[i];
|
||||
}
|
||||
|
||||
// 渲染带有模型图标的标签
|
||||
export function renderModelTag(modelName, options = {}) {
|
||||
const {
|
||||
color,
|
||||
size = 'large',
|
||||
shape = 'circle',
|
||||
onClick,
|
||||
suffixIcon,
|
||||
} = options;
|
||||
|
||||
const categories = getModelCategories(i18next.t);
|
||||
let icon = null;
|
||||
|
||||
for (const [key, category] of Object.entries(categories)) {
|
||||
if (key !== 'all' && category.filter({ model_name: modelName })) {
|
||||
icon = category.icon;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Tag
|
||||
color={color || stringToColor(modelName)}
|
||||
prefixIcon={icon}
|
||||
suffixIcon={suffixIcon}
|
||||
size={size}
|
||||
shape={shape}
|
||||
onClick={onClick}
|
||||
>
|
||||
{modelName}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderText(text, limit) {
|
||||
if (text.length > limit) {
|
||||
@@ -324,6 +797,9 @@ export function renderModelPrice(
|
||||
fileSearch = false,
|
||||
fileSearchCallCount = 0,
|
||||
fileSearchPrice = 0,
|
||||
audioInputSeperatePrice = false,
|
||||
audioInputTokens = 0,
|
||||
audioInputPrice = 0,
|
||||
) {
|
||||
if (modelPrice !== -1) {
|
||||
return i18next.t(
|
||||
@@ -351,9 +827,12 @@ export function renderModelPrice(
|
||||
effectiveInputTokens =
|
||||
inputTokens - imageOutputTokens + imageOutputTokens * imageRatio;
|
||||
}
|
||||
|
||||
if (audioInputTokens > 0) {
|
||||
effectiveInputTokens -= audioInputTokens;
|
||||
}
|
||||
let price =
|
||||
(effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
|
||||
(audioInputTokens / 1000000) * audioInputPrice * groupRatio +
|
||||
(completionTokens / 1000000) * completionRatioPrice * groupRatio +
|
||||
(webSearchCallCount / 1000) * webSearchPrice * groupRatio +
|
||||
(fileSearchCallCount / 1000) * fileSearchPrice * groupRatio;
|
||||
@@ -362,8 +841,11 @@ export function renderModelPrice(
|
||||
<>
|
||||
<article>
|
||||
<p>
|
||||
{i18next.t('输入价格:${{price}} / 1M tokens', {
|
||||
{i18next.t('输入价格:${{price}} / 1M tokens{{audioPrice}}', {
|
||||
price: inputRatioPrice,
|
||||
audioPrice: audioInputSeperatePrice
|
||||
? `,音频 $${audioInputPrice} / 1M tokens`
|
||||
: '',
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
@@ -417,96 +899,93 @@ export function renderModelPrice(
|
||||
)}
|
||||
<p></p>
|
||||
<p>
|
||||
{cacheTokens > 0 && !image && !webSearch && !fileSearch
|
||||
? i18next.t(
|
||||
'输入 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
|
||||
{(() => {
|
||||
// 构建输入部分描述
|
||||
let inputDesc = '';
|
||||
if (image && imageOutputTokens > 0) {
|
||||
inputDesc = i18next.t(
|
||||
'(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}}',
|
||||
{
|
||||
nonImageInput: inputTokens - imageOutputTokens,
|
||||
imageInput: imageOutputTokens,
|
||||
imageRatio: imageRatio,
|
||||
price: inputRatioPrice,
|
||||
},
|
||||
);
|
||||
} else if (cacheTokens > 0) {
|
||||
inputDesc = i18next.t(
|
||||
'(输入 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}}',
|
||||
{
|
||||
nonCacheInput: inputTokens - cacheTokens,
|
||||
cacheInput: cacheTokens,
|
||||
cachePrice: inputRatioPrice * cacheRatio,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
total: price.toFixed(6),
|
||||
cachePrice: cacheRatioPrice,
|
||||
},
|
||||
)
|
||||
: image && imageOutputTokens > 0 && !webSearch && !fileSearch
|
||||
? i18next.t(
|
||||
'输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
|
||||
{
|
||||
nonImageInput: inputTokens - imageOutputTokens,
|
||||
imageInput: imageOutputTokens,
|
||||
imageRatio: imageRatio,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
total: price.toFixed(6),
|
||||
},
|
||||
)
|
||||
: webSearch && webSearchCallCount > 0 && !image && !fileSearch
|
||||
);
|
||||
} else if (audioInputSeperatePrice && audioInputTokens > 0) {
|
||||
inputDesc = i18next.t(
|
||||
'(输入 {{nonAudioInput}} tokens / 1M tokens * ${{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * ${{audioPrice}}',
|
||||
{
|
||||
nonAudioInput: inputTokens - audioInputTokens,
|
||||
audioInput: audioInputTokens,
|
||||
price: inputRatioPrice,
|
||||
audioPrice: audioInputPrice,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
inputDesc = i18next.t(
|
||||
'(输入 {{input}} tokens / 1M tokens * ${{price}}',
|
||||
{
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 构建输出部分描述
|
||||
const outputDesc = i18next.t(
|
||||
'输出 {{completion}} tokens / 1M tokens * ${{compPrice}}) * 分组倍率 {{ratio}}',
|
||||
{
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
},
|
||||
);
|
||||
|
||||
// 构建额外服务描述
|
||||
const extraServices = [
|
||||
webSearch && webSearchCallCount > 0
|
||||
? i18next.t(
|
||||
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + Web搜索 {{webSearchCallCount}}次 / 1K 次 * ${{webSearchPrice}} * {{ratio}} = ${{total}}',
|
||||
' + Web搜索 {{count}}次 / 1K 次 * ${{price}} * 分组倍率 {{ratio}}',
|
||||
{
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
count: webSearchCallCount,
|
||||
price: webSearchPrice,
|
||||
ratio: groupRatio,
|
||||
webSearchCallCount,
|
||||
webSearchPrice,
|
||||
total: price.toFixed(6),
|
||||
},
|
||||
)
|
||||
: fileSearch &&
|
||||
fileSearchCallCount > 0 &&
|
||||
!image &&
|
||||
!webSearch
|
||||
? i18next.t(
|
||||
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + 文件搜索 {{fileSearchCallCount}}次 / 1K 次 * ${{fileSearchPrice}} * {{ratio}}= ${{total}}',
|
||||
{
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
fileSearchCallCount,
|
||||
fileSearchPrice,
|
||||
total: price.toFixed(6),
|
||||
},
|
||||
)
|
||||
: webSearch &&
|
||||
webSearchCallCount > 0 &&
|
||||
fileSearch &&
|
||||
fileSearchCallCount > 0 &&
|
||||
!image
|
||||
? i18next.t(
|
||||
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + Web搜索 {{webSearchCallCount}}次 / 1K 次 * ${{webSearchPrice}} * {{ratio}}+ 文件搜索 {{fileSearchCallCount}}次 / 1K 次 * ${{fileSearchPrice}} * {{ratio}}= ${{total}}',
|
||||
{
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
webSearchCallCount,
|
||||
webSearchPrice,
|
||||
fileSearchCallCount,
|
||||
fileSearchPrice,
|
||||
total: price.toFixed(6),
|
||||
},
|
||||
)
|
||||
: i18next.t(
|
||||
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
|
||||
{
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
total: price.toFixed(6),
|
||||
},
|
||||
)}
|
||||
: '',
|
||||
fileSearch && fileSearchCallCount > 0
|
||||
? i18next.t(
|
||||
' + 文件搜索 {{count}}次 / 1K 次 * ${{price}} * 分组倍率 {{ratio}}',
|
||||
{
|
||||
count: fileSearchCallCount,
|
||||
price: fileSearchPrice,
|
||||
ratio: groupRatio,
|
||||
},
|
||||
)
|
||||
: '',
|
||||
].join('');
|
||||
|
||||
return i18next.t(
|
||||
'{{inputDesc}} + {{outputDesc}}{{extraServices}} = ${{total}}',
|
||||
{
|
||||
inputDesc,
|
||||
outputDesc,
|
||||
extraServices,
|
||||
total: price.toFixed(6),
|
||||
},
|
||||
);
|
||||
})()}
|
||||
</p>
|
||||
<p>{i18next.t('仅供参考,以实际扣费为准')}</p>
|
||||
</article>
|
||||
@@ -799,137 +1278,6 @@ export function renderQuotaWithPrompt(quota, digits) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const colors = [
|
||||
'amber',
|
||||
'blue',
|
||||
'cyan',
|
||||
'green',
|
||||
'grey',
|
||||
'indigo',
|
||||
'light-blue',
|
||||
'lime',
|
||||
'orange',
|
||||
'pink',
|
||||
'purple',
|
||||
'red',
|
||||
'teal',
|
||||
'violet',
|
||||
'yellow',
|
||||
];
|
||||
|
||||
// 基础10色色板 (N ≤ 10)
|
||||
const baseColors = [
|
||||
'#1664FF', // 主色
|
||||
'#1AC6FF',
|
||||
'#FF8A00',
|
||||
'#3CC780',
|
||||
'#7442D4',
|
||||
'#FFC400',
|
||||
'#304D77',
|
||||
'#B48DEB',
|
||||
'#009488',
|
||||
'#FF7DDA',
|
||||
];
|
||||
|
||||
// 扩展20色色板 (10 < N ≤ 20)
|
||||
const extendedColors = [
|
||||
'#1664FF',
|
||||
'#B2CFFF',
|
||||
'#1AC6FF',
|
||||
'#94EFFF',
|
||||
'#FF8A00',
|
||||
'#FFCE7A',
|
||||
'#3CC780',
|
||||
'#B9EDCD',
|
||||
'#7442D4',
|
||||
'#DDC5FA',
|
||||
'#FFC400',
|
||||
'#FAE878',
|
||||
'#304D77',
|
||||
'#8B959E',
|
||||
'#B48DEB',
|
||||
'#EFE3FF',
|
||||
'#009488',
|
||||
'#59BAA8',
|
||||
'#FF7DDA',
|
||||
'#FFCFEE',
|
||||
];
|
||||
|
||||
export const modelColorMap = {
|
||||
'dall-e': 'rgb(147,112,219)', // 深紫色
|
||||
// 'dall-e-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调
|
||||
'dall-e-3': 'rgb(153,50,204)', // 介于紫罗兰和洋红之间的色调
|
||||
'gpt-3.5-turbo': 'rgb(184,227,167)', // 浅绿色
|
||||
// 'gpt-3.5-turbo-0301': 'rgb(131,220,131)', // 亮绿色
|
||||
'gpt-3.5-turbo-0613': 'rgb(60,179,113)', // 海洋绿
|
||||
'gpt-3.5-turbo-1106': 'rgb(32,178,170)', // 浅海洋绿
|
||||
'gpt-3.5-turbo-16k': 'rgb(149,252,206)', // 淡橙色
|
||||
'gpt-3.5-turbo-16k-0613': 'rgb(119,255,214)', // 淡桃
|
||||
'gpt-3.5-turbo-instruct': 'rgb(175,238,238)', // 粉蓝色
|
||||
'gpt-4': 'rgb(135,206,235)', // 天蓝色
|
||||
// 'gpt-4-0314': 'rgb(70,130,180)', // 钢蓝色
|
||||
'gpt-4-0613': 'rgb(100,149,237)', // 矢车菊蓝
|
||||
'gpt-4-1106-preview': 'rgb(30,144,255)', // 道奇蓝
|
||||
'gpt-4-0125-preview': 'rgb(2,177,236)', // 深天蓝
|
||||
'gpt-4-turbo-preview': 'rgb(2,177,255)', // 深天蓝
|
||||
'gpt-4-32k': 'rgb(104,111,238)', // 中紫色
|
||||
// 'gpt-4-32k-0314': 'rgb(90,105,205)', // 暗灰蓝色
|
||||
'gpt-4-32k-0613': 'rgb(61,71,139)', // 暗蓝灰色
|
||||
'gpt-4-all': 'rgb(65,105,225)', // 皇家蓝
|
||||
'gpt-4-gizmo-*': 'rgb(0,0,255)', // 纯蓝色
|
||||
'gpt-4-vision-preview': 'rgb(25,25,112)', // 午夜蓝
|
||||
'text-ada-001': 'rgb(255,192,203)', // 粉红色
|
||||
'text-babbage-001': 'rgb(255,160,122)', // 浅珊瑚色
|
||||
'text-curie-001': 'rgb(219,112,147)', // 苍紫罗兰色
|
||||
// 'text-davinci-002': 'rgb(199,21,133)', // 中紫罗兰红色
|
||||
'text-davinci-003': 'rgb(219,112,147)', // 苍紫罗兰色(与Curie相同,表示同一个系列)
|
||||
'text-davinci-edit-001': 'rgb(255,105,180)', // 热粉色
|
||||
'text-embedding-ada-002': 'rgb(255,182,193)', // 浅粉红
|
||||
'text-embedding-v1': 'rgb(255,174,185)', // 浅粉红色(略有区别)
|
||||
'text-moderation-latest': 'rgb(255,130,171)', // 强粉色
|
||||
'text-moderation-stable': 'rgb(255,160,122)', // 浅珊瑚色(与Babbage相同,表示同一类功能)
|
||||
'tts-1': 'rgb(255,140,0)', // 深橙色
|
||||
'tts-1-1106': 'rgb(255,165,0)', // 橙色
|
||||
'tts-1-hd': 'rgb(255,215,0)', // 金色
|
||||
'tts-1-hd-1106': 'rgb(255,223,0)', // 金黄色(略有区别)
|
||||
'whisper-1': 'rgb(245,245,220)', // 米色
|
||||
'claude-3-opus-20240229': 'rgb(255,132,31)', // 橙红色
|
||||
'claude-3-sonnet-20240229': 'rgb(253,135,93)', // 橙色
|
||||
'claude-3-haiku-20240307': 'rgb(255,175,146)', // 浅橙色
|
||||
'claude-2.1': 'rgb(255,209,190)', // 浅橙色(略有区别)
|
||||
};
|
||||
|
||||
export function modelToColor(modelName) {
|
||||
// 1. 如果模型在预定义的 modelColorMap 中,使用预定义颜色
|
||||
if (modelColorMap[modelName]) {
|
||||
return modelColorMap[modelName];
|
||||
}
|
||||
|
||||
// 2. 生成一个稳定的数字作为索引
|
||||
let hash = 0;
|
||||
for (let i = 0; i < modelName.length; i++) {
|
||||
hash = (hash << 5) - hash + modelName.charCodeAt(i);
|
||||
hash = hash & hash; // Convert to 32-bit integer
|
||||
}
|
||||
hash = Math.abs(hash);
|
||||
|
||||
// 3. 根据模型名称长度选择不同的色板
|
||||
const colorPalette = modelName.length > 10 ? extendedColors : baseColors;
|
||||
|
||||
// 4. 使用hash值选择颜色
|
||||
const index = hash % colorPalette.length;
|
||||
return colorPalette[index];
|
||||
}
|
||||
|
||||
export function stringToColor(str) {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
sum += str.charCodeAt(i);
|
||||
}
|
||||
let i = sum % colors.length;
|
||||
return colors[i];
|
||||
}
|
||||
|
||||
export function renderClaudeModelPrice(
|
||||
inputTokens,
|
||||
completionTokens,
|
||||
@@ -1128,3 +1476,83 @@ export function renderClaudeModelPriceSimple(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* rehype 插件:将段落等文本节点拆分为逐词 <span>,并添加淡入动画 class。
|
||||
* 仅在流式渲染阶段使用,避免已渲染文字重复动画。
|
||||
*/
|
||||
export function rehypeSplitWordsIntoSpans(options = {}) {
|
||||
const { previousContentLength = 0 } = options;
|
||||
|
||||
return (tree) => {
|
||||
let currentCharCount = 0; // 当前已处理的字符数
|
||||
|
||||
visit(tree, 'element', (node) => {
|
||||
if (
|
||||
['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'strong'].includes(
|
||||
node.tagName,
|
||||
) &&
|
||||
node.children
|
||||
) {
|
||||
const newChildren = [];
|
||||
node.children.forEach((child) => {
|
||||
if (child.type === 'text') {
|
||||
try {
|
||||
// 使用 Intl.Segmenter 精准拆分中英文及标点
|
||||
const segmenter = new Intl.Segmenter('zh', {
|
||||
granularity: 'word',
|
||||
});
|
||||
const segments = segmenter.segment(child.value);
|
||||
|
||||
Array.from(segments)
|
||||
.map((seg) => seg.segment)
|
||||
.filter(Boolean)
|
||||
.forEach((word) => {
|
||||
const wordStartPos = currentCharCount;
|
||||
const wordEndPos = currentCharCount + word.length;
|
||||
|
||||
// 判断这个词是否是新增的(在 previousContentLength 之后)
|
||||
const isNewContent = wordStartPos >= previousContentLength;
|
||||
|
||||
newChildren.push({
|
||||
type: 'element',
|
||||
tagName: 'span',
|
||||
properties: {
|
||||
className: isNewContent ? ['animate-fade-in'] : [],
|
||||
},
|
||||
children: [{ type: 'text', value: word }],
|
||||
});
|
||||
|
||||
currentCharCount = wordEndPos;
|
||||
});
|
||||
} catch (_) {
|
||||
// Fallback:如果浏览器不支持 Segmenter
|
||||
const textStartPos = currentCharCount;
|
||||
const isNewContent = textStartPos >= previousContentLength;
|
||||
|
||||
if (isNewContent) {
|
||||
// 新内容,添加动画
|
||||
newChildren.push({
|
||||
type: 'element',
|
||||
tagName: 'span',
|
||||
properties: {
|
||||
className: ['animate-fade-in'],
|
||||
},
|
||||
children: [{ type: 'text', value: child.value }],
|
||||
});
|
||||
} else {
|
||||
// 旧内容,不添加动画
|
||||
newChildren.push(child);
|
||||
}
|
||||
|
||||
currentCharCount += child.value.length;
|
||||
}
|
||||
} else {
|
||||
newChildren.push(child);
|
||||
}
|
||||
});
|
||||
node.children = newChildren;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { API } from './api';
|
||||
|
||||
/**
|
||||
* 获取可用的token keys
|
||||
* @returns {Promise<string[]>} 返回active状态的token key数组
|
||||
*/
|
||||
export async function fetchTokenKeys() {
|
||||
try {
|
||||
const response = await API.get('/api/token/?p=0&size=100');
|
||||
const { success, data } = response.data;
|
||||
if (success) {
|
||||
const activeTokens = data.filter((token) => token.status === 1);
|
||||
return activeTokens.map((token) => token.key);
|
||||
} else {
|
||||
throw new Error('Failed to fetch token keys');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching token keys:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器地址
|
||||
* @returns {string} 服务器地址
|
||||
*/
|
||||
export function getServerAddress() {
|
||||
let status = localStorage.getItem('status');
|
||||
let serverAddress = '';
|
||||
|
||||
if (status) {
|
||||
try {
|
||||
status = JSON.parse(status);
|
||||
serverAddress = status.server_address || '';
|
||||
} catch (error) {
|
||||
console.error('Failed to parse status from localStorage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!serverAddress) {
|
||||
serverAddress = window.location.origin;
|
||||
}
|
||||
|
||||
return serverAddress;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { Toast } from '@douyinfe/semi-ui';
|
||||
import { toastConstants } from '../constants';
|
||||
import React from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { THINK_TAG_REGEX, MESSAGE_ROLES } from '../constants/playground.constants';
|
||||
|
||||
const HTMLToastContent = ({ htmlContent }) => {
|
||||
return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
|
||||
@@ -283,3 +284,165 @@ export function compareObjects(oldObject, newObject) {
|
||||
|
||||
return changedProperties;
|
||||
}
|
||||
|
||||
// playground message
|
||||
|
||||
// 生成唯一ID
|
||||
let messageId = 4;
|
||||
export const generateMessageId = () => `${messageId++}`;
|
||||
|
||||
// 提取消息中的文本内容
|
||||
export const getTextContent = (message) => {
|
||||
if (!message || !message.content) return '';
|
||||
|
||||
if (Array.isArray(message.content)) {
|
||||
const textContent = message.content.find(item => item.type === 'text');
|
||||
return textContent?.text || '';
|
||||
}
|
||||
return typeof message.content === 'string' ? message.content : '';
|
||||
};
|
||||
|
||||
// 处理 think 标签
|
||||
export const processThinkTags = (content, reasoningContent = '') => {
|
||||
if (!content || !content.includes('<think>')) {
|
||||
return { content, reasoningContent };
|
||||
}
|
||||
|
||||
const thoughts = [];
|
||||
const replyParts = [];
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
|
||||
THINK_TAG_REGEX.lastIndex = 0;
|
||||
while ((match = THINK_TAG_REGEX.exec(content)) !== null) {
|
||||
replyParts.push(content.substring(lastIndex, match.index));
|
||||
thoughts.push(match[1]);
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
replyParts.push(content.substring(lastIndex));
|
||||
|
||||
const processedContent = replyParts.join('').replace(/<\/?think>/g, '').trim();
|
||||
const thoughtsStr = thoughts.join('\n\n---\n\n');
|
||||
const processedReasoningContent = reasoningContent && thoughtsStr
|
||||
? `${reasoningContent}\n\n---\n\n${thoughtsStr}`
|
||||
: reasoningContent || thoughtsStr;
|
||||
|
||||
return {
|
||||
content: processedContent,
|
||||
reasoningContent: processedReasoningContent
|
||||
};
|
||||
};
|
||||
|
||||
// 处理未完成的 think 标签
|
||||
export const processIncompleteThinkTags = (content, reasoningContent = '') => {
|
||||
if (!content) return { content: '', reasoningContent };
|
||||
|
||||
const lastOpenThinkIndex = content.lastIndexOf('<think>');
|
||||
if (lastOpenThinkIndex === -1) {
|
||||
return processThinkTags(content, reasoningContent);
|
||||
}
|
||||
|
||||
const fragmentAfterLastOpen = content.substring(lastOpenThinkIndex);
|
||||
if (!fragmentAfterLastOpen.includes('</think>')) {
|
||||
const unclosedThought = fragmentAfterLastOpen.substring('<think>'.length).trim();
|
||||
const cleanContent = content.substring(0, lastOpenThinkIndex);
|
||||
const processedReasoningContent = unclosedThought
|
||||
? reasoningContent ? `${reasoningContent}\n\n---\n\n${unclosedThought}` : unclosedThought
|
||||
: reasoningContent;
|
||||
|
||||
return processThinkTags(cleanContent, processedReasoningContent);
|
||||
}
|
||||
|
||||
return processThinkTags(content, reasoningContent);
|
||||
};
|
||||
|
||||
// 构建消息内容(包含图片)
|
||||
export const buildMessageContent = (textContent, imageUrls = [], imageEnabled = false) => {
|
||||
if (!textContent && (!imageUrls || imageUrls.length === 0)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const validImageUrls = imageUrls.filter(url => url && url.trim() !== '');
|
||||
|
||||
if (imageEnabled && validImageUrls.length > 0) {
|
||||
return [
|
||||
{ type: 'text', text: textContent || '' },
|
||||
...validImageUrls.map(url => ({
|
||||
type: 'image_url',
|
||||
image_url: { url: url.trim() }
|
||||
}))
|
||||
];
|
||||
}
|
||||
|
||||
return textContent || '';
|
||||
};
|
||||
|
||||
// 创建新消息
|
||||
export const createMessage = (role, content, options = {}) => ({
|
||||
role,
|
||||
content,
|
||||
createAt: Date.now(),
|
||||
id: generateMessageId(),
|
||||
...options
|
||||
});
|
||||
|
||||
// 创建加载中的助手消息
|
||||
export const createLoadingAssistantMessage = () => createMessage(
|
||||
MESSAGE_ROLES.ASSISTANT,
|
||||
'',
|
||||
{
|
||||
reasoningContent: '',
|
||||
isReasoningExpanded: true,
|
||||
isThinkingComplete: false,
|
||||
hasAutoCollapsed: false,
|
||||
status: 'loading'
|
||||
}
|
||||
);
|
||||
|
||||
// 检查消息是否包含图片
|
||||
export const hasImageContent = (message) => {
|
||||
return message &&
|
||||
Array.isArray(message.content) &&
|
||||
message.content.some(item => item.type === 'image_url');
|
||||
};
|
||||
|
||||
// 格式化消息用于API请求
|
||||
export const formatMessageForAPI = (message) => {
|
||||
if (!message) return null;
|
||||
|
||||
return {
|
||||
role: message.role,
|
||||
content: message.content
|
||||
};
|
||||
};
|
||||
|
||||
// 验证消息是否有效
|
||||
export const isValidMessage = (message) => {
|
||||
return message &&
|
||||
message.role &&
|
||||
(message.content || message.content === '');
|
||||
};
|
||||
|
||||
// 获取最后一条用户消息
|
||||
export const getLastUserMessage = (messages) => {
|
||||
if (!Array.isArray(messages)) return null;
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].role === MESSAGE_ROLES.USER) {
|
||||
return messages[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 获取最后一条助手消息
|
||||
export const getLastAssistantMessage = (messages) => {
|
||||
if (!Array.isArray(messages)) return null;
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].role === MESSAGE_ROLES.ASSISTANT) {
|
||||
return messages[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SSE } from 'sse';
|
||||
import { getUserIdFromLocalStorage } from '../helpers/index.js';
|
||||
import {
|
||||
API_ENDPOINTS,
|
||||
MESSAGE_STATUS,
|
||||
DEBUG_TABS
|
||||
} from '../utils/constants';
|
||||
import {
|
||||
buildApiPayload,
|
||||
handleApiError
|
||||
} from '../utils/apiUtils';
|
||||
} from '../constants/playground.constants';
|
||||
import {
|
||||
getUserIdFromLocalStorage,
|
||||
handleApiError,
|
||||
processThinkTags,
|
||||
processIncompleteThinkTags
|
||||
} from '../utils/messageUtils';
|
||||
} from '../helpers';
|
||||
|
||||
export const useApiRequest = (
|
||||
setMessage,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { API, showError } from '../helpers/index.js';
|
||||
import { API_ENDPOINTS } from '../utils/constants';
|
||||
import { processModelsData, processGroupsData } from '../utils/apiUtils';
|
||||
import { API, processModelsData, processGroupsData } from '../helpers';
|
||||
import { API_ENDPOINTS } from '../constants/playground.constants';
|
||||
|
||||
export const useDataLoader = (
|
||||
userState,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useCallback } from 'react';
|
||||
import { Toast, Modal } from '@douyinfe/semi-ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getTextContent } from '../utils/messageUtils';
|
||||
import { ERROR_MESSAGES } from '../utils/constants';
|
||||
import { getTextContent } from '../helpers';
|
||||
import { ERROR_MESSAGES } from '../constants/playground.constants';
|
||||
|
||||
export const useMessageActions = (message, setMessage, onMessageSend, saveMessages) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useCallback, useState, useRef } from 'react';
|
||||
import { Toast, Modal } from '@douyinfe/semi-ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getTextContent, buildApiPayload, createLoadingAssistantMessage } from '../utils/messageUtils';
|
||||
import { MESSAGE_ROLES } from '../utils/constants';
|
||||
import { getTextContent, buildApiPayload, createLoadingAssistantMessage } from '../helpers';
|
||||
import { MESSAGE_ROLES } from '../constants/playground.constants';
|
||||
|
||||
export const useMessageEdit = (
|
||||
setMessage,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { DEFAULT_MESSAGES, DEFAULT_CONFIG, DEBUG_TABS, MESSAGE_STATUS } from '../utils/constants';
|
||||
import { DEFAULT_MESSAGES, DEFAULT_CONFIG, DEBUG_TABS, MESSAGE_STATUS } from '../constants/playground.constants';
|
||||
import { loadConfig, saveConfig, loadMessages, saveMessages } from '../components/playground/configStorage';
|
||||
import { processIncompleteThinkTags } from '../utils/messageUtils';
|
||||
import { processIncompleteThinkTags } from '../helpers';
|
||||
|
||||
export const usePlaygroundState = () => {
|
||||
// 使用惰性初始化,确保只在组件首次挂载时加载配置和消息
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { MESSAGE_ROLES } from '../utils/constants';
|
||||
import { MESSAGE_ROLES } from '../constants/playground.constants';
|
||||
|
||||
export const useSyncMessageAndCustomBody = (
|
||||
customRequestMode,
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { fetchTokenKeys, getServerAddress } from '../helpers/token';
|
||||
import { showError } from '../helpers';
|
||||
|
||||
export function useTokenKeys(id) {
|
||||
const [keys, setKeys] = useState([]);
|
||||
const [serverAddress, setServerAddress] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const loadAllData = async () => {
|
||||
const fetchedKeys = await fetchTokenKeys();
|
||||
if (fetchedKeys.length === 0) {
|
||||
showError('当前没有可用的启用令牌,请确认是否有令牌处于启用状态!');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/console/token';
|
||||
}, 1500); // 延迟 1.5 秒后跳转
|
||||
}
|
||||
setKeys(fetchedKeys);
|
||||
setIsLoading(false);
|
||||
|
||||
const address = getServerAddress();
|
||||
setServerAddress(address);
|
||||
};
|
||||
|
||||
loadAllData();
|
||||
}, []);
|
||||
|
||||
return { keys, serverAddress, isLoading };
|
||||
}
|
||||
@@ -6,14 +6,15 @@
|
||||
"或": "or",
|
||||
"登 录": "Log In",
|
||||
"注 册": "Sign Up",
|
||||
"使用 邮箱 登录": "Sign in with Email",
|
||||
"使用 邮箱或用户名 登录": "Sign in with Email or Username",
|
||||
"使用 GitHub 继续": "Continue with GitHub",
|
||||
"使用 OIDC 继续": "Continue with OIDC",
|
||||
"使用 微信 继续": "Continue with WeChat",
|
||||
"使用 LinuxDO 继续": "Continue with LinuxDO",
|
||||
"使用 邮箱 注册": "Sign up with Email",
|
||||
"使用 用户名 注册": "Sign up with Username",
|
||||
"其他登录选项": "Other login options",
|
||||
"其他注册选项": "Other registration options",
|
||||
"请输入您的用户名或邮箱地址": "Please enter your username or email address",
|
||||
"请输入您的邮箱地址": "Please enter your email address",
|
||||
"请输入您的密码": "Please enter your password",
|
||||
"继续": "Continue",
|
||||
@@ -585,7 +586,7 @@
|
||||
"您正在使用默认密码!": "You are using the default password!",
|
||||
"请立刻修改默认密码!": "Please change the default password immediately!",
|
||||
"请输入用户名和密码!": "Please enter username and password!",
|
||||
"用户名/邮箱": "Username/email",
|
||||
"用户名或邮箱": "Username or email",
|
||||
"微信扫码登录": "WeChat scan code to log in",
|
||||
"刷新成功": "Refresh successful",
|
||||
"刷新失败": "Refresh failed",
|
||||
@@ -742,7 +743,6 @@
|
||||
"无效的用户单独并发限制数据": "Invalid user individual concurrency limit data",
|
||||
"未绑定": "Not bound",
|
||||
"修改绑定": "Modify binding",
|
||||
"绑定邮箱": "Bind email",
|
||||
"确认新密码": "Confirm new password",
|
||||
"历史消耗": "Consumption",
|
||||
"查看": "Check",
|
||||
@@ -1367,7 +1367,7 @@
|
||||
"提示 {{nonCacheInput}} tokens + 缓存 {{cacheInput}} tokens * {{cacheRatio}} / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}": "Prompt {{nonCacheInput}} tokens + cache {{cacheInput}} tokens * {{cacheRatio}} / 1M tokens * ${{price}} + completion {{completion}} tokens / 1M tokens * ${{compPrice}} * group {{ratio}} = ${{total}}",
|
||||
"缓存 Tokens": "Cache Tokens",
|
||||
"系统初始化": "System initialization",
|
||||
"管理员账号已经初始化过,请继续设置系统参数": "The admin account has already been initialized, please continue to set the system parameters",
|
||||
"管理员账号已经初始化过,请继续设置其他参数": "The admin account has already been initialized, please continue to set other parameters",
|
||||
"管理员账号": "Admin account",
|
||||
"请输入管理员用户名": "Please enter the admin username",
|
||||
"请输入管理员密码": "Please enter the admin password",
|
||||
@@ -1457,7 +1457,7 @@
|
||||
"管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。": "The administrator has not enabled the online recharge function, please contact the administrator to enable it or recharge with a redemption code.",
|
||||
"点击模型名称可复制": "Click the model name to copy",
|
||||
"管理您的邀请链接和收益": "Manage your invitation link and earnings",
|
||||
"模型与邀请": "Model and Invitation",
|
||||
"没有可用模型": "No available models",
|
||||
"账户绑定": "Account Binding",
|
||||
"安全设置": "Security Settings",
|
||||
"系统访问令牌": "System Access Token",
|
||||
@@ -1532,5 +1532,29 @@
|
||||
"搜索条件": "Search Conditions",
|
||||
"加载中...": "Loading...",
|
||||
"暂无公告": "No Notice",
|
||||
"操练场": "Playground"
|
||||
"操练场": "Playground",
|
||||
"欢迎使用,请完成以下设置以开始使用系统": "Welcome to use, please complete the following settings to start using the system",
|
||||
"数据库信息": "Database Information",
|
||||
"您正在使用 MySQL 数据库。MySQL 是一个可靠的关系型数据库管理系统,适合生产环境使用。": "You are using the MySQL database. MySQL is a reliable relational database management system, suitable for production environments.",
|
||||
"您正在使用 PostgreSQL 数据库。PostgreSQL 是一个功能强大的开源关系型数据库系统,提供了出色的可靠性和数据完整性,适合生产环境使用。": "You are using the PostgreSQL database. PostgreSQL is a powerful open-source relational database system that provides excellent reliability and data integrity, suitable for production environments.",
|
||||
"设置系统管理员的登录信息": "Set the login information for the system administrator",
|
||||
"选择适合您使用场景的模式": "Select the mode suitable for your usage scenario",
|
||||
"使用模式说明": "Usage mode description",
|
||||
"计费模式": "Billing mode",
|
||||
"多用户支持": "Multi-user support",
|
||||
"个人使用": "Personal use",
|
||||
"功能演示": "Function demonstration",
|
||||
"体验试用": "Experience trial",
|
||||
"默认模式": "Default Mode",
|
||||
"无需计费": "No Charge",
|
||||
"演示体验": "Demo Experience",
|
||||
"提供基础功能演示,方便用户了解系统特性。": "Provide basic feature demonstrations to help users understand the system features.",
|
||||
"适用于为多个用户提供服务的场景": "Suitable for scenarios where multiple users are provided.",
|
||||
"适用于个人使用的场景,不需要设置模型价格": "Suitable for personal use, no need to set model price.",
|
||||
"适用于展示系统功能的场景,提供基础功能演示": "Suitable for scenarios where the system functions are displayed, providing basic feature demonstrations.",
|
||||
"账户数据": "Account Data",
|
||||
"使用统计": "Usage Statistics",
|
||||
"资源消耗": "Resource Consumption",
|
||||
"性能指标": "Performance Indicators",
|
||||
"模型数据分析": "Model Data Analysis"
|
||||
}
|
||||
+307
-152
@@ -1,3 +1,4 @@
|
||||
/* ==================== Tailwind CSS 配置 ==================== */
|
||||
@layer tailwind-base, semi, tailwind-components, tailwind-utils;
|
||||
|
||||
@layer tailwind-base {
|
||||
@@ -12,6 +13,7 @@
|
||||
@tailwind utilities;
|
||||
}
|
||||
|
||||
/* ==================== 全局基础样式 ==================== */
|
||||
body {
|
||||
margin: 0;
|
||||
padding-top: 0;
|
||||
@@ -25,80 +27,6 @@ body {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#root {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#root>section>header>section>div>div>div>div.semi-navigation-header-list-outer>div.semi-navigation-list-wrapper>ul>div>a>li>span {
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 767px) {
|
||||
|
||||
#root>section>header>section>div>div>div>div.semi-navigation-footer>div>a>li {
|
||||
padding: 0 0;
|
||||
}
|
||||
|
||||
#root>section>header>section>div>div>div>div.semi-navigation-header-list-outer>div.semi-navigation-list-wrapper>ul>div>a>li {
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
#root>section>header>section>div>div>div>div.semi-navigation-footer>div:nth-child(1)>a>li {
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.semi-navigation-horizontal .semi-navigation-header {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/* 确保移动端内容可滚动 */
|
||||
.semi-layout-content {
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
overscroll-behavior-y: auto !important;
|
||||
}
|
||||
|
||||
/* 修复移动端下拉刷新 */
|
||||
body {
|
||||
overflow: visible !important;
|
||||
overscroll-behavior-y: auto !important;
|
||||
position: static !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
/* 确保内容区域在移动端可以正常滚动 */
|
||||
#root {
|
||||
overflow: visible !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.semi-table-tbody,
|
||||
.semi-table-row,
|
||||
.semi-table-row-cell {
|
||||
display: block !important;
|
||||
width: auto !important;
|
||||
padding: 2px !important;
|
||||
}
|
||||
|
||||
.semi-table-row-cell {
|
||||
border-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.semi-table-tbody>.semi-table-row {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.tableShow {
|
||||
display: revert;
|
||||
}
|
||||
|
||||
.tableHiddle {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
@@ -108,10 +36,14 @@ code {
|
||||
source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.custom-footer {
|
||||
font-size: 1.1em;
|
||||
#root {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ==================== 布局相关样式 ==================== */
|
||||
.semi-layout-content::-webkit-scrollbar,
|
||||
.semi-sider::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
@@ -134,19 +66,161 @@ code {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* ==================== 导航和侧边栏样式 ==================== */
|
||||
/* 导航项样式 */
|
||||
.semi-navigation-sub-title,
|
||||
.semi-chat-inputBox-sendButton,
|
||||
.semi-page-item,
|
||||
.semi-navigation-item,
|
||||
.semi-tag-closable,
|
||||
.semi-datepicker-range-input {
|
||||
border-radius: 9999px !important;
|
||||
}
|
||||
|
||||
.semi-navigation-item {
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
|
||||
.semi-navigation-item-icon {
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.semi-navigation-item-icon-info {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.semi-navigation-sub-title {
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.semi-navigation-item-collapsed {
|
||||
height: 44px !important;
|
||||
}
|
||||
|
||||
#root>section>header>section>div>div>div>div.semi-navigation-header-list-outer>div.semi-navigation-list-wrapper>ul>div>a>li>span {
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
/* 自定义侧边栏样式 */
|
||||
.sidebar-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
background: var(--semi-color-bg-0);
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border-right: none;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.sidebar-nav::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 侧边栏导航项样式 */
|
||||
.sidebar-nav-item {
|
||||
border-radius: 6px;
|
||||
margin: 3px 8px;
|
||||
transition: all 0.15s ease;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.sidebar-nav-item:hover {
|
||||
background-color: rgba(var(--semi-blue-0), 0.08);
|
||||
color: var(--semi-color-primary);
|
||||
}
|
||||
|
||||
.sidebar-nav-item-selected {
|
||||
background-color: rgba(var(--semi-blue-0), 0.12);
|
||||
color: var(--semi-color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 图标容器样式 */
|
||||
.sidebar-icon-container {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 10px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar-sub-icon-container {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 10px;
|
||||
margin-left: 1px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* 分割线样式 */
|
||||
.sidebar-divider {
|
||||
margin: 4px 8px;
|
||||
opacity: 0.15;
|
||||
}
|
||||
|
||||
/* 分组标签样式 */
|
||||
.sidebar-group-label {
|
||||
padding: 4px 15px 8px;
|
||||
color: var(--semi-color-text-2);
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 底部折叠按钮 */
|
||||
.sidebar-collapse-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
background-color: var(--semi-color-bg-0);
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
box-shadow: 0 -10px 10px -5px var(--semi-color-bg-0);
|
||||
backdrop-filter: blur(4px);
|
||||
border-top: 1px solid rgba(var(--semi-grey-0), 0.08);
|
||||
}
|
||||
|
||||
.sidebar-collapse-button-inner {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 9999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--semi-color-fill-0);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.semi-tabs-content {
|
||||
padding: 0 !important;
|
||||
.sidebar-collapse-icon-container {
|
||||
display: inline-block;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* 聊天 */
|
||||
/* 侧边栏区域容器 */
|
||||
.sidebar-section {
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
/* ==================== 聊天界面样式 ==================== */
|
||||
.semi-chat {
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
@@ -183,13 +257,26 @@ code {
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.semi-chat-container {
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.semi-chat-chatBox-action {
|
||||
column-gap: 0 !important;
|
||||
}
|
||||
|
||||
.semi-chat-inputBox-clearButton.semi-button .semi-icon {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
|
||||
/* 隐藏所有聊天相关区域的滚动条 */
|
||||
.semi-chat::-webkit-scrollbar,
|
||||
.semi-chat-chatBox::-webkit-scrollbar,
|
||||
.semi-chat-chatBox-wrap::-webkit-scrollbar,
|
||||
.semi-chat-chatBox-content::-webkit-scrollbar,
|
||||
.semi-chat-content::-webkit-scrollbar,
|
||||
.semi-chat-list::-webkit-scrollbar {
|
||||
.semi-chat-list::-webkit-scrollbar,
|
||||
.semi-chat-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -198,40 +285,110 @@ code {
|
||||
.semi-chat-chatBox-wrap,
|
||||
.semi-chat-chatBox-content,
|
||||
.semi-chat-content,
|
||||
.semi-chat-list {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.semi-chat-container {
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.semi-chat-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.semi-chat-list,
|
||||
.semi-chat-container {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
/* ==================== 组件特定样式 ==================== */
|
||||
/* Tabs组件样式 */
|
||||
.semi-tabs-content {
|
||||
padding: 0 !important;
|
||||
height: calc(100% - 40px) !important;
|
||||
flex: 1 !important;
|
||||
}
|
||||
|
||||
.semi-tabs-content .semi-tabs-pane {
|
||||
height: 100% !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.semi-tabs-content .semi-tabs-pane>div {
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
.tableShow {
|
||||
display: revert;
|
||||
}
|
||||
|
||||
.tableHiddle {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 页脚样式 */
|
||||
.custom-footer {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
/* ==================== 调试面板特定样式 ==================== */
|
||||
.debug-panel .semi-tabs {
|
||||
height: 100% !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.debug-panel .semi-tabs-bar {
|
||||
flex-shrink: 0 !important;
|
||||
}
|
||||
|
||||
.debug-panel .semi-tabs-content {
|
||||
flex: 1 !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
/* ==================== 滚动条样式统一管理 ==================== */
|
||||
/* 表格滚动条样式 */
|
||||
.semi-table-body::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.semi-table-body::-webkit-scrollbar-thumb {
|
||||
background: rgba(var(--semi-grey-2), 0.3);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.semi-table-body::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(var(--semi-grey-2), 0.5);
|
||||
}
|
||||
|
||||
.semi-table-body::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 侧边抽屉滚动条样式 */
|
||||
.semi-sidesheet-body::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.semi-sidesheet-body::-webkit-scrollbar-thumb {
|
||||
background: rgba(var(--semi-grey-2), 0.3);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.semi-sidesheet-body::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(var(--semi-grey-2), 0.5);
|
||||
}
|
||||
|
||||
.semi-sidesheet-body::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 隐藏模型设置区域的滚动条 */
|
||||
.model-settings-scroll::-webkit-scrollbar {
|
||||
.model-settings-scroll::-webkit-scrollbar,
|
||||
.thinking-content-scroll::-webkit-scrollbar,
|
||||
.custom-request-textarea .semi-input::-webkit-scrollbar,
|
||||
.custom-request-textarea textarea::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.model-settings-scroll {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
/* 思考内容区域滚动条样式 */
|
||||
.thinking-content-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.thinking-content-scroll {
|
||||
.model-settings-scroll,
|
||||
.thinking-content-scroll,
|
||||
.custom-request-textarea .semi-input,
|
||||
.custom-request-textarea textarea {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
@@ -255,60 +412,58 @@ code {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 隐藏请求体 JSON TextArea 的滚动条 */
|
||||
.custom-request-textarea .semi-input::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
/* ==================== 响应式/移动端样式 ==================== */
|
||||
@media only screen and (max-width: 767px) {
|
||||
#root>section>header>section>div>div>div>div.semi-navigation-footer>div>a>li {
|
||||
padding: 0 0;
|
||||
}
|
||||
|
||||
.custom-request-textarea .semi-input {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
#root>section>header>section>div>div>div>div.semi-navigation-header-list-outer>div.semi-navigation-list-wrapper>ul>div>a>li {
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.custom-request-textarea textarea::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
#root>section>header>section>div>div>div>div.semi-navigation-footer>div:nth-child(1)>a>li {
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.custom-request-textarea textarea {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.semi-navigation-horizontal .semi-navigation-header {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/* 调试面板标签样式 */
|
||||
.semi-tabs-content {
|
||||
height: calc(100% - 40px) !important;
|
||||
flex: 1 !important;
|
||||
}
|
||||
/* 确保移动端内容可滚动 */
|
||||
.semi-layout-content {
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
overscroll-behavior-y: auto !important;
|
||||
}
|
||||
|
||||
.semi-tabs-content .semi-tabs-pane {
|
||||
height: 100% !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
/* 修复移动端下拉刷新 */
|
||||
body {
|
||||
overflow: visible !important;
|
||||
overscroll-behavior-y: auto !important;
|
||||
position: static !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.semi-tabs-content .semi-tabs-pane>div {
|
||||
height: 100% !important;
|
||||
}
|
||||
/* 确保内容区域在移动端可以正常滚动 */
|
||||
#root {
|
||||
overflow: visible !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
/* 调试面板特定样式 */
|
||||
.debug-panel .semi-tabs {
|
||||
height: 100% !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
/* 移动端表格样式调整 */
|
||||
.semi-table-tbody,
|
||||
.semi-table-row,
|
||||
.semi-table-row-cell {
|
||||
display: block !important;
|
||||
width: auto !important;
|
||||
padding: 2px !important;
|
||||
}
|
||||
|
||||
.debug-panel .semi-tabs-bar {
|
||||
flex-shrink: 0 !important;
|
||||
}
|
||||
.semi-table-row-cell {
|
||||
border-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.debug-panel .semi-tabs-content {
|
||||
flex: 1 !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.semi-chat-chatBox-action {
|
||||
column-gap: 0 !important;
|
||||
}
|
||||
|
||||
.semi-chat-inputBox-clearButton.semi-button .semi-icon {
|
||||
font-size: 20px !important;
|
||||
.semi-table-tbody>.semi-table-row {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -7,8 +7,9 @@ import { StatusProvider } from './context/Status';
|
||||
import { Layout } from '@douyinfe/semi-ui';
|
||||
import { ThemeProvider } from './context/Theme';
|
||||
import { StyleProvider } from './context/Style/index.js';
|
||||
import PageLayout from './components/PageLayout.js';
|
||||
import PageLayout from './components/layout/PageLayout.js';
|
||||
import './i18n/i18n.js';
|
||||
import './index.css';
|
||||
|
||||
// initialization
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
Card,
|
||||
Tag,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { getChannelModels } from '../../components/utils.js';
|
||||
import { getChannelModels } from '../../helpers';
|
||||
import {
|
||||
IconSave,
|
||||
IconClose,
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
IconUser,
|
||||
IconCode,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { getChannelModels } from '../../components/utils.js';
|
||||
import { getChannelModels } from '../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import ChannelsTable from '../../components/ChannelsTable';
|
||||
import ChannelsTable from '../../components/table/ChannelsTable';
|
||||
|
||||
const File = () => {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useTokenKeys } from '../../components/fetchTokenKeys';
|
||||
import { useTokenKeys } from '../../hooks/useTokenKeys';
|
||||
import { Banner, Layout } from '@douyinfe/semi-ui';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTokenKeys } from '../../components/fetchTokenKeys';
|
||||
import { useTokenKeys } from '../../hooks/useTokenKeys';
|
||||
|
||||
const chat2page = () => {
|
||||
const { keys, chatLink, serverAddress, isLoading } = useTokenKeys();
|
||||
|
||||
+274
-96
@@ -30,14 +30,12 @@ import {
|
||||
showError,
|
||||
timestamp2string,
|
||||
timestamp2string1,
|
||||
} from '../../helpers';
|
||||
import {
|
||||
getQuotaWithUnit,
|
||||
modelColorMap,
|
||||
renderNumber,
|
||||
renderQuota,
|
||||
modelToColor,
|
||||
} from '../../helpers/render';
|
||||
modelToColor
|
||||
} from '../../helpers';
|
||||
import { UserContext } from '../../context/User/index.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -210,6 +208,55 @@ const Detail = (props) => {
|
||||
// 添加一个新的状态来存储模型-颜色映射
|
||||
const [modelColors, setModelColors] = useState({});
|
||||
|
||||
// 添加趋势数据状态
|
||||
const [trendData, setTrendData] = useState({
|
||||
balance: [],
|
||||
usedQuota: [],
|
||||
requestCount: [],
|
||||
times: [],
|
||||
consumeQuota: [],
|
||||
tokens: [],
|
||||
rpm: [],
|
||||
tpm: []
|
||||
});
|
||||
|
||||
// 迷你趋势图配置
|
||||
const getTrendSpec = (data, color) => ({
|
||||
type: 'line',
|
||||
data: [{ id: 'trend', values: data.map((val, idx) => ({ x: idx, y: val })) }],
|
||||
xField: 'x',
|
||||
yField: 'y',
|
||||
height: 40,
|
||||
width: 100,
|
||||
axes: [
|
||||
{
|
||||
orient: 'bottom',
|
||||
visible: false
|
||||
},
|
||||
{
|
||||
orient: 'left',
|
||||
visible: false
|
||||
}
|
||||
],
|
||||
padding: 0,
|
||||
autoFit: false,
|
||||
legends: { visible: false },
|
||||
tooltip: { visible: false },
|
||||
crosshair: { visible: false },
|
||||
line: {
|
||||
style: {
|
||||
stroke: color,
|
||||
lineWidth: 2
|
||||
}
|
||||
},
|
||||
point: {
|
||||
visible: false
|
||||
},
|
||||
background: {
|
||||
fill: 'transparent'
|
||||
}
|
||||
});
|
||||
|
||||
// 显示搜索Modal
|
||||
const showSearchModal = () => {
|
||||
setSearchModalVisible(true);
|
||||
@@ -284,12 +331,75 @@ const Detail = (props) => {
|
||||
let uniqueModels = new Set();
|
||||
let totalTokens = 0;
|
||||
|
||||
// 收集所有唯一的模型名称
|
||||
// 趋势数据处理
|
||||
let timePoints = [];
|
||||
let timeQuotaMap = new Map();
|
||||
let timeTokensMap = new Map();
|
||||
let timeCountMap = new Map();
|
||||
|
||||
// 收集所有唯一的模型名称和时间点
|
||||
data.forEach((item) => {
|
||||
uniqueModels.add(item.model_name);
|
||||
totalTokens += item.token_used;
|
||||
totalQuota += item.quota;
|
||||
totalTimes += item.count;
|
||||
|
||||
// 记录时间点
|
||||
const timeKey = timestamp2string1(item.created_at, dataExportDefaultTime);
|
||||
if (!timePoints.includes(timeKey)) {
|
||||
timePoints.push(timeKey);
|
||||
}
|
||||
|
||||
// 按时间点累加数据
|
||||
if (!timeQuotaMap.has(timeKey)) {
|
||||
timeQuotaMap.set(timeKey, 0);
|
||||
timeTokensMap.set(timeKey, 0);
|
||||
timeCountMap.set(timeKey, 0);
|
||||
}
|
||||
timeQuotaMap.set(timeKey, timeQuotaMap.get(timeKey) + item.quota);
|
||||
timeTokensMap.set(timeKey, timeTokensMap.get(timeKey) + item.token_used);
|
||||
timeCountMap.set(timeKey, timeCountMap.get(timeKey) + item.count);
|
||||
});
|
||||
|
||||
// 确保时间点有序
|
||||
timePoints.sort();
|
||||
|
||||
// 生成趋势数据
|
||||
const quotaTrend = timePoints.map(time => timeQuotaMap.get(time) || 0);
|
||||
const tokensTrend = timePoints.map(time => timeTokensMap.get(time) || 0);
|
||||
const countTrend = timePoints.map(time => timeCountMap.get(time) || 0);
|
||||
|
||||
// 计算RPM和TPM趋势
|
||||
const rpmTrend = [];
|
||||
const tpmTrend = [];
|
||||
|
||||
if (timePoints.length >= 2) {
|
||||
const interval = dataExportDefaultTime === 'hour'
|
||||
? 60 // 分钟/小时
|
||||
: dataExportDefaultTime === 'day'
|
||||
? 1440 // 分钟/天
|
||||
: 10080; // 分钟/周
|
||||
|
||||
for (let i = 0; i < timePoints.length; i++) {
|
||||
rpmTrend.push(timeCountMap.get(timePoints[i]) / interval);
|
||||
tpmTrend.push(timeTokensMap.get(timePoints[i]) / interval);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新趋势数据状态
|
||||
setTrendData({
|
||||
// 账户数据不在API返回中,保持空数组
|
||||
balance: [],
|
||||
usedQuota: [],
|
||||
// 使用统计
|
||||
requestCount: [], // 没有总请求次数趋势数据
|
||||
times: countTrend,
|
||||
// 资源消耗
|
||||
consumeQuota: quotaTrend,
|
||||
tokens: tokensTrend,
|
||||
// 性能指标
|
||||
rpm: rpmTrend,
|
||||
tpm: tpmTrend
|
||||
});
|
||||
|
||||
// 处理颜色映射
|
||||
@@ -338,10 +448,10 @@ const Detail = (props) => {
|
||||
}));
|
||||
|
||||
// 生成时间点序列
|
||||
let timePoints = Array.from(
|
||||
let chartTimePoints = Array.from(
|
||||
new Set([...aggregatedData.values()].map((d) => d.time)),
|
||||
);
|
||||
if (timePoints.length < 7) {
|
||||
if (chartTimePoints.length < 7) {
|
||||
const lastTime = Math.max(...data.map((item) => item.created_at));
|
||||
const interval =
|
||||
dataExportDefaultTime === 'hour'
|
||||
@@ -350,13 +460,13 @@ const Detail = (props) => {
|
||||
? 86400
|
||||
: 604800;
|
||||
|
||||
timePoints = Array.from({ length: 7 }, (_, i) =>
|
||||
chartTimePoints = Array.from({ length: 7 }, (_, i) =>
|
||||
timestamp2string1(lastTime - (6 - i) * interval, dataExportDefaultTime),
|
||||
);
|
||||
}
|
||||
|
||||
// 生成柱状图数据
|
||||
timePoints.forEach((time) => {
|
||||
chartTimePoints.forEach((time) => {
|
||||
// 为每个时间点收集所有模型的数据
|
||||
let timeData = Array.from(uniqueModels).map((model) => {
|
||||
const key = `${time}-${model}`;
|
||||
@@ -443,71 +553,103 @@ const Detail = (props) => {
|
||||
}, []);
|
||||
|
||||
// 数据卡片信息
|
||||
const statsData = [
|
||||
const groupedStatsData = [
|
||||
{
|
||||
title: t('当前余额'),
|
||||
value: renderQuota(userState?.user?.quota),
|
||||
icon: <IconMoneyExchangeStroked size="large" />,
|
||||
title: t('账户数据'),
|
||||
color: 'bg-blue-50',
|
||||
avatarColor: 'blue',
|
||||
onClick: () => navigate('/console/topup'),
|
||||
items: [
|
||||
{
|
||||
title: t('当前余额'),
|
||||
value: renderQuota(userState?.user?.quota),
|
||||
icon: <IconMoneyExchangeStroked size="large" />,
|
||||
avatarColor: 'blue',
|
||||
onClick: () => navigate('/console/topup'),
|
||||
trendData: [], // 当前余额没有趋势数据
|
||||
trendColor: '#3b82f6'
|
||||
},
|
||||
{
|
||||
title: t('历史消耗'),
|
||||
value: renderQuota(userState?.user?.used_quota),
|
||||
icon: <IconHistogram size="large" />,
|
||||
avatarColor: 'purple',
|
||||
trendData: [], // 历史消耗没有趋势数据
|
||||
trendColor: '#8b5cf6'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t('历史消耗'),
|
||||
value: renderQuota(userState?.user?.used_quota),
|
||||
icon: <IconHistogram size="large" />,
|
||||
color: 'bg-purple-50',
|
||||
avatarColor: 'purple',
|
||||
},
|
||||
{
|
||||
title: t('请求次数'),
|
||||
value: userState.user?.request_count,
|
||||
icon: <IconRotate size="large" />,
|
||||
title: t('使用统计'),
|
||||
color: 'bg-green-50',
|
||||
avatarColor: 'green',
|
||||
items: [
|
||||
{
|
||||
title: t('请求次数'),
|
||||
value: userState.user?.request_count,
|
||||
icon: <IconRotate size="large" />,
|
||||
avatarColor: 'green',
|
||||
trendData: [], // 请求次数没有趋势数据
|
||||
trendColor: '#10b981'
|
||||
},
|
||||
{
|
||||
title: t('统计次数'),
|
||||
value: times,
|
||||
icon: <IconPulse size="large" />,
|
||||
avatarColor: 'cyan',
|
||||
trendData: trendData.times,
|
||||
trendColor: '#06b6d4'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t('统计额度'),
|
||||
value: renderQuota(consumeQuota),
|
||||
icon: <IconCoinMoneyStroked size="large" />,
|
||||
title: t('资源消耗'),
|
||||
color: 'bg-yellow-50',
|
||||
avatarColor: 'yellow',
|
||||
items: [
|
||||
{
|
||||
title: t('统计额度'),
|
||||
value: renderQuota(consumeQuota),
|
||||
icon: <IconCoinMoneyStroked size="large" />,
|
||||
avatarColor: 'yellow',
|
||||
trendData: trendData.consumeQuota,
|
||||
trendColor: '#f59e0b'
|
||||
},
|
||||
{
|
||||
title: t('统计Tokens'),
|
||||
value: isNaN(consumeTokens) ? 0 : consumeTokens,
|
||||
icon: <IconTextStroked size="large" />,
|
||||
avatarColor: 'pink',
|
||||
trendData: trendData.tokens,
|
||||
trendColor: '#ec4899'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t('统计Tokens'),
|
||||
value: isNaN(consumeTokens) ? 0 : consumeTokens,
|
||||
icon: <IconTextStroked size="large" />,
|
||||
color: 'bg-pink-50',
|
||||
avatarColor: 'pink',
|
||||
},
|
||||
{
|
||||
title: t('统计次数'),
|
||||
value: times,
|
||||
icon: <IconPulse size="large" />,
|
||||
color: 'bg-teal-50',
|
||||
avatarColor: 'cyan',
|
||||
},
|
||||
{
|
||||
title: t('平均RPM'),
|
||||
value: (
|
||||
times /
|
||||
((Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000)
|
||||
).toFixed(3),
|
||||
icon: <IconStopwatchStroked size="large" />,
|
||||
title: t('性能指标'),
|
||||
color: 'bg-indigo-50',
|
||||
avatarColor: 'indigo',
|
||||
},
|
||||
{
|
||||
title: t('平均TPM'),
|
||||
value: (() => {
|
||||
const tpm = consumeTokens /
|
||||
((Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000);
|
||||
return isNaN(tpm) ? '0' : tpm.toFixed(3);
|
||||
})(),
|
||||
icon: <IconTypograph size="large" />,
|
||||
color: 'bg-orange-50',
|
||||
avatarColor: 'orange',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
title: t('平均RPM'),
|
||||
value: (
|
||||
times /
|
||||
((Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000)
|
||||
).toFixed(3),
|
||||
icon: <IconStopwatchStroked size="large" />,
|
||||
avatarColor: 'indigo',
|
||||
trendData: trendData.rpm,
|
||||
trendColor: '#6366f1'
|
||||
},
|
||||
{
|
||||
title: t('平均TPM'),
|
||||
value: (() => {
|
||||
const tpm = consumeTokens /
|
||||
((Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000);
|
||||
return isNaN(tpm) ? '0' : tpm.toFixed(3);
|
||||
})(),
|
||||
icon: <IconTypograph size="large" />,
|
||||
avatarColor: 'orange',
|
||||
trendData: trendData.tpm,
|
||||
trendColor: '#f97316'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// 获取问候语
|
||||
@@ -614,48 +756,84 @@ const Detail = (props) => {
|
||||
<Spin spinning={loading}>
|
||||
<div className="mb-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{statsData.map((stat, idx) => (
|
||||
{groupedStatsData.map((group, idx) => (
|
||||
<Card
|
||||
key={idx}
|
||||
shadows='hover'
|
||||
className={`${stat.color} border-0 !rounded-2xl w-full`}
|
||||
headerLine={false}
|
||||
onClick={stat.onClick}
|
||||
shadows='always'
|
||||
bordered={false}
|
||||
className={`${group.color} border-0 !rounded-2xl w-full`}
|
||||
headerLine={true}
|
||||
header={<div style={{ color: 'white', fontWeight: 'bold', fontSize: '16px' }}>{group.title}</div>}
|
||||
headerStyle={{
|
||||
background: idx === 0
|
||||
? 'linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%)'
|
||||
: idx === 1
|
||||
? 'linear-gradient(135deg, #10b981 0%, #34d399 100%)'
|
||||
: idx === 2
|
||||
? 'linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%)'
|
||||
: 'linear-gradient(135deg, #ec4899 0%, #f472b6 100%)',
|
||||
borderTopLeftRadius: '16px',
|
||||
borderTopRightRadius: '16px',
|
||||
padding: '12px 16px',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Avatar
|
||||
className="mr-3"
|
||||
size="medium"
|
||||
color={stat.avatarColor}
|
||||
>
|
||||
{stat.icon}
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="text-sm text-gray-500">{stat.title}</div>
|
||||
<div className="text-xl font-semibold">{stat.value}</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{group.items.map((item, itemIdx) => (
|
||||
<div
|
||||
key={itemIdx}
|
||||
className="flex items-center justify-between cursor-pointer"
|
||||
onClick={item.onClick}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Avatar
|
||||
className="mr-3"
|
||||
size="small"
|
||||
color={item.avatarColor}
|
||||
>
|
||||
{item.icon}
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="text-xs text-gray-500">{item.title}</div>
|
||||
<div className="text-lg font-semibold">{item.value}</div>
|
||||
</div>
|
||||
</div>
|
||||
{item.trendData && item.trendData.length > 0 && (
|
||||
<div className="w-24 h-10">
|
||||
<VChart
|
||||
spec={getTrendSpec(item.trendData, item.trendColor)}
|
||||
option={{ mode: 'desktop-browser' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<Card shadows='hover' className="shadow-sm !rounded-2xl" headerLine={true} title={t('模型消耗分布')}>
|
||||
<div style={{ height: 400 }}>
|
||||
<VChart
|
||||
spec={spec_line}
|
||||
option={{ mode: 'desktop-browser' }}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card shadows='hover' className="shadow-sm !rounded-2xl" headerLine={true} title={t('模型调用次数占比')}>
|
||||
<div style={{ height: 400 }}>
|
||||
<VChart
|
||||
spec={spec_pie}
|
||||
option={{ mode: 'desktop-browser' }}
|
||||
/>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-1 gap-6 mb-6">
|
||||
<Card
|
||||
shadows='always'
|
||||
bordered={false}
|
||||
className="shadow-sm !rounded-2xl"
|
||||
headerLine={true}
|
||||
title={t('模型数据分析')}
|
||||
>
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
||||
<div style={{ height: 400 }}>
|
||||
<VChart
|
||||
spec={spec_line}
|
||||
option={{ mode: 'desktop-browser' }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ height: 400 }}>
|
||||
<VChart
|
||||
spec={spec_pie}
|
||||
option={{ mode: 'desktop-browser' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -5,9 +5,9 @@ import { StatusContext } from '../../context/Status';
|
||||
import { marked } from 'marked';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IconGithubLogo } from '@douyinfe/semi-icons';
|
||||
import exampleImage from '../../images/example.png';
|
||||
import exampleImage from '/example.png';
|
||||
import { Link } from 'react-router-dom';
|
||||
import NoticeModal from '../../components/NoticeModal';
|
||||
import NoticeModal from '../../components/layout/NoticeModal';
|
||||
import { Moonshot, OpenAI, XAI, Zhipu, Volcengine, Cohere, Claude, Gemini, Suno, Minimax, Wenxin, Spark, Qingyan, DeepSeek, Qwen, Midjourney, Grok, AzureAI, Hunyuan, Xinference } from '@lobehub/icons';
|
||||
|
||||
const { Text } = Typography;
|
||||
@@ -22,11 +22,23 @@ const Home = () => {
|
||||
const isDemoSiteMode = statusState?.status?.demo_site_enabled || false;
|
||||
|
||||
useEffect(() => {
|
||||
const lastCloseDate = localStorage.getItem('notice_close_date');
|
||||
const today = new Date().toDateString();
|
||||
if (lastCloseDate !== today) {
|
||||
setNoticeVisible(true);
|
||||
}
|
||||
const checkNoticeAndShow = async () => {
|
||||
const lastCloseDate = localStorage.getItem('notice_close_date');
|
||||
const today = new Date().toDateString();
|
||||
if (lastCloseDate !== today) {
|
||||
try {
|
||||
const res = await API.get('/api/notice');
|
||||
const { success, data } = res.data;
|
||||
if (success && data && data.trim() !== '') {
|
||||
setNoticeVisible(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取公告失败:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkNoticeAndShow();
|
||||
}, []);
|
||||
|
||||
const displayHomePageContent = async () => {
|
||||
@@ -79,7 +91,7 @@ const Home = () => {
|
||||
<div className="flex-shrink-0 w-full md:w-[480px] md:mr-[60px] lg:mr-[120px] mb-8 md:mb-0">
|
||||
<div className="flex items-center gap-2 justify-center md:justify-start">
|
||||
<h1 className="text-3xl md:text-4xl lg:text-5xl font-semibold text-semi-color-text-0 w-auto leading-normal md:leading-[67px]">
|
||||
{statusState?.status?.system_name}
|
||||
{statusState?.status?.system_name || 'New API'}
|
||||
</h1>
|
||||
{statusState?.status?.version && (
|
||||
<Tag color='light-blue' size='large' shape='circle' className="ml-1">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import LogsTable from '../../components/LogsTable';
|
||||
import LogsTable from '../../components/table/LogsTable';
|
||||
|
||||
const Token = () => (
|
||||
<>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import MjLogsTable from '../../components/MjLogsTable';
|
||||
import MjLogsTable from '../../components/table/MjLogsTable';
|
||||
|
||||
const Midjourney = () => (
|
||||
<>
|
||||
|
||||
@@ -7,9 +7,7 @@ import { Layout, Toast, Modal } from '@douyinfe/semi-ui';
|
||||
import { UserContext } from '../../context/User/index.js';
|
||||
import { useStyle, styleActions } from '../../context/Style/index.js';
|
||||
|
||||
// Utils and hooks
|
||||
import { getLogo } from '../../helpers/index.js';
|
||||
import { stringToColor } from '../../helpers/render.js';
|
||||
// hooks
|
||||
import { usePlaygroundState } from '../../hooks/usePlaygroundState.js';
|
||||
import { useMessageActions } from '../../hooks/useMessageActions.js';
|
||||
import { useApiRequest } from '../../hooks/useApiRequest.js';
|
||||
@@ -19,17 +17,18 @@ import { useDataLoader } from '../../hooks/useDataLoader.js';
|
||||
|
||||
// Constants and utils
|
||||
import {
|
||||
DEFAULT_MESSAGES,
|
||||
MESSAGE_ROLES,
|
||||
ERROR_MESSAGES
|
||||
} from '../../utils/constants.js';
|
||||
} from '../../constants/playground.constants.js';
|
||||
import {
|
||||
getLogo,
|
||||
stringToColor,
|
||||
buildMessageContent,
|
||||
createMessage,
|
||||
createLoadingAssistantMessage,
|
||||
getTextContent,
|
||||
buildApiPayload
|
||||
} from '../../utils/messageUtils.js';
|
||||
} from '../../helpers';
|
||||
|
||||
// Components
|
||||
import {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import ModelPricing from '../../components/ModelPricing.js';
|
||||
import ModelPricing from '../../components/table/ModelPricing.js';
|
||||
|
||||
const Pricing = () => (
|
||||
<>
|
||||
|
||||
@@ -6,11 +6,9 @@ import {
|
||||
isMobile,
|
||||
showError,
|
||||
showSuccess,
|
||||
} from '../../helpers';
|
||||
import {
|
||||
renderQuota,
|
||||
renderQuotaWithPrompt,
|
||||
} from '../../helpers/render';
|
||||
renderQuotaWithPrompt
|
||||
} from '../../helpers';
|
||||
import {
|
||||
AutoComplete,
|
||||
Button,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import RedemptionsTable from '../../components/RedemptionsTable';
|
||||
import RedemptionsTable from '../../components/table/RedemptionsTable';
|
||||
|
||||
const Redemption = () => {
|
||||
return (
|
||||
|
||||
@@ -17,8 +17,7 @@ import {
|
||||
IconSave,
|
||||
IconBolt,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { showError, showSuccess } from '../../../helpers';
|
||||
import { API } from '../../../helpers';
|
||||
import { API, showError, showSuccess } from '../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function ModelRatioNotSetEditor(props) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ModelSettingsVisualEditor.js
|
||||
import React, { useContext, useEffect, useState, useRef } from 'react';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Button,
|
||||
@@ -8,9 +8,7 @@ import {
|
||||
Form,
|
||||
Space,
|
||||
RadioGroup,
|
||||
Radio,
|
||||
Tabs,
|
||||
TabPane,
|
||||
Radio
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IconDelete,
|
||||
@@ -19,11 +17,8 @@ import {
|
||||
IconSave,
|
||||
IconEdit,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { showError, showSuccess } from '../../../helpers';
|
||||
import { API } from '../../../helpers';
|
||||
import { API, showError, showSuccess, getQuotaPerUnit } from '../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StatusContext } from '../../../context/Status/index.js';
|
||||
import { getQuotaPerUnit } from '../../../helpers/render.js';
|
||||
|
||||
export default function ModelSettingsVisualEditor(props) {
|
||||
const { t } = useTranslation();
|
||||
@@ -304,11 +299,11 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
prev.map((model, index) =>
|
||||
index === existingModelIndex
|
||||
? {
|
||||
name: values.name,
|
||||
price: values.price || '',
|
||||
ratio: values.ratio || '',
|
||||
completionRatio: values.completionRatio || '',
|
||||
}
|
||||
name: values.name,
|
||||
price: values.price || '',
|
||||
ratio: values.ratio || '',
|
||||
completionRatio: values.completionRatio || '',
|
||||
}
|
||||
: model,
|
||||
),
|
||||
);
|
||||
@@ -456,8 +451,8 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
<Modal
|
||||
title={
|
||||
currentModel &&
|
||||
currentModel.name &&
|
||||
models.some((model) => model.name === currentModel.name)
|
||||
currentModel.name &&
|
||||
models.some((model) => model.name === currentModel.name)
|
||||
? t('编辑模型')
|
||||
: t('添加模型')
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@ import { Layout, TabPane, Tabs } from '@douyinfe/semi-ui';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import SystemSetting from '../../components/SystemSetting';
|
||||
import SystemSetting from '../../components/settings/SystemSetting.js';
|
||||
import { isRoot } from '../../helpers';
|
||||
import OtherSetting from '../../components/OtherSetting';
|
||||
import PersonalSetting from '../../components/PersonalSetting';
|
||||
import OperationSetting from '../../components/OperationSetting';
|
||||
import RateLimitSetting from '../../components/RateLimitSetting.js';
|
||||
import ModelSetting from '../../components/ModelSetting.js';
|
||||
import OtherSetting from '../../components/settings/OtherSetting';
|
||||
import PersonalSetting from '../../components/settings/PersonalSetting.js';
|
||||
import OperationSetting from '../../components/settings/OperationSetting.js';
|
||||
import RateLimitSetting from '../../components/settings/RateLimitSetting.js';
|
||||
import ModelSetting from '../../components/settings/ModelSetting.js';
|
||||
|
||||
const Setting = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
+433
-145
@@ -6,14 +6,20 @@ import {
|
||||
Typography,
|
||||
Modal,
|
||||
Banner,
|
||||
Layout,
|
||||
Tag,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { API, showError, showNotice } from '../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
IconHelpCircle,
|
||||
IconInfoCircle,
|
||||
IconAlertTriangle,
|
||||
IconUser,
|
||||
IconLock,
|
||||
IconSetting,
|
||||
IconCheckCircleStroked,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { Shield, Rocket, FlaskConical, Database, Layers } from 'lucide-react';
|
||||
|
||||
const Setup = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -127,163 +133,445 @@ const Setup = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
|
||||
<Card>
|
||||
<Title heading={2} style={{ marginBottom: '24px' }}>
|
||||
{t('系统初始化')}
|
||||
</Title>
|
||||
|
||||
{setupStatus.database_type === 'sqlite' && (
|
||||
<Banner
|
||||
type='warning'
|
||||
icon={<IconAlertTriangle size='large' />}
|
||||
closeIcon={null}
|
||||
title={t('数据库警告')}
|
||||
description={
|
||||
<div>
|
||||
<p>
|
||||
{t(
|
||||
'您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!',
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{t(
|
||||
'建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。',
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
style={{ marginBottom: '24px' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Form
|
||||
getFormApi={(formApi) => {
|
||||
formRef.current = formApi;
|
||||
console.log('Form API set:', formApi);
|
||||
}}
|
||||
initValues={formData}
|
||||
>
|
||||
{setupStatus.root_init ? (
|
||||
<Banner
|
||||
type='info'
|
||||
icon={<IconInfoCircle />}
|
||||
closeIcon={null}
|
||||
description={t('管理员账号已经初始化过,请继续设置系统参数')}
|
||||
style={{ marginBottom: '24px' }}
|
||||
/>
|
||||
) : (
|
||||
<Form.Section text={t('管理员账号')}>
|
||||
<Form.Input
|
||||
field='username'
|
||||
label={t('用户名')}
|
||||
placeholder={t('请输入管理员用户名')}
|
||||
showClear
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, username: value })
|
||||
}
|
||||
/>
|
||||
<Form.Input
|
||||
field='password'
|
||||
label={t('密码')}
|
||||
placeholder={t('请输入管理员密码')}
|
||||
type='password'
|
||||
showClear
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, password: value })
|
||||
}
|
||||
/>
|
||||
<Form.Input
|
||||
field='confirmPassword'
|
||||
label={t('确认密码')}
|
||||
placeholder={t('请确认管理员密码')}
|
||||
type='password'
|
||||
showClear
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, confirmPassword: value })
|
||||
}
|
||||
/>
|
||||
</Form.Section>
|
||||
)}
|
||||
|
||||
<Form.Section
|
||||
text={
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
{t('系统设置')}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Form.RadioGroup
|
||||
field='usageMode'
|
||||
label={
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
{t('使用模式')}
|
||||
<IconHelpCircle
|
||||
style={{
|
||||
marginLeft: '4px',
|
||||
color: 'var(--semi-color-primary)',
|
||||
verticalAlign: 'middle',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
// e.preventDefault();
|
||||
// e.stopPropagation();
|
||||
setUsageModeInfoVisible(true);
|
||||
}}
|
||||
/>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Layout>
|
||||
<Layout.Content>
|
||||
<div className="flex justify-center px-4 py-8">
|
||||
<div className="w-full max-w-3xl">
|
||||
{/* 主卡片容器 */}
|
||||
<Card className="!rounded-2xl shadow-lg border-0">
|
||||
{/* 顶部装饰性区域 */}
|
||||
<Card
|
||||
className="!rounded-2xl !border-0 !shadow-2xl overflow-hidden mb-6"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #f97316 0%, #f59e0b 25%, #f43f5e 50%, #ec4899 75%, #e879f9 100%)',
|
||||
position: 'relative'
|
||||
}}
|
||||
bodyStyle={{ padding: 0 }}
|
||||
>
|
||||
{/* 装饰性背景元素 */}
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
<div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-10 rounded-full"></div>
|
||||
<div className="absolute -bottom-16 -left-16 w-48 h-48 bg-white opacity-5 rounded-full"></div>
|
||||
<div className="absolute top-1/2 right-1/4 w-24 h-24 bg-yellow-400 opacity-10 rounded-full"></div>
|
||||
</div>
|
||||
}
|
||||
extraText={t('可在初始化后修改')}
|
||||
initValue='external'
|
||||
onChange={handleUsageModeChange}
|
||||
>
|
||||
<Form.Radio value='external'>{t('对外运营模式')}</Form.Radio>
|
||||
<Form.Radio value='self'>{t('自用模式')}</Form.Radio>
|
||||
<Form.Radio value='demo'>{t('演示站点模式')}</Form.Radio>
|
||||
</Form.RadioGroup>
|
||||
</Form.Section>
|
||||
</Form>
|
||||
|
||||
<div style={{ marginTop: '24px', textAlign: 'right' }}>
|
||||
<Button type='primary' onClick={onSubmit} loading={loading}>
|
||||
{t('初始化系统')}
|
||||
</Button>
|
||||
<div className="relative py-5 px-6 flex items-center" style={{ color: 'white' }}>
|
||||
<div className="w-14 h-14 rounded-full bg-white bg-opacity-20 flex items-center justify-center mr-5 shadow-lg flex-shrink-0">
|
||||
<IconSetting size="large" style={{ color: 'white' }} />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<Title heading={3} style={{ color: 'white', marginBottom: '2px' }}>
|
||||
{t('系统初始化')}
|
||||
</Title>
|
||||
<Text style={{ color: 'rgba(255, 255, 255, 0.9)', fontSize: '15px' }}>
|
||||
{t('欢迎使用,请完成以下设置以开始使用系统')}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
{/* 数据库警告 */}
|
||||
{setupStatus.database_type === 'sqlite' && (
|
||||
<div className="px-4">
|
||||
<Banner
|
||||
type='warning'
|
||||
icon={
|
||||
<div className="w-12 h-12 rounded-lg bg-orange-50 flex items-center justify-center">
|
||||
<Database size={22} className="text-orange-500" />
|
||||
</div>
|
||||
}
|
||||
closeIcon={null}
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<span className="font-medium">{t('数据库警告')}</span>
|
||||
<Tag color='orange' size='small' className="ml-2 !rounded-full">
|
||||
SQLite
|
||||
</Tag>
|
||||
</div>
|
||||
}
|
||||
description={
|
||||
<div>
|
||||
<p>
|
||||
{t(
|
||||
'您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!',
|
||||
)}
|
||||
</p>
|
||||
<p className="mt-1">
|
||||
<strong>{t(
|
||||
'建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。',
|
||||
)}</strong>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
className="!rounded-xl mb-6"
|
||||
fullMode={false}
|
||||
bordered
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* MySQL数据库提示 */}
|
||||
{setupStatus.database_type === 'mysql' && (
|
||||
<div className="px-4">
|
||||
<Banner
|
||||
type='info'
|
||||
icon={
|
||||
<div className="w-12 h-12 rounded-lg bg-blue-50 flex items-center justify-center">
|
||||
<Database size={22} className="text-blue-500" />
|
||||
</div>
|
||||
}
|
||||
closeIcon={null}
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<span className="font-medium">{t('数据库信息')}</span>
|
||||
<Tag color='blue' size='small' className="ml-2 !rounded-full">
|
||||
MySQL
|
||||
</Tag>
|
||||
</div>
|
||||
}
|
||||
description={
|
||||
<div>
|
||||
<p>
|
||||
{t(
|
||||
'您正在使用 MySQL 数据库。MySQL 是一个可靠的关系型数据库管理系统,适合生产环境使用。',
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
className="!rounded-xl mb-6"
|
||||
fullMode={false}
|
||||
bordered
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* PostgreSQL数据库提示 */}
|
||||
{setupStatus.database_type === 'postgres' && (
|
||||
<div className="px-4">
|
||||
<Banner
|
||||
type='success'
|
||||
icon={
|
||||
<div className="w-12 h-12 rounded-lg bg-green-50 flex items-center justify-center">
|
||||
<Database size={22} className="text-green-500" />
|
||||
</div>
|
||||
}
|
||||
closeIcon={null}
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<span className="font-medium">{t('数据库信息')}</span>
|
||||
<Tag color='green' size='small' className="ml-2 !rounded-full">
|
||||
PostgreSQL
|
||||
</Tag>
|
||||
</div>
|
||||
}
|
||||
description={
|
||||
<div>
|
||||
<p>
|
||||
{t(
|
||||
'您正在使用 PostgreSQL 数据库。PostgreSQL 是一个功能强大的开源关系型数据库系统,提供了出色的可靠性和数据完整性,适合生产环境使用。',
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
className="!rounded-xl mb-6"
|
||||
fullMode={false}
|
||||
bordered
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 主内容区域 */}
|
||||
<Form
|
||||
getFormApi={(formApi) => {
|
||||
formRef.current = formApi;
|
||||
console.log('Form API set:', formApi);
|
||||
}}
|
||||
initValues={formData}
|
||||
>
|
||||
{/* 管理员账号设置 */}
|
||||
<Card className="!rounded-2xl shadow-sm border-0 mb-6">
|
||||
<div className="flex items-center mb-4 p-6 rounded-xl" style={{
|
||||
background: 'linear-gradient(135deg, #1e3a8a 0%, #2563eb 50%, #3b82f6 100%)',
|
||||
position: 'relative'
|
||||
}}>
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
<div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
|
||||
<div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
|
||||
<IconUser size="large" style={{ color: '#ffffff' }} />
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('管理员账号')}</Text>
|
||||
<div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('设置系统管理员的登录信息')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{setupStatus.root_init ? (
|
||||
<>
|
||||
<Banner
|
||||
type='info'
|
||||
icon={
|
||||
<div className="w-10 h-10 rounded-full bg-blue-50 flex items-center justify-center">
|
||||
<IconCheckCircleStroked size="large" className="text-blue-500" />
|
||||
</div>
|
||||
}
|
||||
closeIcon={null}
|
||||
description={
|
||||
<div className="flex items-center">
|
||||
<span>{t('管理员账号已经初始化过,请继续设置其他参数')}</span>
|
||||
</div>
|
||||
}
|
||||
className="!rounded-lg"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Form.Input
|
||||
field='username'
|
||||
label={t('用户名')}
|
||||
placeholder={t('请输入管理员用户名')}
|
||||
prefix={<IconUser />}
|
||||
showClear
|
||||
size='large'
|
||||
className="mb-4 !rounded-lg"
|
||||
noLabel={false}
|
||||
validateStatus="default"
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, username: value })
|
||||
}
|
||||
/>
|
||||
<Form.Input
|
||||
field='password'
|
||||
label={t('密码')}
|
||||
placeholder={t('请输入管理员密码')}
|
||||
type='password'
|
||||
prefix={<IconLock />}
|
||||
showClear
|
||||
size='large'
|
||||
className="mb-4 !rounded-lg"
|
||||
noLabel={false}
|
||||
mode="password"
|
||||
validateStatus="default"
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, password: value })
|
||||
}
|
||||
/>
|
||||
<Form.Input
|
||||
field='confirmPassword'
|
||||
label={t('确认密码')}
|
||||
placeholder={t('请确认管理员密码')}
|
||||
type='password'
|
||||
prefix={<IconLock />}
|
||||
showClear
|
||||
size='large'
|
||||
className="!rounded-lg"
|
||||
noLabel={false}
|
||||
mode="password"
|
||||
validateStatus="default"
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, confirmPassword: value })
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 使用模式 */}
|
||||
<Card className="!rounded-2xl shadow-sm border-0 mb-6">
|
||||
<div className="flex items-center mb-4 p-6 rounded-xl" style={{
|
||||
background: 'linear-gradient(135deg, #4c1d95 0%, #6d28d9 50%, #7c3aed 100%)',
|
||||
position: 'relative'
|
||||
}}>
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
<div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
|
||||
<div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
|
||||
<Layers size={22} style={{ color: '#ffffff' }} />
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="flex items-center">
|
||||
<Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('使用模式')}</Text>
|
||||
<Button
|
||||
theme='borderless'
|
||||
type='tertiary'
|
||||
icon={<IconHelpCircle size="small" style={{ color: '#ffffff' }} />}
|
||||
size='small'
|
||||
onClick={() => setUsageModeInfoVisible(true)}
|
||||
className="!rounded-full"
|
||||
/>
|
||||
</div>
|
||||
<div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('选择适合您使用场景的模式')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form.RadioGroup
|
||||
field='usageMode'
|
||||
noLabel={true}
|
||||
initValue='external'
|
||||
onChange={handleUsageModeChange}
|
||||
type='pureCard'
|
||||
className="[&_.semi-radio-addon-buttonRadio-wrapper]:!rounded-xl"
|
||||
validateStatus="default"
|
||||
>
|
||||
<div className="space-y-3 mt-2">
|
||||
<Form.Radio
|
||||
value='external'
|
||||
className="!p-4 !rounded-xl hover:!bg-blue-50 transition-colors w-full"
|
||||
extra={
|
||||
<div className="flex items-start">
|
||||
<div className="w-10 h-10 rounded-full bg-blue-50 flex items-center justify-center mr-3 flex-shrink-0">
|
||||
<Rocket size={20} className="text-blue-500" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-gray-900 mb-1">{t('对外运营模式')}</div>
|
||||
<div className="text-sm text-gray-500">{t('适用于为多个用户提供服务的场景')}</div>
|
||||
<Tag color='blue' size='small' className="!rounded-full mt-2">
|
||||
{t('默认模式')}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Form.Radio
|
||||
value='self'
|
||||
className="!p-4 !rounded-xl hover:!bg-green-50 transition-colors w-full"
|
||||
extra={
|
||||
<div className="flex items-start">
|
||||
<div className="w-10 h-10 rounded-full bg-green-50 flex items-center justify-center mr-3 flex-shrink-0">
|
||||
<Shield size={20} className="text-green-500" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-gray-900 mb-1">{t('自用模式')}</div>
|
||||
<div className="text-sm text-gray-500">{t('适用于个人使用的场景,不需要设置模型价格')}</div>
|
||||
<Tag color='green' size='small' className="!rounded-full mt-2">
|
||||
{t('无需计费')}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Form.Radio
|
||||
value='demo'
|
||||
className="!p-4 !rounded-xl hover:!bg-purple-50 transition-colors w-full"
|
||||
extra={
|
||||
<div className="flex items-start">
|
||||
<div className="w-10 h-10 rounded-full bg-purple-50 flex items-center justify-center mr-3 flex-shrink-0">
|
||||
<FlaskConical size={20} className="text-purple-500" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-gray-900 mb-1">{t('演示站点模式')}</div>
|
||||
<div className="text-sm text-gray-500">{t('适用于展示系统功能的场景,提供基础功能演示')}</div>
|
||||
<Tag color='purple' size='small' className="!rounded-full mt-2">
|
||||
{t('演示体验')}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Form.RadioGroup>
|
||||
</Card>
|
||||
</Form>
|
||||
|
||||
<div className="flex justify-center mt-6">
|
||||
<Button
|
||||
type='primary'
|
||||
onClick={onSubmit}
|
||||
loading={loading}
|
||||
size='large'
|
||||
className="!rounded-lg !bg-gradient-to-r !from-orange-500 !to-pink-500 hover:!from-orange-600 hover:!to-pink-600 !border-0 !px-8"
|
||||
icon={<IconCheckCircleStroked />}
|
||||
>
|
||||
{t('初始化系统')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
|
||||
{/* 使用模式说明模态框 */}
|
||||
<Modal
|
||||
title={t('使用模式说明')}
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<IconInfoCircle className="mr-2 text-blue-500" />
|
||||
{t('使用模式说明')}
|
||||
</div>
|
||||
}
|
||||
visible={selfUseModeInfoVisible}
|
||||
onOk={() => setUsageModeInfoVisible(false)}
|
||||
onCancel={() => setUsageModeInfoVisible(false)}
|
||||
closeOnEsc={true}
|
||||
okText={t('确定')}
|
||||
okText={t('我已了解')}
|
||||
cancelText={null}
|
||||
centered={true}
|
||||
size='medium'
|
||||
className="[&_.semi-modal-body]:!p-6"
|
||||
>
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<Title heading={6}>{t('对外运营模式')}</Title>
|
||||
<p>{t('默认模式,适用于为多个用户提供服务的场景。')}</p>
|
||||
<p>
|
||||
{t(
|
||||
'此模式下,系统将计算每次调用的用量,您需要对每个模型都设置价格,如果没有设置价格,用户将无法使用该模型。',
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<Title heading={6}>{t('自用模式')}</Title>
|
||||
<p>{t('适用于个人使用的场景。')}</p>
|
||||
<p>
|
||||
{t('不需要设置模型价格,系统将弱化用量计算,您可专注于使用模型。')}
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<Title heading={6}>{t('演示站点模式')}</Title>
|
||||
<p>{t('适用于展示系统功能的场景。')}</p>
|
||||
<div className="space-y-6">
|
||||
{/* 对外运营模式 */}
|
||||
<div className="bg-blue-50 rounded-xl p-4">
|
||||
<div className="flex items-start">
|
||||
<div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center mr-3 flex-shrink-0">
|
||||
<Rocket size={20} className="text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<Title heading={6} className="text-blue-900 mb-2">{t('对外运营模式')}</Title>
|
||||
<div className="space-y-2 text-sm text-gray-700">
|
||||
<p>{t('默认模式,适用于为多个用户提供服务的场景。')}</p>
|
||||
<p>{t('此模式下,系统将计算每次调用的用量,您需要对每个模型都设置价格,如果没有设置价格,用户将无法使用该模型。')}</p>
|
||||
<div className="mt-3">
|
||||
<Tag color='blue' className="!rounded-full mr-2">{t('计费模式')}</Tag>
|
||||
<Tag color='blue' className="!rounded-full">{t('多用户支持')}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 自用模式 */}
|
||||
<div className="bg-green-50 rounded-xl p-4">
|
||||
<div className="flex items-start">
|
||||
<div className="w-10 h-10 rounded-full bg-green-100 flex items-center justify-center mr-3 flex-shrink-0">
|
||||
<Shield size={20} className="text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<Title heading={6} className="text-green-900 mb-2">{t('自用模式')}</Title>
|
||||
<div className="space-y-2 text-sm text-gray-700">
|
||||
<p>{t('适用于个人使用的场景。')}</p>
|
||||
<p>{t('不需要设置模型价格,系统将弱化用量计算,您可专注于使用模型。')}</p>
|
||||
<div className="mt-3">
|
||||
<Tag color='green' className="!rounded-full mr-2">{t('无需计费')}</Tag>
|
||||
<Tag color='green' className="!rounded-full">{t('个人使用')}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 演示站点模式 */}
|
||||
<div className="bg-purple-50 rounded-xl p-4">
|
||||
<div className="flex items-start">
|
||||
<div className="w-10 h-10 rounded-full bg-purple-100 flex items-center justify-center mr-3 flex-shrink-0">
|
||||
<FlaskConical size={20} className="text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<Title heading={6} className="text-purple-900 mb-2">{t('演示站点模式')}</Title>
|
||||
<div className="space-y-2 text-sm text-gray-700">
|
||||
<p>{t('适用于展示系统功能的场景。')}</p>
|
||||
<p>{t('提供基础功能演示,方便用户了解系统特性。')}</p>
|
||||
<div className="mt-3">
|
||||
<Tag color='purple' className="!rounded-full mr-2">{t('功能演示')}</Tag>
|
||||
<Tag color='purple' className="!rounded-full">{t('体验试用')}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import TaskLogsTable from '../../components/TaskLogsTable.js';
|
||||
import TaskLogsTable from '../../components/table/TaskLogsTable.js';
|
||||
|
||||
const Task = () => (
|
||||
<>
|
||||
|
||||
@@ -6,8 +6,9 @@ import {
|
||||
showError,
|
||||
showSuccess,
|
||||
timestamp2string,
|
||||
renderGroupOption,
|
||||
renderQuotaWithPrompt
|
||||
} from '../../helpers';
|
||||
import { renderGroupOption, renderQuotaWithPrompt } from '../../helpers/render';
|
||||
import {
|
||||
AutoComplete,
|
||||
Banner,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user