diff --git a/api/chat_main_service.go b/api/chat_main_service.go index af04fb13..278b0f4e 100644 --- a/api/chat_main_service.go +++ b/api/chat_main_service.go @@ -101,18 +101,18 @@ func (s *ChatService) CreateChatMessageSimple(ctx context.Context, sessionUuid, } chatMessage := sqlc_queries.CreateChatMessageParams{ - ChatSessionUuid: sessionUuid, - Uuid: uuid, - Role: role, - Content: content, + ChatSessionUuid: sessionUuid, + Uuid: uuid, + Role: role, + Content: content, ReasoningContent: reasoningContent, - Model: model, - UserID: userId, - CreatedBy: userId, - UpdatedBy: userId, - LlmSummary: summary, - TokenCount: int32(numTokens), - Raw: json.RawMessage([]byte("{}")), + Model: model, + UserID: userId, + CreatedBy: userId, + UpdatedBy: userId, + LlmSummary: summary, + TokenCount: int32(numTokens), + Raw: json.RawMessage([]byte("{}")), } message, err := s.q.CreateChatMessage(ctx, chatMessage) if err != nil { diff --git a/api/chat_model_handler.go b/api/chat_model_handler.go index 150a95b1..2ac15f1c 100644 --- a/api/chat_model_handler.go +++ b/api/chat_model_handler.go @@ -5,9 +5,11 @@ import ( "fmt" "net/http" "strconv" + "time" "github.com/gorilla/mux" "github.com/rotisserie/eris" + "github.com/samber/lo" "github.com/swuecho/chat_backend/sqlc_queries" ) @@ -46,8 +48,37 @@ func (h *ChatModelHandler) ListSystemChatModels(w http.ResponseWriter, r *http.R return } + latestUsageTimeOfModels, err := h.db.GetLatestUsageTimeOfModel(ctx, "30 days") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(fmt.Sprintf("Error listing chat APIs: %s", err.Error()))) + return + } + // create a map of model id to usage time + usageTimeMap := make(map[string]sqlc_queries.GetLatestUsageTimeOfModelRow) + for _, usageTime := range latestUsageTimeOfModels { + usageTimeMap[usageTime.Model] = usageTime + } + + // create a ChatModelWithUsage struct + type ChatModelWithUsage struct { + sqlc_queries.ChatModel + LastUsageTime time.Time `json:"lastUsageTime,omitempty"` + MessageCount int64 `json:"messageCount"` + } + + // merge ChatModels and usageTimeMap with pre-allocated slice + chatModelsWithUsage := lo.Map(ChatModels, func(model sqlc_queries.ChatModel, _ int) ChatModelWithUsage { + usage := usageTimeMap[model.Name] + return ChatModelWithUsage{ + ChatModel: model, + LastUsageTime: usage.LatestMessageTime, + MessageCount: usage.MessageCount, + } + }) + w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(ChatModels) + json.NewEncoder(w).Encode(chatModelsWithUsage) } func (h *ChatModelHandler) ChatModelByID(w http.ResponseWriter, r *http.Request) { diff --git a/api/llm/openai/openai.go b/api/llm/openai/openai.go index c95c9674..6c4ceb72 100644 --- a/api/llm/openai/openai.go +++ b/api/llm/openai/openai.go @@ -1,4 +1,5 @@ package openai + // code is copied from openai go to add reasoningContent field type ChatCompletionStreamChoiceDelta struct { Content string `json:"content,omitempty"` diff --git a/api/sqlc/queries/chat_message.sql b/api/sqlc/queries/chat_message.sql index 0e384ad1..99342e3b 100644 --- a/api/sqlc/queries/chat_message.sql +++ b/api/sqlc/queries/chat_message.sql @@ -144,3 +144,17 @@ JOIN chat_session cs ON (cm.chat_session_uuid = cs.uuid AND cs.user_id = cm.user WHERE cm.user_id = $1 AND cs.model = $2 AND cm.created_at >= NOW() - INTERVAL '10 minutes'; + + +-- name: GetLatestUsageTimeOfModel :many +SELECT + model, + MAX(created_at)::timestamp as latest_message_time, + COUNT(*) as message_count +FROM chat_message +WHERE + created_at >= NOW() - sqlc.arg(time_interval)::text::INTERVAL + AND is_deleted = false + AND model != '' +GROUP BY model +ORDER BY latest_message_time DESC; \ No newline at end of file diff --git a/api/sqlc_queries/chat_message.sql.go b/api/sqlc_queries/chat_message.sql.go index 36f21a0a..6347c5b2 100644 --- a/api/sqlc_queries/chat_message.sql.go +++ b/api/sqlc_queries/chat_message.sql.go @@ -8,6 +8,7 @@ package sqlc_queries import ( "context" "encoding/json" + "time" ) const createChatMessage = `-- name: CreateChatMessage :one @@ -521,6 +522,49 @@ func (q *Queries) GetLatestMessagesBySessionUUID(ctx context.Context, arg GetLat return items, nil } +const getLatestUsageTimeOfModel = `-- name: GetLatestUsageTimeOfModel :many +SELECT + model, + MAX(created_at)::timestamp as latest_message_time, + COUNT(*) as message_count +FROM chat_message +WHERE + created_at >= NOW() - $1::text::INTERVAL + AND is_deleted = false + AND model != '' +GROUP BY model +ORDER BY latest_message_time DESC +` + +type GetLatestUsageTimeOfModelRow struct { + Model string `json:"model"` + LatestMessageTime time.Time `json:"latestMessageTime"` + MessageCount int64 `json:"messageCount"` +} + +func (q *Queries) GetLatestUsageTimeOfModel(ctx context.Context, timeInterval string) ([]GetLatestUsageTimeOfModelRow, error) { + rows, err := q.db.QueryContext(ctx, getLatestUsageTimeOfModel, timeInterval) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetLatestUsageTimeOfModelRow + for rows.Next() { + var i GetLatestUsageTimeOfModelRow + if err := rows.Scan(&i.Model, &i.LatestMessageTime, &i.MessageCount); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const hasChatMessagePermission = `-- name: HasChatMessagePermission :one SELECT COUNT(*) > 0 as has_permission FROM chat_message cm diff --git a/web/package-lock.json b/web/package-lock.json index 898f4206..2f04068e 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -12,6 +12,7 @@ "@vicons/ionicons5": "^0.12.0", "@vscode/markdown-it-katex": "^1.0.3", "@vueuse/core": "^9.13.0", + "date-fns": "^2.30.0", "dot-env": "^0.0.1", "highlight.js": "^11.7.0", "html2canvas": "^1.4.1", @@ -283,9 +284,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", - "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz", + "integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -10016,9 +10017,9 @@ } }, "@babel/runtime": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", - "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz", + "integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==", "requires": { "regenerator-runtime": "^0.14.0" } diff --git a/web/package.json b/web/package.json index ac55f383..9bc054fa 100644 --- a/web/package.json +++ b/web/package.json @@ -22,6 +22,7 @@ "@vicons/ionicons5": "^0.12.0", "@vscode/markdown-it-katex": "^1.0.3", "@vueuse/core": "^9.13.0", + "date-fns": "^2.30.0", "dot-env": "^0.0.1", "highlight.js": "^11.7.0", "html2canvas": "^1.4.1", diff --git a/web/src/views/chat/components/ModelSelector.vue b/web/src/views/chat/components/ModelSelector.vue index 562c13a7..bffde1a2 100644 --- a/web/src/views/chat/components/ModelSelector.vue +++ b/web/src/views/chat/components/ModelSelector.vue @@ -1,11 +1,12 @@