From a7daa31273f01549f89085dca192c64f8f80d12f Mon Sep 17 00:00:00 2001
From: feng <1304903146@qq.com>
Date: Tue, 1 Apr 2025 17:06:48 +0800
Subject: [PATCH 1/9] perf: Resurrection Kael
---
pkg/httpd/chat.go | 292 ++++++++++++++++++----------
pkg/httpd/message.go | 11 +-
pkg/httpd/webserver.go | 6 +-
pkg/jms-sdk-go/model/account.go | 12 ++
pkg/jms-sdk-go/service/jms_asset.go | 6 +
pkg/jms-sdk-go/service/url.go | 1 +
pkg/proxy/chat.go | 170 ++++++++++++++++
pkg/srvconn/conn_openai.go | 12 +-
8 files changed, 401 insertions(+), 109 deletions(-)
create mode 100644 pkg/proxy/chat.go
diff --git a/pkg/httpd/chat.go b/pkg/httpd/chat.go
index fc2e6b6d1..9836cf941 100644
--- a/pkg/httpd/chat.go
+++ b/pkg/httpd/chat.go
@@ -1,163 +1,253 @@
package httpd
import (
+ "context"
"encoding/json"
+ "fmt"
"github.com/jumpserver/koko/pkg/common"
+ "github.com/jumpserver/koko/pkg/i18n"
+ "github.com/jumpserver/koko/pkg/logger"
+ "github.com/jumpserver/koko/pkg/proxy"
+ "github.com/jumpserver/koko/pkg/session"
"github.com/sashabaranov/go-openai"
"sync"
"time"
"github.com/jumpserver/koko/pkg/jms-sdk-go/model"
- "github.com/jumpserver/koko/pkg/logger"
"github.com/jumpserver/koko/pkg/srvconn"
)
var _ Handler = (*chat)(nil)
type chat struct {
- ws *UserWebsocket
+ ws *UserWebsocket
+ term *model.TerminalConfig
- conversationMap sync.Map
-
- termConf *model.TerminalConfig
+ // conversationMap: map[conversationID]*AIConversation
+ conversations sync.Map
}
func (h *chat) Name() string {
return ChatName
}
-func (h *chat) CleanUp() {
- h.CleanConversationMap()
-}
+func (h *chat) CleanUp() { h.cleanupAll() }
func (h *chat) CheckValidation() error {
return nil
}
func (h *chat) HandleMessage(msg *Message) {
- conversationID := msg.Id
- conversation := &AIConversation{}
-
- if conversationID == "" {
- id := common.UUID()
- conversation = &AIConversation{
- Id: id,
- Prompt: msg.Prompt,
- HistoryRecords: make([]string, 0),
- InterruptCurrentChat: false,
- }
+ if msg.Interrupt {
+ h.interrupt(msg.Id)
+ return
+ }
- // T000 Currently a websocket connection only retains one conversation
- h.CleanConversationMap()
- h.conversationMap.Store(id, conversation)
- } else {
- c, ok := h.conversationMap.Load(conversationID)
- if !ok {
- logger.Errorf("Ws[%s] conversation %s not found", h.ws.Uuid, conversationID)
- h.sendErrorMessage(conversationID, "conversation not found")
- return
+ conv, err := h.getOrCreateConversation(msg)
+ if err != nil {
+ h.sendError(msg.Id, err.Error())
+ return
+ }
+ conv.Question = msg.Data
+ conv.NewDialogue = true
+
+ go h.runChat(conv)
+}
+
+func (h *chat) getOrCreateConversation(msg *Message) (*AIConversation, error) {
+ if msg.Id != "" {
+ if v, ok := h.conversations.Load(msg.Id); ok {
+ return v.(*AIConversation), nil
}
- conversation = c.(*AIConversation)
+ return nil, fmt.Errorf("conversation %s not found", msg.Id)
}
- if msg.Interrupt {
- conversation.InterruptCurrentChat = true
- return
+ jmsSrv, err := proxy.NewChatJMSServer(
+ h.ws.user.String(), h.ws.ClientIP(),
+ h.ws.user.ID, h.ws.langCode, h.ws.apiClient, h.term,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("create JMS server: %w", err)
}
- openAIParam := &OpenAIParam{
- AuthToken: h.termConf.GptApiKey,
- BaseURL: h.termConf.GptBaseUrl,
- Proxy: h.termConf.GptProxy,
- Model: h.termConf.GptModel,
- Prompt: conversation.Prompt,
+ sess := session.NewSession(jmsSrv.Session, h.sessionCallback)
+ session.AddSession(sess)
+
+ conv := &AIConversation{
+ Id: jmsSrv.Session.ID,
+ Prompt: msg.Prompt,
+ Context: make([]QARecord, 0),
+ JMSServer: jmsSrv,
}
- conversation.HistoryRecords = append(conversation.HistoryRecords, msg.Data)
- go h.chat(openAIParam, conversation)
-}
-
-func (h *chat) chat(
- chatGPTParam *OpenAIParam, conversation *AIConversation,
-) string {
- doneCh := make(chan string)
- answerCh := make(chan string)
- defer close(doneCh)
- defer close(answerCh)
-
- c := srvconn.NewOpenAIClient(
- chatGPTParam.AuthToken,
- chatGPTParam.BaseURL,
- chatGPTParam.Proxy,
+ h.conversations.Store(jmsSrv.Session.ID, conv)
+ go h.Monitor(conv)
+ return conv, nil
+}
+
+func (h *chat) sessionCallback(task *model.TerminalTask) error {
+ if task.Name == model.TaskKillSession {
+ h.endConversation(task.Args, "close", "kill session")
+ return nil
+ }
+ return fmt.Errorf("unknown session task %s", task.Name)
+}
+
+func (h *chat) runChat(conv *AIConversation) {
+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
+ defer cancel()
+
+ client := srvconn.NewOpenAIClient(
+ h.term.GptApiKey, h.term.GptBaseUrl, h.term.GptProxy,
)
- startIndex := len(conversation.HistoryRecords) - 15
- if startIndex < 0 {
- startIndex = 0
+ // Keep the last 8 contexts
+ if len(conv.Context) > 8 {
+ conv.Context = conv.Context[len(conv.Context)-8:]
}
- contents := conversation.HistoryRecords[startIndex:]
-
- openAIConn := &srvconn.OpenAIConn{
- Id: conversation.Id,
- Client: c,
- Prompt: chatGPTParam.Prompt,
- Model: chatGPTParam.Model,
- Contents: contents,
+ messages := buildChatMessages(conv)
+
+ conn := &srvconn.OpenAIConn{
+ Id: conv.Id,
+ Client: client,
+ Prompt: conv.Prompt,
+ Model: h.term.GptModel,
+ Question: conv.Question,
+ Context: messages,
+ AnswerCh: make(chan string),
+ DoneCh: make(chan string),
IsReasoning: false,
- AnswerCh: answerCh,
- DoneCh: doneCh,
- Type: h.termConf.ChatAIType,
+ Type: h.term.ChatAIType,
}
- go openAIConn.Chat(&conversation.InterruptCurrentChat)
- return h.processChatMessages(openAIConn)
+ // 启动 streaming
+ go conn.Chat(&conv.InterruptCurrentChat)
+
+ conv.JMSServer.Replay.WriteInput(conv.Question)
+
+ h.streamResponses(ctx, conv, conn)
+}
+
+func buildChatMessages(conv *AIConversation) []openai.ChatCompletionMessage {
+ msgs := make([]openai.ChatCompletionMessage, 0, len(conv.Context)*2)
+ for _, r := range conv.Context {
+ msgs = append(msgs,
+ openai.ChatCompletionMessage{Role: openai.ChatMessageRoleUser, Content: r.Question},
+ openai.ChatCompletionMessage{Role: openai.ChatMessageRoleAssistant, Content: r.Answer},
+ )
+ }
+ return msgs
}
-func (h *chat) processChatMessages(
- openAIConn *srvconn.OpenAIConn,
-) string {
- messageID := common.UUID()
- id := openAIConn.Id
+func (h *chat) streamResponses(
+ ctx context.Context, conv *AIConversation, conn *srvconn.OpenAIConn,
+) {
+ msgID := common.UUID()
for {
select {
- case answer := <-openAIConn.AnswerCh:
- h.sendSessionMessage(id, answer, messageID, "message", openAIConn.IsReasoning)
- case answer := <-openAIConn.DoneCh:
- h.sendSessionMessage(id, answer, messageID, "finish", false)
- return answer
+ case <-ctx.Done():
+ h.sendError(conv.Id, "chat timeout")
+ return
+ case ans := <-conn.AnswerCh:
+ h.sendMessage(conv.Id, msgID, ans, "message", conn.IsReasoning)
+ case ans := <-conn.DoneCh:
+ h.sendMessage(conv.Id, msgID, ans, "finish", false)
+ h.finalizeConversation(conv, ans)
+ return
}
}
}
-func (h *chat) sendSessionMessage(id, answer, messageID, messageType string, isReasoning bool) {
- message := ChatGPTMessage{
- Content: answer,
- ID: messageID,
+func (h *chat) finalizeConversation(conv *AIConversation, fullAnswer string) {
+ runes := []rune(fullAnswer)
+ snippet := fullAnswer
+ if len(runes) > 100 {
+ snippet = string(runes[:100])
+ }
+ conv.Context = append(conv.Context, QARecord{Question: conv.Question, Answer: snippet})
+
+ cmd := conv.JMSServer.GenerateCommandItem(h.ws.user.String(), conv.Question, fullAnswer)
+ go conv.JMSServer.CmdR.Record(cmd)
+ go conv.JMSServer.Replay.WriteOutput(fullAnswer)
+}
+
+func (h *chat) sendMessage(
+ convID, msgID, content, typ string, reasoning bool,
+) {
+ msg := ChatGPTMessage{
+ Content: content,
+ ID: msgID,
CreateTime: time.Now(),
- Type: messageType,
+ Type: typ,
Role: openai.ChatMessageRoleAssistant,
- IsReasoning: isReasoning,
+ IsReasoning: reasoning,
}
- data, _ := json.Marshal(message)
- msg := Message{
- Id: id,
- Type: "message",
- Data: string(data),
+ data, _ := json.Marshal(msg)
+ h.ws.SendMessage(&Message{Id: convID, Type: "message", Data: string(data)})
+}
+
+func (h *chat) sendError(convID, errMsg string) {
+ h.endConversation(convID, "error", errMsg)
+}
+
+func (h *chat) endConversation(convID, typ, msg string) {
+
+ defer func() {
+ if r := recover(); r != nil {
+ logger.Errorf("panic while sending message to session %s: %v", convID, r)
+ }
+ }()
+
+ if v, ok := h.conversations.Load(convID); ok {
+ if conv, ok2 := v.(*AIConversation); ok2 && conv.JMSServer != nil {
+ conv.JMSServer.Close(msg)
+ }
}
- h.ws.SendMessage(&msg)
+ h.conversations.Delete(convID)
+ h.ws.SendMessage(&Message{Id: convID, Type: typ, Data: msg})
}
-func (h *chat) sendErrorMessage(id, message string) {
- msg := Message{
- Id: id,
- Type: "error",
- Data: message,
+func (h *chat) interrupt(convID string) {
+ if v, ok := h.conversations.Load(convID); ok {
+ v.(*AIConversation).InterruptCurrentChat = true
}
- h.ws.SendMessage(&msg)
}
-func (h *chat) CleanConversationMap() {
- h.conversationMap.Range(func(key, value interface{}) bool {
- h.conversationMap.Delete(key)
+func (h *chat) cleanupAll() {
+ h.conversations.Range(func(key, _ interface{}) bool {
+ h.endConversation(key.(string), "close", "")
return true
})
}
+
+func (h *chat) Monitor(conv *AIConversation) {
+ lang := i18n.NewLang(h.ws.langCode)
+
+ lastActiveTime := time.Now()
+ maxIdleTime := time.Duration(h.term.MaxIdleTime) * time.Minute
+ MaxSessionTime := time.Now().Add(time.Duration(h.term.MaxSessionTime) * time.Hour)
+
+ for {
+ now := time.Now()
+ if MaxSessionTime.Before(now) {
+ msg := lang.T("Session max time reached, disconnect")
+ logger.Infof("Session[%s] max session time reached, disconnect", conv.Id)
+ h.endConversation(conv.Id, "close", msg)
+ return
+ }
+
+ outTime := lastActiveTime.Add(maxIdleTime)
+ if now.After(outTime) {
+ msg := fmt.Sprintf(lang.T("Connect idle more than %d minutes, disconnect"), h.term.MaxIdleTime)
+ logger.Infof("Session[%s] idle more than %d minutes, disconnect", conv.Id, h.term.MaxIdleTime)
+ h.endConversation(conv.Id, "close", msg)
+ return
+ }
+
+ if conv.NewDialogue {
+ lastActiveTime = time.Now()
+ conv.NewDialogue = false
+ }
+
+ time.Sleep(10 * time.Second)
+ }
+}
diff --git a/pkg/httpd/message.go b/pkg/httpd/message.go
index 9ed65854f..191fa4f27 100644
--- a/pkg/httpd/message.go
+++ b/pkg/httpd/message.go
@@ -1,6 +1,7 @@
package httpd
import (
+ "github.com/jumpserver/koko/pkg/proxy"
"time"
"github.com/jumpserver/koko/pkg/exchange"
@@ -163,11 +164,19 @@ type OpenAIParam struct {
Type string
}
+type QARecord struct {
+ Question string
+ Answer string
+}
+
type AIConversation struct {
Id string
Prompt string
- HistoryRecords []string
+ Question string
+ Context []QARecord
+ JMSServer *proxy.ChatJMSServer
InterruptCurrentChat bool
+ NewDialogue bool
}
type ChatGPTMessage struct {
diff --git a/pkg/httpd/webserver.go b/pkg/httpd/webserver.go
index 86ff6e280..ca494d1b4 100644
--- a/pkg/httpd/webserver.go
+++ b/pkg/httpd/webserver.go
@@ -158,9 +158,9 @@ func (s *Server) ChatAIWebsocket(ctx *gin.Context) {
}
userConn.handler = &chat{
- ws: userConn,
- conversationMap: sync.Map{},
- termConf: &termConf,
+ ws: userConn,
+ conversations: sync.Map{},
+ term: &termConf,
}
s.broadCaster.EnterUserWebsocket(userConn)
defer s.broadCaster.LeaveUserWebsocket(userConn)
diff --git a/pkg/jms-sdk-go/model/account.go b/pkg/jms-sdk-go/model/account.go
index 0e9e893ed..fbedc194b 100644
--- a/pkg/jms-sdk-go/model/account.go
+++ b/pkg/jms-sdk-go/model/account.go
@@ -55,6 +55,18 @@ type AccountDetail struct {
Privileged bool `json:"privileged"`
}
+type AssetChat struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+}
+
+type AccountChatDetail struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Username string `json:"username"`
+ Asset AssetChat `json:"asset"`
+}
+
type PermAccount struct {
Name string `json:"name"`
Username string `json:"username"`
diff --git a/pkg/jms-sdk-go/service/jms_asset.go b/pkg/jms-sdk-go/service/jms_asset.go
index fc15ce8cf..39d57c430 100644
--- a/pkg/jms-sdk-go/service/jms_asset.go
+++ b/pkg/jms-sdk-go/service/jms_asset.go
@@ -23,3 +23,9 @@ func (s *JMService) GetAccountSecretById(accountId string) (res model.AccountDet
_, err = s.authClient.Get(url, &res)
return
}
+
+func (s *JMService) GetAccountChat() (res model.AccountChatDetail, err error) {
+ url := fmt.Sprintf(AccountChatURL)
+ _, err = s.authClient.Get(url, &res)
+ return
+}
diff --git a/pkg/jms-sdk-go/service/url.go b/pkg/jms-sdk-go/service/url.go
index 893e40d5b..39051e826 100644
--- a/pkg/jms-sdk-go/service/url.go
+++ b/pkg/jms-sdk-go/service/url.go
@@ -77,6 +77,7 @@ const (
UserPermsAssetAccountsURL = "/api/v1/perms/users/%s/assets/%s/"
AccountSecretURL = "/api/v1/assets/account-secrets/%s/"
+ AccountChatURL = "/api/v1/accounts/accounts/chat/"
UserPermsAssetsURL = "/api/v1/perms/users/%s/assets/"
AssetLoginConfirmURL = "/api/v1/acls/login-asset/check/"
diff --git a/pkg/proxy/chat.go b/pkg/proxy/chat.go
new file mode 100644
index 000000000..0e845f206
--- /dev/null
+++ b/pkg/proxy/chat.go
@@ -0,0 +1,170 @@
+package proxy
+
+import (
+ "fmt"
+ "github.com/jumpserver/koko/pkg/common"
+ modelCommon "github.com/jumpserver/koko/pkg/jms-sdk-go/common"
+ "github.com/jumpserver/koko/pkg/jms-sdk-go/model"
+ "github.com/jumpserver/koko/pkg/jms-sdk-go/service"
+ "github.com/jumpserver/koko/pkg/logger"
+ "github.com/jumpserver/koko/pkg/session"
+ "strings"
+ "time"
+)
+
+type ChatReplyRecorder struct {
+ *ReplyRecorder
+}
+
+func (rh *ChatReplyRecorder) WriteInput(inputStr string) {
+ currentTime := time.Now()
+ formattedTime := currentTime.Format("2006-01-02 15:04:05")
+ inputStr = fmt.Sprintf("[%s]#: %s", formattedTime, inputStr)
+ rh.Record([]byte(inputStr))
+}
+
+func (rh *ChatReplyRecorder) WriteOutput(outputStr string) {
+ wrappedText := rh.wrapText(outputStr)
+ outputStr = "\r\n" + wrappedText + "\r\n"
+ rh.Record([]byte(outputStr))
+
+}
+
+func (rh *ChatReplyRecorder) wrapText(text string) string {
+ var wrappedTextBuilder strings.Builder
+ words := strings.Fields(text)
+ currentLineLength := 0
+
+ for _, word := range words {
+ wordLength := len(word)
+
+ if currentLineLength+wordLength > rh.Writer.Width {
+ wrappedTextBuilder.WriteString("\r\n" + word + " ")
+ currentLineLength = wordLength + 1
+ } else {
+ wrappedTextBuilder.WriteString(word + " ")
+ currentLineLength += wordLength + 1
+ }
+ }
+
+ return wrappedTextBuilder.String()
+}
+
+func NewChatJMSServer(
+ user, ip, userID, langCode string,
+ jmsService *service.JMService, conf *model.TerminalConfig) (*ChatJMSServer, error) {
+ accountInfo, err := jmsService.GetAccountChat()
+ if err != nil {
+ logger.Errorf("Get account chat info error: %s", err)
+ return nil, err
+ }
+
+ id := common.UUID()
+
+ apiSession := &model.Session{
+ ID: id,
+ User: user,
+ LoginFrom: model.LoginFromWeb,
+ RemoteAddr: ip,
+ Protocol: model.ActionALL,
+ Asset: accountInfo.Asset.Name,
+ Account: accountInfo.Name,
+ AccountID: accountInfo.ID,
+ AssetID: accountInfo.Asset.ID,
+ UserID: userID,
+ OrgID: "00000000-0000-0000-0000-000000000004",
+ Type: model.NORMALType,
+ LangCode: langCode,
+ DateStart: modelCommon.NewNowUTCTime(),
+ }
+
+ _, err2 := jmsService.CreateSession(*apiSession)
+ if err2 != nil {
+ return nil, err2
+ }
+
+ chat := &ChatJMSServer{
+ JmsService: jmsService,
+ Session: apiSession,
+ Conf: conf,
+ }
+
+ chat.CmdR = chat.GetCommandRecorder()
+ chat.Replay = chat.GetReplayRecorder()
+
+ if err1 := jmsService.RecordSessionLifecycleLog(id, model.AssetConnectSuccess,
+ model.EmptyLifecycleLog); err1 != nil {
+ logger.Errorf("Record session activity log err: %s", err1)
+ }
+
+ return chat, nil
+}
+
+type ChatJMSServer struct {
+ JmsService *service.JMService
+ Session *model.Session
+ CmdR *CommandRecorder
+ Replay *ChatReplyRecorder
+ Conf *model.TerminalConfig
+}
+
+func (s *ChatJMSServer) GenerateCommandItem(user, input, output string) *model.Command {
+ createdDate := time.Now()
+ return &model.Command{
+ SessionID: s.Session.ID,
+ OrgID: s.Session.OrgID,
+ Input: input,
+ Output: output,
+ User: user,
+ Server: s.Session.Asset,
+ Account: s.Session.Account,
+ Timestamp: createdDate.Unix(),
+ RiskLevel: model.NormalLevel,
+ DateCreated: createdDate.UTC(),
+ }
+}
+
+func (s *ChatJMSServer) GetReplayRecorder() *ChatReplyRecorder {
+ info := &ReplyInfo{
+ Width: 200,
+ Height: 200,
+ TimeStamp: time.Now(),
+ }
+ recorder, err := NewReplayRecord(s.Session.ID, s.JmsService,
+ NewReplayStorage(s.JmsService, s.Conf),
+ info)
+ if err != nil {
+ logger.Error(err)
+ }
+
+ return &ChatReplyRecorder{recorder}
+}
+
+func (s *ChatJMSServer) GetCommandRecorder() *CommandRecorder {
+ cmdR := CommandRecorder{
+ sessionID: s.Session.ID,
+ storage: NewCommandStorage(s.JmsService, s.Conf),
+ queue: make(chan *model.Command, 10),
+ closed: make(chan struct{}),
+ jmsService: s.JmsService,
+ }
+ go cmdR.record()
+ return &cmdR
+}
+
+func (s *ChatJMSServer) Close(msg string) {
+ session.RemoveSessionById(s.Session.ID)
+ if err := s.JmsService.SessionFinished(s.Session.ID, modelCommon.NewNowUTCTime()); err != nil {
+ logger.Errorf("finish session %s: %v", s.Session.ID, err)
+ }
+
+ s.CmdR.End()
+ s.Replay.End()
+
+ logObj := model.SessionLifecycleLog{Reason: msg, User: s.Session.User}
+ err := s.JmsService.RecordSessionLifecycleLog(s.Session.ID, model.AssetConnectFinished, logObj)
+ if err != nil {
+ logger.Errorf("record session lifecycle log %s: %v", s.Session.ID, err)
+ return
+ }
+}
diff --git a/pkg/srvconn/conn_openai.go b/pkg/srvconn/conn_openai.go
index 9d639a6a9..50b1e5ad6 100644
--- a/pkg/srvconn/conn_openai.go
+++ b/pkg/srvconn/conn_openai.go
@@ -93,7 +93,8 @@ type OpenAIConn struct {
Client *openai.Client
Model string
Prompt string
- Contents []string
+ Question string
+ Context []openai.ChatCompletionMessage
IsReasoning bool
AnswerCh chan string
DoneCh chan string
@@ -102,11 +103,10 @@ type OpenAIConn struct {
func (conn *OpenAIConn) Chat(interruptCurrentChat *bool) {
ctx := context.Background()
- var messages []openai.ChatCompletionMessage
- messages = append(messages, openai.ChatCompletionMessage{
+ messages := append(conn.Context, openai.ChatCompletionMessage{
Role: openai.ChatMessageRoleUser,
- Content: strings.Join(conn.Contents, "\n"),
+ Content: conn.Question,
})
systemPrompt := conn.Prompt
@@ -182,6 +182,10 @@ func (conn *OpenAIConn) Chat(interruptCurrentChat *bool) {
newContent = response.Choices[0].Delta.Content
}
+ if newContent == "" {
+ continue
+ }
+
content += newContent
conn.AnswerCh <- content
}
From 45bc9cf8c107cad20d368ffdb1bdbb01d4d12203 Mon Sep 17 00:00:00 2001
From: feng <1304903146@qq.com>
Date: Wed, 30 Apr 2025 17:41:30 +0800
Subject: [PATCH 2/9] perf: add chat model
---
pkg/httpd/chat.go | 8 +++++++-
pkg/httpd/message.go | 2 ++
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/pkg/httpd/chat.go b/pkg/httpd/chat.go
index 9836cf941..3d5c4b80a 100644
--- a/pkg/httpd/chat.go
+++ b/pkg/httpd/chat.go
@@ -76,6 +76,7 @@ func (h *chat) getOrCreateConversation(msg *Message) (*AIConversation, error) {
conv := &AIConversation{
Id: jmsSrv.Session.ID,
Prompt: msg.Prompt,
+ Model: msg.ChatModel,
Context: make([]QARecord, 0),
JMSServer: jmsSrv,
}
@@ -106,11 +107,16 @@ func (h *chat) runChat(conv *AIConversation) {
}
messages := buildChatMessages(conv)
+ chatModel := conv.Model
+ if conv.Model == "" {
+ chatModel = h.term.GptModel
+ }
+
conn := &srvconn.OpenAIConn{
Id: conv.Id,
Client: client,
Prompt: conv.Prompt,
- Model: h.term.GptModel,
+ Model: chatModel,
Question: conv.Question,
Context: messages,
AnswerCh: make(chan string),
diff --git a/pkg/httpd/message.go b/pkg/httpd/message.go
index 191fa4f27..3c98fb2d4 100644
--- a/pkg/httpd/message.go
+++ b/pkg/httpd/message.go
@@ -19,6 +19,7 @@ type Message struct {
//Chat AI
Prompt string `json:"prompt"`
Interrupt bool `json:"interrupt"`
+ ChatModel string `json:"chat_model"`
//K8s
KubernetesId string `json:"k8s_id"`
@@ -173,6 +174,7 @@ type AIConversation struct {
Id string
Prompt string
Question string
+ Model string
Context []QARecord
JMSServer *proxy.ChatJMSServer
InterruptCurrentChat bool
From 505035df2e0c9fa47c74c7e893003c1b448e5218 Mon Sep 17 00:00:00 2001
From: zhaojisen <1301338853@qq.com>
Date: Wed, 30 Apr 2025 17:47:46 +0800
Subject: [PATCH 3/9] Perf: change UI
---
ui/components.d.ts | 1 +
ui/public/icons/logo.svg | 1 +
ui/src/hooks/helper/index.ts | 3 +
ui/src/index.css | 7 ++
ui/src/overrides.ts | 2 +-
.../chat/components/Conversation/index.vue | 70 ++++++++++++++++
.../components/Conversation/optionRender.tsx | 72 +++++++++++++++++
ui/src/views/chat/components/Header/index.vue | 21 +++++
ui/src/views/chat/components/Sider/index.vue | 59 ++++++++++++++
.../views/chat/components/Welcome/index.vue | 14 ++++
ui/src/views/chat/index.vue | 79 +++++++------------
11 files changed, 279 insertions(+), 50 deletions(-)
create mode 100755 ui/public/icons/logo.svg
create mode 100644 ui/src/views/chat/components/Conversation/index.vue
create mode 100644 ui/src/views/chat/components/Conversation/optionRender.tsx
create mode 100644 ui/src/views/chat/components/Header/index.vue
create mode 100644 ui/src/views/chat/components/Sider/index.vue
create mode 100644 ui/src/views/chat/components/Welcome/index.vue
diff --git a/ui/components.d.ts b/ui/components.d.ts
index 7d80f5542..7b71b1018 100644
--- a/ui/components.d.ts
+++ b/ui/components.d.ts
@@ -59,6 +59,7 @@ declare module 'vue' {
NResult: typeof import('naive-ui')['NResult']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
+ NSpace: typeof import('naive-ui')['NSpace']
NSpin: typeof import('naive-ui')['NSpin']
NSplit: typeof import('naive-ui')['NSplit']
NSwitch: typeof import('naive-ui')['NSwitch']
diff --git a/ui/public/icons/logo.svg b/ui/public/icons/logo.svg
new file mode 100755
index 000000000..eb25227d1
--- /dev/null
+++ b/ui/public/icons/logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/hooks/helper/index.ts b/ui/src/hooks/helper/index.ts
index 6b99888a4..9b6ef1586 100644
--- a/ui/src/hooks/helper/index.ts
+++ b/ui/src/hooks/helper/index.ts
@@ -306,6 +306,9 @@ export const generateWsURL = () => {
connectURL = BASE_WS_URL + '/koko/ws/terminal/?' + requireParams.toString();
break;
}
+ case 'Chat':
+ connectURL = BASE_WS_URL + `/koko/ws/chat/system`
+ break;
default: {
connectURL = urlParams ? `${BASE_WS_URL}/koko/ws/terminal/?${urlParams.toString()}` : '';
}
diff --git a/ui/src/index.css b/ui/src/index.css
index 3d552a61f..db1950261 100644
--- a/ui/src/index.css
+++ b/ui/src/index.css
@@ -1,2 +1,9 @@
@import "tailwindcss";
+.icon-hover-primary {
+ @apply cursor-pointer hover:text-[#16987D] focus:outline-none;
+}
+
+.icon-hover-danger {
+ @apply cursor-pointer hover:text-[#ff0000] focus:outline-none;
+}
diff --git a/ui/src/overrides.ts b/ui/src/overrides.ts
index 707313c2a..30a5dedbc 100644
--- a/ui/src/overrides.ts
+++ b/ui/src/overrides.ts
@@ -126,7 +126,7 @@ export const themeOverrides: GlobalThemeOverrides = {
},
Layout: {
color: 'rgba(0, 0, 0, 1)',
- siderColor: 'rgba(0, 0, 0, 1)',
+ siderColor: 'rgba(255, 255, 255, 0.09)',
headerColor: 'rgba(0, 0, 0, 1)'
}
};
diff --git a/ui/src/views/chat/components/Conversation/index.vue b/ui/src/views/chat/components/Conversation/index.vue
new file mode 100644
index 000000000..7c0761a39
--- /dev/null
+++ b/ui/src/views/chat/components/Conversation/index.vue
@@ -0,0 +1,70 @@
+
+
+
+ 今天
+
+
+
+
+
+ 随便聊聊
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 昨天
+
+
+
+
+
+ 随便聊聊
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/views/chat/components/Conversation/optionRender.tsx b/ui/src/views/chat/components/Conversation/optionRender.tsx
new file mode 100644
index 000000000..df8aed2c8
--- /dev/null
+++ b/ui/src/views/chat/components/Conversation/optionRender.tsx
@@ -0,0 +1,72 @@
+import { useI18n } from 'vue-i18n';
+import { NSpace, NText } from 'naive-ui';
+import { SquareArrowOutUpRight, PencilLine, Trash2 } from 'lucide-vue-next';
+
+import type { SelectOption } from 'naive-ui';
+import type { FunctionalComponent } from 'vue';
+import type { LucideProps } from 'lucide-vue-next';
+
+interface OptionItem {
+ value: string;
+ label: string;
+ textColor?: string;
+ iconColor?: string;
+ click: (chatId: string) => void;
+ icon: FunctionalComponent;
+}
+
+type EmitsType = {
+ (e: 'chat-share', shareId: string): void;
+ (e: 'chat-rename', shareId: string): void;
+ (e: 'chat-delete', shareId: string): void;
+};
+
+export const OptionRender = (emits: EmitsType): SelectOption[] => {
+ const { t } = useI18n();
+
+ const optionItems: OptionItem[] = [
+ {
+ value: 'share',
+ icon: SquareArrowOutUpRight,
+ label: t('Share'),
+ iconColor: 'white',
+ click: (chatId: string) => {
+ emits('chat-share', chatId);
+ }
+ },
+ {
+ value: 'rename',
+ icon: PencilLine,
+ label: t('Rename'),
+ iconColor: 'white',
+ click: (chatId: string) => {
+ emits('chat-rename', chatId);
+ }
+ },
+ {
+ value: 'delete',
+ icon: Trash2,
+ label: t('Delete'),
+ iconColor: '#fb2c36',
+ textColor: '!text-red-500',
+ click: (chatId: string) => {
+ emits('chat-delete', chatId);
+ }
+ }
+ ];
+
+ const commonClass = 'px-4 py-2 w-28 hover:bg-[#ffffff1A] cursor-pointer transition-all duration-300';
+
+ return optionItems.map(item => ({
+ value: item.value,
+ render: () => (
+ item.click(item.value)}>
+ {item.icon && }
+
+
+ {item.label}
+
+
+ )
+ }));
+};
diff --git a/ui/src/views/chat/components/Header/index.vue b/ui/src/views/chat/components/Header/index.vue
new file mode 100644
index 000000000..a62d03f3a
--- /dev/null
+++ b/ui/src/views/chat/components/Header/index.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+ GPT-4
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/views/chat/components/Sider/index.vue b/ui/src/views/chat/components/Sider/index.vue
new file mode 100644
index 000000000..fc8881802
--- /dev/null
+++ b/ui/src/views/chat/components/Sider/index.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/views/chat/components/Welcome/index.vue b/ui/src/views/chat/components/Welcome/index.vue
new file mode 100644
index 000000000..fcd0ec290
--- /dev/null
+++ b/ui/src/views/chat/components/Welcome/index.vue
@@ -0,0 +1,14 @@
+
+
+
+ 散财消灾
+
+
+
+
+
diff --git a/ui/src/views/chat/index.vue b/ui/src/views/chat/index.vue
index e0446676e..3018d7e89 100644
--- a/ui/src/views/chat/index.vue
+++ b/ui/src/views/chat/index.vue
@@ -1,48 +1,15 @@
-
-
+
-
- 随便聊聊
-
- 随便聊聊
- 随便聊聊
- 随便聊聊
- 随便聊聊
-
-
-
-
- 随便聊聊
-
-
-
-
- GPT-4
-
-
-
+
+
+
-
-
- 散财消灾
-
-
+
@@ -51,17 +18,31 @@
-
+// ws.value?.onopen(() => {
+// console.log('open')
+// })
+
+// ws.value?.onmessage((message: MessageEvent) => {
+// console.log(message)
+// })
+
+
+onMounted(() => {
+
+})
+
From 73c055a0a75436fe5066d03e40b05d24c801bd87 Mon Sep 17 00:00:00 2001
From: zhaojisen <1301338853@qq.com>
Date: Wed, 30 Apr 2025 18:33:34 +0800
Subject: [PATCH 4/9] Perf: Socket Connect
---
ui/src/hooks/helper/index.ts | 3 ++-
ui/src/hooks/useChat.ts | 45 +++++++++++++++++++++++++++++++
ui/src/store/modules/chat.ts | 8 ++++++
ui/src/types/modules/chat.type.ts | 5 ++++
ui/src/views/chat/index.vue | 5 ++--
5 files changed, 63 insertions(+), 3 deletions(-)
create mode 100644 ui/src/hooks/useChat.ts
create mode 100644 ui/src/store/modules/chat.ts
create mode 100644 ui/src/types/modules/chat.type.ts
diff --git a/ui/src/hooks/helper/index.ts b/ui/src/hooks/helper/index.ts
index 9b6ef1586..328edb554 100644
--- a/ui/src/hooks/helper/index.ts
+++ b/ui/src/hooks/helper/index.ts
@@ -307,7 +307,8 @@ export const generateWsURL = () => {
break;
}
case 'Chat':
- connectURL = BASE_WS_URL + `/koko/ws/chat/system`
+ // connectURL = BASE_WS_URL + `/koko/ws/chat/system`
+ connectURL = 'ws://localhost:5050' + `/koko/ws/chat/system/`
break;
default: {
connectURL = urlParams ? `${BASE_WS_URL}/koko/ws/terminal/?${urlParams.toString()}` : '';
diff --git a/ui/src/hooks/useChat.ts b/ui/src/hooks/useChat.ts
new file mode 100644
index 000000000..10c6d7bb3
--- /dev/null
+++ b/ui/src/hooks/useChat.ts
@@ -0,0 +1,45 @@
+import { ref } from 'vue'
+import { MessageType } from '@/enum'
+import { useWebSocket } from '@vueuse/core'
+import { generateWsURL } from '@/hooks/helper';
+
+export const useChat = () => {
+
+ const socket = ref();
+
+ const socketOnMessage = (message: MessageEvent) => {
+ const messageData = JSON.parse(message.data);
+
+ console.log(messageData);
+
+ switch (messageData.type) {
+ case MessageType.CONNECT:
+ break;
+ }
+ }
+
+ const createChatSocket = () => {
+ const url = generateWsURL()
+
+ const { ws } = useWebSocket(url)
+
+ if (!ws.value) {
+
+ }
+
+ ws.value.onopen(() => {
+ // TODO 心跳
+ console.log('Connected to websocket');
+ })
+ ws.value.onmessage = socketOnMessage
+
+ return {
+ socket: socket.value,
+ }
+ }
+
+
+ return {
+ createChatSocket
+ }
+}
\ No newline at end of file
diff --git a/ui/src/store/modules/chat.ts b/ui/src/store/modules/chat.ts
new file mode 100644
index 000000000..c776c81f1
--- /dev/null
+++ b/ui/src/store/modules/chat.ts
@@ -0,0 +1,8 @@
+import { defineStore } from 'pinia'
+
+export const useChatStore = defineStore('chat-store', {
+ state: () => ({}),
+ actions: {
+
+ }
+})
\ No newline at end of file
diff --git a/ui/src/types/modules/chat.type.ts b/ui/src/types/modules/chat.type.ts
new file mode 100644
index 000000000..a26defdc2
--- /dev/null
+++ b/ui/src/types/modules/chat.type.ts
@@ -0,0 +1,5 @@
+export interface ChatState {
+ username: string;
+
+ messageId: string
+}
\ No newline at end of file
diff --git a/ui/src/views/chat/index.vue b/ui/src/views/chat/index.vue
index 3018d7e89..6906f7536 100644
--- a/ui/src/views/chat/index.vue
+++ b/ui/src/views/chat/index.vue
@@ -25,13 +25,14 @@ import InputArea from './components/InputArea/index.vue';
import { onMounted } from 'vue'
import { useMessage } from 'naive-ui'
+import { useChat } from '@/hooks/useChat.ts'
import { useWebSocket } from '@vueuse/core'
import { generateWsURL } from '@/hooks/helper'
const message = useMessage()
+const { createChatSocket } = useChat()
-const url = generateWsURL()
-const { ws } = useWebSocket(url)
+createChatSocket()
// ws.value?.onopen(() => {
// console.log('open')
From e047b4844e863d48c18c76102d5a70699d0001e9 Mon Sep 17 00:00:00 2001
From: zhaojisen <1301338853@qq.com>
Date: Tue, 6 May 2025 14:37:16 +0800
Subject: [PATCH 5/9] Perf: Basic layout
---
ui/package.json | 2 +-
ui/src/index.css | 4 +--
.../views/chat/components/InputArea/index.vue | 9 +++++-
ui/src/views/chat/index.vue | 31 +++++++++----------
ui/yarn.lock | 8 ++---
5 files changed, 29 insertions(+), 25 deletions(-)
diff --git a/ui/package.json b/ui/package.json
index ffb133840..0f5108024 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -23,7 +23,7 @@
"clipboard-polyfill": "^4.1.0",
"dayjs": "^1.11.13",
"loglevel": "^1.9.1",
- "lucide-vue-next": "^0.487.0",
+ "lucide-vue-next": "^0.507.0",
"mitt": "^3.0.1",
"naive-ui": "^2.39.0",
"nora-zmodemjs": "^1.1.1",
diff --git a/ui/src/index.css b/ui/src/index.css
index db1950261..2534f1b56 100644
--- a/ui/src/index.css
+++ b/ui/src/index.css
@@ -1,9 +1,9 @@
@import "tailwindcss";
.icon-hover-primary {
- @apply cursor-pointer hover:text-[#16987D] focus:outline-none;
+ @apply cursor-pointer hover:text-[#16987D] focus:outline-none transition-all duration-300;
}
.icon-hover-danger {
- @apply cursor-pointer hover:text-[#ff0000] focus:outline-none;
+ @apply cursor-pointer hover:text-[#ff0000] focus:outline-none transition-all duration-300;
}
diff --git a/ui/src/views/chat/components/InputArea/index.vue b/ui/src/views/chat/components/InputArea/index.vue
index 225e4cc47..0d30f1d79 100644
--- a/ui/src/views/chat/components/InputArea/index.vue
+++ b/ui/src/views/chat/components/InputArea/index.vue
@@ -1,7 +1,13 @@
-
+
+
+
+
+
+
+
发送
@@ -11,6 +17,7 @@
diff --git a/ui/yarn.lock b/ui/yarn.lock
index 60fa84f0c..9c73e53f9 100644
--- a/ui/yarn.lock
+++ b/ui/yarn.lock
@@ -2006,10 +2006,10 @@ lru-cache@^5.1.1:
dependencies:
yallist "^3.0.2"
-lucide-vue-next@^0.487.0:
- version "0.487.0"
- resolved "https://registry.yarnpkg.com/lucide-vue-next/-/lucide-vue-next-0.487.0.tgz#18d9e6b21c37c823d1c4879203b323dd97ada5f0"
- integrity sha512-ilVgu9EHkfId7WSjmoPkzp13cuzpSGO5J16AmltjRHFoj6MlCBGY8BzsBU/ISKVlDheUxM+MsIYjNo/1hSEa6w==
+lucide-vue-next@^0.507.0:
+ version "0.507.0"
+ resolved "https://registry.yarnpkg.com/lucide-vue-next/-/lucide-vue-next-0.507.0.tgz#bd2d9a5f85550875fb6f85082dd108c46a1870fd"
+ integrity sha512-n0AZRmez4xq5vu5ZOfnrhC5wKBpu5tpwDHA6Ue2+sKyJv3USAPxqIerXLbdYbkHRW1tVZ3U9GE1MACsu6X/Z7Q==
magic-string@*, magic-string@^0.30.10, magic-string@^0.30.11:
version "0.30.11"
From 89d109e05bbeb2790099b0458f8646e6753d62c3 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Tue, 6 May 2025 06:47:46 +0000
Subject: [PATCH 6/9] perf: Update Dockerfile with new base image tag
---
Dockerfile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Dockerfile b/Dockerfile
index 21a318e04..464a33855 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM jumpserver/koko-base:20250429_034346 AS stage-build
+FROM jumpserver/koko-base:20250506_063744 AS stage-build
WORKDIR /opt/koko
ARG TARGETARCH
From b2183d19cb7794e7a2c697b1adc2992c9a631195 Mon Sep 17 00:00:00 2001
From: zhaojisen <1301338853@qq.com>
Date: Thu, 8 May 2025 15:13:38 +0800
Subject: [PATCH 7/9] Fixed: socket error
---
ui/components.d.ts | 4 ---
ui/src/hooks/useChat.ts | 36 +++++++++----------
.../components/Conversation/optionRender.tsx | 2 +-
ui/src/views/chat/components/Sider/index.vue | 7 ----
4 files changed, 19 insertions(+), 30 deletions(-)
diff --git a/ui/components.d.ts b/ui/components.d.ts
index 7b71b1018..e9abf586b 100644
--- a/ui/components.d.ts
+++ b/ui/components.d.ts
@@ -40,13 +40,11 @@ declare module 'vue' {
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
NGrid: typeof import('naive-ui')['NGrid']
NGridItem: typeof import('naive-ui')['NGridItem']
- NH2: typeof import('naive-ui')['NH2']
NIcon: typeof import('naive-ui')['NIcon']
NImage: typeof import('naive-ui')['NImage']
NInput: typeof import('naive-ui')['NInput']
NLayout: typeof import('naive-ui')['NLayout']
NLayoutContent: typeof import('naive-ui')['NLayoutContent']
- NLayoutFooter: typeof import('naive-ui')['NLayoutFooter']
NLayoutHeader: typeof import('naive-ui')['NLayoutHeader']
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
NList: typeof import('naive-ui')['NList']
@@ -63,13 +61,11 @@ declare module 'vue' {
NSpin: typeof import('naive-ui')['NSpin']
NSplit: typeof import('naive-ui')['NSplit']
NSwitch: typeof import('naive-ui')['NSwitch']
- NTab: typeof import('naive-ui')['NTab']
NTable: typeof import('naive-ui')['NTable']
NTabPane: typeof import('naive-ui')['NTabPane']
NTabs: typeof import('naive-ui')['NTabs']
NTag: typeof import('naive-ui')['NTag']
NText: typeof import('naive-ui')['NText']
- NTextarea: typeof import('naive-ui')['NTextarea']
NThing: typeof import('naive-ui')['NThing']
NTooltip: typeof import('naive-ui')['NTooltip']
NTree: typeof import('naive-ui')['NTree']
diff --git a/ui/src/hooks/useChat.ts b/ui/src/hooks/useChat.ts
index 10c6d7bb3..4eeea5479 100644
--- a/ui/src/hooks/useChat.ts
+++ b/ui/src/hooks/useChat.ts
@@ -1,10 +1,9 @@
-import { ref } from 'vue'
-import { MessageType } from '@/enum'
-import { useWebSocket } from '@vueuse/core'
+import { ref } from 'vue';
+import { MessageType } from '@/enum';
+import { useWebSocket } from '@vueuse/core';
import { generateWsURL } from '@/hooks/helper';
export const useChat = () => {
-
const socket = ref();
const socketOnMessage = (message: MessageEvent) => {
@@ -16,30 +15,31 @@ export const useChat = () => {
case MessageType.CONNECT:
break;
}
- }
+ };
const createChatSocket = () => {
- const url = generateWsURL()
+ const url = generateWsURL();
- const { ws } = useWebSocket(url)
+ const { ws } = useWebSocket(url);
if (!ws.value) {
-
+ return;
}
- ws.value.onopen(() => {
- // TODO 心跳
+ ws.value.onopen = () => {
console.log('Connected to websocket');
- })
- ws.value.onmessage = socketOnMessage
+ };
+ ws.value.onmessage = socketOnMessage;
+ ws.value.onclose = () => {
+ console.log('Disconnected from websocket');
+ };
return {
- socket: socket.value,
- }
- }
-
+ socket: socket.value
+ };
+ };
return {
createChatSocket
- }
-}
\ No newline at end of file
+ };
+};
diff --git a/ui/src/views/chat/components/Conversation/optionRender.tsx b/ui/src/views/chat/components/Conversation/optionRender.tsx
index df8aed2c8..544ea6332 100644
--- a/ui/src/views/chat/components/Conversation/optionRender.tsx
+++ b/ui/src/views/chat/components/Conversation/optionRender.tsx
@@ -55,7 +55,7 @@ export const OptionRender = (emits: EmitsType): SelectOption[] => {
}
];
- const commonClass = 'px-4 py-2 w-28 hover:bg-[#ffffff1A] cursor-pointer transition-all duration-300';
+ const commonClass = 'px-4 py-2 w-30 hover:bg-[#ffffff1A] cursor-pointer transition-all duration-300';
return optionItems.map(item => ({
value: item.value,
diff --git a/ui/src/views/chat/components/Sider/index.vue b/ui/src/views/chat/components/Sider/index.vue
index fc8881802..6e597e062 100644
--- a/ui/src/views/chat/components/Sider/index.vue
+++ b/ui/src/views/chat/components/Sider/index.vue
@@ -13,12 +13,6 @@
-
-
-
-
-
import { reactive } from 'vue';
-import { Search, SquarePen } from 'lucide-vue-next';
import Conversation from '../Conversation/index.vue';
import type { CSSProperties } from 'vue';
From 0825d08793a164d426da9270074d94ee8b4ce971 Mon Sep 17 00:00:00 2001
From: zhaojisen <1301338853@qq.com>
Date: Mon, 12 May 2025 10:21:04 +0800
Subject: [PATCH 8/9] Change Layout
---
ui/components.d.ts | 2 +
ui/package.json | 1 +
ui/public/icons/help.svg | 1 -
ui/public/icons/k8s.svg | 1 -
ui/public/icons/organize.svg | 1 -
ui/public/icons/setting.svg | 1 -
ui/public/icons/split.svg | 1 -
ui/public/icons/tree.svg | 1 -
ui/public/icons/user.svg | 1 -
ui/public/images/ChatGPT-Logo.png | Bin 0 -> 43937 bytes
ui/public/images/DeepSeek_Logo.webp | Bin 0 -> 4862 bytes
ui/src/enum/index.ts | 2 +
ui/src/hooks/useChat.ts | 38 +++-
ui/src/store/modules/useChat.ts | 27 +++
ui/src/types/modules/chat.type.ts | 56 +++++-
.../views/chat/components/Content/index.vue | 92 +++++++++
.../views/chat/components/InputArea/index.vue | 58 ++++--
ui/src/views/chat/index.vue | 30 ++-
ui/yarn.lock | 179 +++++++++++++++++-
19 files changed, 435 insertions(+), 57 deletions(-)
delete mode 100644 ui/public/icons/help.svg
delete mode 100644 ui/public/icons/k8s.svg
delete mode 100644 ui/public/icons/organize.svg
delete mode 100644 ui/public/icons/setting.svg
delete mode 100644 ui/public/icons/split.svg
delete mode 100644 ui/public/icons/tree.svg
delete mode 100644 ui/public/icons/user.svg
create mode 100644 ui/public/images/ChatGPT-Logo.png
create mode 100644 ui/public/images/DeepSeek_Logo.webp
create mode 100644 ui/src/store/modules/useChat.ts
create mode 100644 ui/src/views/chat/components/Content/index.vue
diff --git a/ui/components.d.ts b/ui/components.d.ts
index e9abf586b..4d9fc9b03 100644
--- a/ui/components.d.ts
+++ b/ui/components.d.ts
@@ -16,6 +16,7 @@ declare module 'vue' {
Keyboard: typeof import('./src/components/Drawer/components/Keyboard/index.vue')['default']
Logo: typeof import('./src/components/Kubernetes/Sidebar/components/Logo/index.vue')['default']
MainContent: typeof import('./src/components/Kubernetes/MainContent/index.vue')['default']
+ NAvatar: typeof import('naive-ui')['NAvatar']
NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
NButton: typeof import('naive-ui')['NButton']
@@ -67,6 +68,7 @@ declare module 'vue' {
NTag: typeof import('naive-ui')['NTag']
NText: typeof import('naive-ui')['NText']
NThing: typeof import('naive-ui')['NThing']
+ NTime: typeof import('naive-ui')['NTime']
NTooltip: typeof import('naive-ui')['NTooltip']
NTree: typeof import('naive-ui')['NTree']
NUpload: typeof import('naive-ui')['NUpload']
diff --git a/ui/package.json b/ui/package.json
index 0f5108024..4a2ba8b53 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -40,6 +40,7 @@
"xterm-theme": "^1.1.0"
},
"devDependencies": {
+ "@duskmoon/vue3-typed-js": "^0.0.4",
"@eslint/js": "^9.23.0",
"@types/node": "^20.14.11",
"@types/sortablejs": "^1.15.0",
diff --git a/ui/public/icons/help.svg b/ui/public/icons/help.svg
deleted file mode 100644
index e5bdc1fbb..000000000
--- a/ui/public/icons/help.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/ui/public/icons/k8s.svg b/ui/public/icons/k8s.svg
deleted file mode 100644
index 4a4b34d91..000000000
--- a/ui/public/icons/k8s.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/ui/public/icons/organize.svg b/ui/public/icons/organize.svg
deleted file mode 100644
index 0d78983d8..000000000
--- a/ui/public/icons/organize.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/ui/public/icons/setting.svg b/ui/public/icons/setting.svg
deleted file mode 100644
index a8d642278..000000000
--- a/ui/public/icons/setting.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/ui/public/icons/split.svg b/ui/public/icons/split.svg
deleted file mode 100644
index 2fdae88cb..000000000
--- a/ui/public/icons/split.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/ui/public/icons/tree.svg b/ui/public/icons/tree.svg
deleted file mode 100644
index 19b13eb69..000000000
--- a/ui/public/icons/tree.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/ui/public/icons/user.svg b/ui/public/icons/user.svg
deleted file mode 100644
index 58a1fb7af..000000000
--- a/ui/public/icons/user.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/ui/public/images/ChatGPT-Logo.png b/ui/public/images/ChatGPT-Logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..71c600e9aca2ec321d8f6a839f29e8c0b64f1e47
GIT binary patch
literal 43937
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 | |