* feat: add chat page * feat: add chat page * Update ai.go * Update ai_test.go * Update message.go --------- Co-authored-by: hsluoyz <hsluoyz@qq.com>HEAD
| @@ -0,0 +1,141 @@ | |||||
| // Copyright 2023 The casbin Authors. All Rights Reserved. | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| package ai | |||||
| import ( | |||||
| "context" | |||||
| "fmt" | |||||
| "io" | |||||
| "net/http" | |||||
| "strings" | |||||
| "time" | |||||
| "github.com/sashabaranov/go-openai" | |||||
| ) | |||||
| func queryAnswer(authToken string, question string, timeout int) (string, error) { | |||||
| // fmt.Printf("Question: %s\n", question) | |||||
| client := getProxyClientFromToken(authToken) | |||||
| ctx, cancel := context.WithTimeout(context.Background(), time.Duration(2+timeout*2)*time.Second) | |||||
| defer cancel() | |||||
| resp, err := client.CreateChatCompletion( | |||||
| ctx, | |||||
| openai.ChatCompletionRequest{ | |||||
| Model: openai.GPT3Dot5Turbo, | |||||
| Messages: []openai.ChatCompletionMessage{ | |||||
| { | |||||
| Role: openai.ChatMessageRoleUser, | |||||
| Content: question, | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| ) | |||||
| if err != nil { | |||||
| return "", err | |||||
| } | |||||
| res := resp.Choices[0].Message.Content | |||||
| res = strings.Trim(res, "\n") | |||||
| // fmt.Printf("Answer: %s\n\n", res) | |||||
| return res, nil | |||||
| } | |||||
| func QueryAnswerSafe(authToken string, question string) string { | |||||
| var res string | |||||
| var err error | |||||
| for i := 0; i < 10; i++ { | |||||
| res, err = queryAnswer(authToken, question, i) | |||||
| if err != nil { | |||||
| if i > 0 { | |||||
| fmt.Printf("\tFailed (%d): %s\n", i+1, err.Error()) | |||||
| } | |||||
| } else { | |||||
| break | |||||
| } | |||||
| } | |||||
| if err != nil { | |||||
| panic(err) | |||||
| } | |||||
| return res | |||||
| } | |||||
| func QueryAnswerStream(authToken string, question string, writer io.Writer, builder *strings.Builder) error { | |||||
| client := getProxyClientFromToken(authToken) | |||||
| ctx := context.Background() | |||||
| flusher, ok := writer.(http.Flusher) | |||||
| if !ok { | |||||
| return fmt.Errorf("writer does not implement http.Flusher") | |||||
| } | |||||
| // https://platform.openai.com/tokenizer | |||||
| // https://github.com/pkoukk/tiktoken-go#available-encodings | |||||
| promptTokens, err := getTokenSize(openai.GPT3TextDavinci003, question) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| // https://platform.openai.com/docs/models/gpt-3-5 | |||||
| maxTokens := 4097 - promptTokens | |||||
| respStream, err := client.CreateCompletionStream( | |||||
| ctx, | |||||
| openai.CompletionRequest{ | |||||
| Model: openai.GPT3TextDavinci003, | |||||
| Prompt: question, | |||||
| MaxTokens: maxTokens, | |||||
| Stream: true, | |||||
| }, | |||||
| ) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| defer respStream.Close() | |||||
| isLeadingReturn := true | |||||
| for { | |||||
| completion, streamErr := respStream.Recv() | |||||
| if streamErr != nil { | |||||
| if streamErr == io.EOF { | |||||
| break | |||||
| } | |||||
| return streamErr | |||||
| } | |||||
| data := completion.Choices[0].Text | |||||
| if isLeadingReturn && len(data) != 0 { | |||||
| if strings.Count(data, "\n") == len(data) { | |||||
| continue | |||||
| } else { | |||||
| isLeadingReturn = false | |||||
| } | |||||
| } | |||||
| fmt.Printf("%s", data) | |||||
| // Write the streamed data as Server-Sent Events | |||||
| if _, err = fmt.Fprintf(writer, "event: message\ndata: %s\n\n", data); err != nil { | |||||
| return err | |||||
| } | |||||
| flusher.Flush() | |||||
| // Append the response to the strings.Builder | |||||
| builder.WriteString(data) | |||||
| } | |||||
| return nil | |||||
| } | |||||
| @@ -0,0 +1,42 @@ | |||||
| // Copyright 2023 The casbin Authors. All Rights Reserved. | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| //go:build !skipCi | |||||
| // +build !skipCi | |||||
| package ai | |||||
| import ( | |||||
| "testing" | |||||
| "github.com/casbin/casibase/object" | |||||
| "github.com/casbin/casibase/proxy" | |||||
| "github.com/sashabaranov/go-openai" | |||||
| ) | |||||
| func TestRun(t *testing.T) { | |||||
| object.InitConfig() | |||||
| proxy.InitHttpClient() | |||||
| text, err := queryAnswer("", "hi", 5) | |||||
| if err != nil { | |||||
| panic(err) | |||||
| } | |||||
| println(text) | |||||
| } | |||||
| func TestToken(t *testing.T) { | |||||
| println(getTokenSize(openai.GPT3TextDavinci003, "")) | |||||
| } | |||||
| @@ -0,0 +1,28 @@ | |||||
| // Copyright 2023 The casbin Authors. All Rights Reserved. | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| package ai | |||||
| import ( | |||||
| "github.com/casbin/casibase/proxy" | |||||
| "github.com/sashabaranov/go-openai" | |||||
| ) | |||||
| func getProxyClientFromToken(authToken string) *openai.Client { | |||||
| config := openai.DefaultConfig(authToken) | |||||
| config.HTTPClient = proxy.ProxyHttpClient | |||||
| c := openai.NewClientWithConfig(config) | |||||
| return c | |||||
| } | |||||
| @@ -0,0 +1,28 @@ | |||||
| // Copyright 2023 The casbin Authors. All Rights Reserved. | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| package ai | |||||
| import "github.com/pkoukk/tiktoken-go" | |||||
| func getTokenSize(model string, prompt string) (int, error) { | |||||
| tkm, err := tiktoken.EncodingForModel(model) | |||||
| if err != nil { | |||||
| return 0, err | |||||
| } | |||||
| token := tkm.Encode(prompt, nil, nil) | |||||
| res := len(token) | |||||
| return res, nil | |||||
| } | |||||
| @@ -15,4 +15,5 @@ casdoorDbName = casdoor | |||||
| casdoorOrganization = "casbin" | casdoorOrganization = "casbin" | ||||
| casdoorApplication = "app-casibase" | casdoorApplication = "app-casibase" | ||||
| cacheDir = "C:/casibase_cache" | cacheDir = "C:/casibase_cache" | ||||
| appDir = "" | |||||
| appDir = "" | |||||
| socks5Proxy = "127.0.0.1:7890" | |||||
| @@ -0,0 +1,119 @@ | |||||
| // Copyright 2023 The casbin Authors. All Rights Reserved. | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| package conf | |||||
| import ( | |||||
| "os" | |||||
| "runtime" | |||||
| "strconv" | |||||
| "strings" | |||||
| "github.com/astaxie/beego" | |||||
| ) | |||||
| func init() { | |||||
| // this array contains the beego configuration items that may be modified via env | |||||
| presetConfigItems := []string{"httpport", "appname"} | |||||
| for _, key := range presetConfigItems { | |||||
| if value, ok := os.LookupEnv(key); ok { | |||||
| err := beego.AppConfig.Set(key, value) | |||||
| if err != nil { | |||||
| panic(err) | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| func GetConfigString(key string) string { | |||||
| if value, ok := os.LookupEnv(key); ok { | |||||
| return value | |||||
| } | |||||
| res := beego.AppConfig.String(key) | |||||
| if res == "" { | |||||
| if key == "staticBaseUrl" { | |||||
| res = "https://cdn.casbin.org" | |||||
| } else if key == "logConfig" { | |||||
| res = "{\"filename\": \"logs/casdoor.log\", \"maxdays\":99999, \"perm\":\"0770\"}" | |||||
| } | |||||
| } | |||||
| return res | |||||
| } | |||||
| func GetConfigBool(key string) bool { | |||||
| value := GetConfigString(key) | |||||
| if value == "true" { | |||||
| return true | |||||
| } else { | |||||
| return false | |||||
| } | |||||
| } | |||||
| func GetConfigInt64(key string) (int64, error) { | |||||
| value := GetConfigString(key) | |||||
| num, err := strconv.ParseInt(value, 10, 64) | |||||
| return num, err | |||||
| } | |||||
| func GetConfigDataSourceName() string { | |||||
| dataSourceName := GetConfigString("dataSourceName") | |||||
| runningInDocker := os.Getenv("RUNNING_IN_DOCKER") | |||||
| if runningInDocker == "true" { | |||||
| // https://stackoverflow.com/questions/48546124/what-is-linux-equivalent-of-host-docker-internal | |||||
| if runtime.GOOS == "linux" { | |||||
| dataSourceName = strings.ReplaceAll(dataSourceName, "localhost", "172.17.0.1") | |||||
| } else { | |||||
| dataSourceName = strings.ReplaceAll(dataSourceName, "localhost", "host.docker.internal") | |||||
| } | |||||
| } | |||||
| return dataSourceName | |||||
| } | |||||
| func GetLanguage(language string) string { | |||||
| if language == "" || language == "*" { | |||||
| return "en" | |||||
| } | |||||
| if len(language) != 2 || language == "nu" { | |||||
| return "en" | |||||
| } else { | |||||
| return language | |||||
| } | |||||
| } | |||||
| func IsDemoMode() bool { | |||||
| return strings.ToLower(GetConfigString("isDemoMode")) == "true" | |||||
| } | |||||
| func GetConfigBatchSize() int { | |||||
| res, err := strconv.Atoi(GetConfigString("batchSize")) | |||||
| if err != nil { | |||||
| res = 100 | |||||
| } | |||||
| return res | |||||
| } | |||||
| func GetConfigRealDataSourceName(driverName string) string { | |||||
| var dataSourceName string | |||||
| if driverName != "mysql" { | |||||
| dataSourceName = GetConfigDataSourceName() | |||||
| } else { | |||||
| dataSourceName = GetConfigDataSourceName() + GetConfigString("dbName") | |||||
| } | |||||
| return dataSourceName | |||||
| } | |||||
| @@ -16,8 +16,12 @@ package controllers | |||||
| import ( | import ( | ||||
| "encoding/json" | "encoding/json" | ||||
| "fmt" | |||||
| "strings" | |||||
| "github.com/casbin/casibase/ai" | |||||
| "github.com/casbin/casibase/object" | "github.com/casbin/casibase/object" | ||||
| "github.com/casbin/casibase/util" | |||||
| ) | ) | ||||
| func (c *ApiController) GetGlobalMessages() { | func (c *ApiController) GetGlobalMessages() { | ||||
| @@ -32,26 +36,146 @@ func (c *ApiController) GetGlobalMessages() { | |||||
| func (c *ApiController) GetMessages() { | func (c *ApiController) GetMessages() { | ||||
| owner := c.Input().Get("owner") | owner := c.Input().Get("owner") | ||||
| chat := c.Input().Get("chat") | |||||
| messages, err := object.GetMessages(owner) | |||||
| if owner != "" && chat == "" { | |||||
| messages, err := object.GetMessages(owner) | |||||
| if err != nil { | |||||
| c.ResponseError(err.Error()) | |||||
| return | |||||
| } | |||||
| c.ResponseOk(messages) | |||||
| } else if chat != "" && owner == "" { | |||||
| messages, err := object.GetChatMessages(chat) | |||||
| if err != nil { | |||||
| c.ResponseError(err.Error()) | |||||
| return | |||||
| } | |||||
| c.ResponseOk(messages) | |||||
| } else { | |||||
| c.ResponseError("Invalid get messages request") | |||||
| return | |||||
| } | |||||
| } | |||||
| func (c *ApiController) GetMessage() { | |||||
| id := c.Input().Get("id") | |||||
| message, err := object.GetMessage(id) | |||||
| if err != nil { | if err != nil { | ||||
| c.ResponseError(err.Error()) | c.ResponseError(err.Error()) | ||||
| return | return | ||||
| } | } | ||||
| c.ResponseOk(messages) | |||||
| c.ResponseOk(message) | |||||
| } | } | ||||
| func (c *ApiController) GetMessage() { | |||||
| func (c *ApiController) ResponseErrorStream(errorText string) { | |||||
| event := fmt.Sprintf("event: myerror\ndata: %s\n\n", errorText) | |||||
| _, err := c.Ctx.ResponseWriter.Write([]byte(event)) | |||||
| if err != nil { | |||||
| c.ResponseError(err.Error()) | |||||
| return | |||||
| } | |||||
| } | |||||
| func (c *ApiController) GetMessageAnswer() { | |||||
| id := c.Input().Get("id") | id := c.Input().Get("id") | ||||
| c.Ctx.ResponseWriter.Header().Set("Content-Type", "text/event-stream") | |||||
| c.Ctx.ResponseWriter.Header().Set("Cache-Control", "no-cache") | |||||
| c.Ctx.ResponseWriter.Header().Set("Connection", "keep-alive") | |||||
| message, err := object.GetMessage(id) | message, err := object.GetMessage(id) | ||||
| if err != nil { | if err != nil { | ||||
| c.ResponseError(err.Error()) | c.ResponseError(err.Error()) | ||||
| return | return | ||||
| } | } | ||||
| c.ResponseOk(message) | |||||
| if message == nil { | |||||
| c.ResponseErrorStream(fmt.Sprintf("The message: %s is not found", id)) | |||||
| return | |||||
| } | |||||
| if message.Author != "AI" || message.ReplyTo == "" || message.Text != "" { | |||||
| c.ResponseErrorStream("The message is invalid") | |||||
| return | |||||
| } | |||||
| chatId := util.GetIdFromOwnerAndName("admin", message.Chat) | |||||
| chat, err := object.GetChat(chatId) | |||||
| if err != nil { | |||||
| c.ResponseError(err.Error()) | |||||
| return | |||||
| } | |||||
| //if chat == nil || chat.Organization != message.Organization { | |||||
| // c.ResponseErrorStream(fmt.Sprintf("The chat: %s is not found", chatId)) | |||||
| // return | |||||
| //} | |||||
| if chat.Type != "AI" { | |||||
| c.ResponseErrorStream("The chat type must be \"AI\"") | |||||
| return | |||||
| } | |||||
| questionMessage, err := object.GetMessage(message.ReplyTo) | |||||
| if questionMessage == nil { | |||||
| c.ResponseErrorStream(fmt.Sprintf("The message: %s is not found", id)) | |||||
| return | |||||
| } | |||||
| providerId := util.GetIdFromOwnerAndName(chat.Owner, chat.User2) | |||||
| provider, err := object.GetProvider(providerId) | |||||
| if err != nil { | |||||
| c.ResponseError(err.Error()) | |||||
| return | |||||
| } | |||||
| if provider == nil { | |||||
| c.ResponseErrorStream(fmt.Sprintf("The provider: %s is not found", providerId)) | |||||
| return | |||||
| } | |||||
| if provider.Category != "AI" || provider.ClientSecret == "" { | |||||
| c.ResponseErrorStream(fmt.Sprintf("The provider: %s is invalid", providerId)) | |||||
| return | |||||
| } | |||||
| c.Ctx.ResponseWriter.Header().Set("Content-Type", "text/event-stream") | |||||
| c.Ctx.ResponseWriter.Header().Set("Cache-Control", "no-cache") | |||||
| c.Ctx.ResponseWriter.Header().Set("Connection", "keep-alive") | |||||
| authToken := provider.ClientSecret | |||||
| question := questionMessage.Text | |||||
| var stringBuilder strings.Builder | |||||
| fmt.Printf("Question: [%s]\n", questionMessage.Text) | |||||
| fmt.Printf("Answer: [") | |||||
| err = ai.QueryAnswerStream(authToken, question, c.Ctx.ResponseWriter, &stringBuilder) | |||||
| if err != nil { | |||||
| c.ResponseErrorStream(err.Error()) | |||||
| return | |||||
| } | |||||
| fmt.Printf("]\n") | |||||
| event := fmt.Sprintf("event: end\ndata: %s\n\n", "end") | |||||
| _, err = c.Ctx.ResponseWriter.Write([]byte(event)) | |||||
| if err != nil { | |||||
| c.ResponseError(err.Error()) | |||||
| return | |||||
| } | |||||
| answer := stringBuilder.String() | |||||
| message.Text = answer | |||||
| _, err = object.UpdateMessage(message.GetId(), message) | |||||
| if err != nil { | |||||
| c.ResponseError(err.Error()) | |||||
| return | |||||
| } | |||||
| } | } | ||||
| func (c *ApiController) UpdateMessage() { | func (c *ApiController) UpdateMessage() { | ||||
| @@ -81,12 +205,47 @@ func (c *ApiController) AddMessage() { | |||||
| return | return | ||||
| } | } | ||||
| var chat *object.Chat | |||||
| if message.Chat != "" { | |||||
| chatId := util.GetId("admin", message.Chat) | |||||
| chat, err = object.GetChat(chatId) | |||||
| if err != nil { | |||||
| c.ResponseError(err.Error()) | |||||
| return | |||||
| } | |||||
| if chat == nil { | |||||
| c.ResponseError(fmt.Sprintf("chat:The chat: %s is not found", chatId)) | |||||
| return | |||||
| } | |||||
| } | |||||
| success, err := object.AddMessage(&message) | success, err := object.AddMessage(&message) | ||||
| if err != nil { | if err != nil { | ||||
| c.ResponseError(err.Error()) | c.ResponseError(err.Error()) | ||||
| return | return | ||||
| } | } | ||||
| if success { | |||||
| if chat != nil && chat.Type == "AI" { | |||||
| answerMessage := &object.Message{ | |||||
| Owner: message.Owner, | |||||
| Name: fmt.Sprintf("message_%s", util.GetRandomName()), | |||||
| CreatedTime: util.GetCurrentTimeEx(message.CreatedTime), | |||||
| // Organization: message.Organization, | |||||
| Chat: message.Chat, | |||||
| ReplyTo: message.GetId(), | |||||
| Author: "AI", | |||||
| Text: "", | |||||
| } | |||||
| _, err = object.AddMessage(answerMessage) | |||||
| if err != nil { | |||||
| c.ResponseError(err.Error()) | |||||
| return | |||||
| } | |||||
| } | |||||
| } | |||||
| c.ResponseOk(success) | c.ResponseOk(success) | ||||
| } | } | ||||
| @@ -13,8 +13,11 @@ require ( | |||||
| github.com/google/uuid v1.3.0 | github.com/google/uuid v1.3.0 | ||||
| github.com/muesli/clusters v0.0.0-20200529215643-2700303c1762 | github.com/muesli/clusters v0.0.0-20200529215643-2700303c1762 | ||||
| github.com/muesli/kmeans v0.3.0 | github.com/muesli/kmeans v0.3.0 | ||||
| github.com/pkoukk/tiktoken-go v0.1.1 | |||||
| github.com/rclone/rclone v1.63.0 | github.com/rclone/rclone v1.63.0 | ||||
| github.com/sashabaranov/go-openai v1.12.0 | |||||
| github.com/tealeg/xlsx v1.0.5 | github.com/tealeg/xlsx v1.0.5 | ||||
| golang.org/x/net v0.8.0 | |||||
| gonum.org/v1/gonum v0.11.0 | gonum.org/v1/gonum v0.11.0 | ||||
| xorm.io/core v0.7.3 | xorm.io/core v0.7.3 | ||||
| xorm.io/xorm v1.2.5 | xorm.io/xorm v1.2.5 | ||||
| @@ -767,6 +767,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c | |||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= | github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= | ||||
| github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= | ||||
| github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0= | |||||
| github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= | |||||
| github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= | github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= | ||||
| github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= | github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= | ||||
| github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= | github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= | ||||
| @@ -1371,6 +1373,8 @@ github.com/pkg/sftp v1.13.6-0.20230213180117-971c283182b6 h1:5TvW1dv00Y13njmQ1AW | |||||
| github.com/pkg/sftp v1.13.6-0.20230213180117-971c283182b6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= | github.com/pkg/sftp v1.13.6-0.20230213180117-971c283182b6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= | ||||
| github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE= | github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE= | ||||
| github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= | github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= | ||||
| github.com/pkoukk/tiktoken-go v0.1.1 h1:jtkYlIECjyM9OW1w4rjPmTohK4arORP9V25y6TM6nXo= | |||||
| github.com/pkoukk/tiktoken-go v0.1.1/go.mod h1:boMWvk9pQCOTx11pgu0DrIdrAKgQzzJKUP6vLXaz7Rw= | |||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
| github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= | ||||
| @@ -1447,6 +1451,8 @@ github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZ | |||||
| github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= | ||||
| github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= | github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= | ||||
| github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= | github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= | ||||
| github.com/sashabaranov/go-openai v1.12.0 h1:aRNHH0gtVfrpIaEolD0sWrLLRnYQNK4cH/bIAHwL8Rk= | |||||
| github.com/sashabaranov/go-openai v1.12.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= | |||||
| github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= | ||||
| github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= | ||||
| github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= | ||||
| @@ -20,6 +20,7 @@ import ( | |||||
| _ "github.com/astaxie/beego/session/redis" | _ "github.com/astaxie/beego/session/redis" | ||||
| "github.com/casbin/casibase/casdoor" | "github.com/casbin/casibase/casdoor" | ||||
| "github.com/casbin/casibase/object" | "github.com/casbin/casibase/object" | ||||
| "github.com/casbin/casibase/proxy" | |||||
| "github.com/casbin/casibase/routers" | "github.com/casbin/casibase/routers" | ||||
| ) | ) | ||||
| @@ -27,6 +28,8 @@ func main() { | |||||
| object.InitAdapter() | object.InitAdapter() | ||||
| casdoor.InitCasdoorAdapter() | casdoor.InitCasdoorAdapter() | ||||
| proxy.InitHttpClient() | |||||
| beego.InsertFilter("*", beego.BeforeRouter, cors.Allow(&cors.Options{ | beego.InsertFilter("*", beego.BeforeRouter, cors.Allow(&cors.Options{ | ||||
| AllowOrigins: []string{"*"}, | AllowOrigins: []string{"*"}, | ||||
| AllowMethods: []string{"GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS"}, | AllowMethods: []string{"GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS"}, | ||||
| @@ -43,6 +43,16 @@ func GetGlobalMessages() ([]*Message, error) { | |||||
| return messages, nil | return messages, nil | ||||
| } | } | ||||
| func GetChatMessages(chat string) ([]*Message, error) { | |||||
| messages := []*Message{} | |||||
| err := adapter.engine.Asc("created_time").Find(&messages, &Message{Chat: chat}) | |||||
| if err != nil { | |||||
| return messages, err | |||||
| } | |||||
| return messages, nil | |||||
| } | |||||
| func GetMessages(owner string) ([]*Message, error) { | func GetMessages(owner string) ([]*Message, error) { | ||||
| messages := []*Message{} | messages := []*Message{} | ||||
| err := adapter.engine.Desc("created_time").Find(&messages, &Message{Owner: owner}) | err := adapter.engine.Desc("created_time").Find(&messages, &Message{Owner: owner}) | ||||
| @@ -0,0 +1,87 @@ | |||||
| // Copyright 2023 The casbin Authors. All Rights Reserved. | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| package proxy | |||||
| import ( | |||||
| "crypto/tls" | |||||
| "fmt" | |||||
| "net" | |||||
| "net/http" | |||||
| "strings" | |||||
| "time" | |||||
| "github.com/casbin/casibase/conf" | |||||
| "golang.org/x/net/proxy" | |||||
| ) | |||||
| var ( | |||||
| DefaultHttpClient *http.Client | |||||
| ProxyHttpClient *http.Client | |||||
| ) | |||||
| func InitHttpClient() { | |||||
| // not use proxy | |||||
| DefaultHttpClient = http.DefaultClient | |||||
| // use proxy | |||||
| ProxyHttpClient = getProxyHttpClient() | |||||
| } | |||||
| func isAddressOpen(address string) bool { | |||||
| timeout := time.Millisecond * 100 | |||||
| conn, err := net.DialTimeout("tcp", address, timeout) | |||||
| if err != nil { | |||||
| // cannot connect to address, proxy is not active | |||||
| return false | |||||
| } | |||||
| if conn != nil { | |||||
| defer conn.Close() | |||||
| fmt.Printf("Socks5 proxy enabled: %s\n", address) | |||||
| return true | |||||
| } | |||||
| return false | |||||
| } | |||||
| func getProxyHttpClient() *http.Client { | |||||
| socks5Proxy := conf.GetConfigString("socks5Proxy") | |||||
| if socks5Proxy == "" { | |||||
| return &http.Client{} | |||||
| } | |||||
| if !isAddressOpen(socks5Proxy) { | |||||
| return &http.Client{} | |||||
| } | |||||
| // https://stackoverflow.com/questions/33585587/creating-a-go-socks5-client | |||||
| dialer, err := proxy.SOCKS5("tcp", socks5Proxy, nil, proxy.Direct) | |||||
| if err != nil { | |||||
| panic(err) | |||||
| } | |||||
| tr := &http.Transport{Dial: dialer.Dial, TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} | |||||
| return &http.Client{ | |||||
| Transport: tr, | |||||
| } | |||||
| } | |||||
| func GetHttpClient(url string) *http.Client { | |||||
| if strings.Contains(url, "githubusercontent.com") || strings.Contains(url, "googleusercontent.com") { | |||||
| return ProxyHttpClient | |||||
| } else { | |||||
| return DefaultHttpClient | |||||
| } | |||||
| } | |||||
| @@ -90,6 +90,7 @@ func initAPI() { | |||||
| beego.Router("/api/get-global-messages", &controllers.ApiController{}, "GET:GetGlobalMessages") | beego.Router("/api/get-global-messages", &controllers.ApiController{}, "GET:GetGlobalMessages") | ||||
| beego.Router("/api/get-messages", &controllers.ApiController{}, "GET:GetMessages") | beego.Router("/api/get-messages", &controllers.ApiController{}, "GET:GetMessages") | ||||
| beego.Router("/api/get-message", &controllers.ApiController{}, "GET:GetMessage") | beego.Router("/api/get-message", &controllers.ApiController{}, "GET:GetMessage") | ||||
| beego.Router("/api/get-message-answer", &controllers.ApiController{}, "GET:GetMessageAnswer") | |||||
| beego.Router("/api/update-message", &controllers.ApiController{}, "POST:UpdateMessage") | beego.Router("/api/update-message", &controllers.ApiController{}, "POST:UpdateMessage") | ||||
| beego.Router("/api/add-message", &controllers.ApiController{}, "POST:AddMessage") | beego.Router("/api/add-message", &controllers.ApiController{}, "POST:AddMessage") | ||||
| beego.Router("/api/delete-message", &controllers.ApiController{}, "POST:DeleteMessage") | beego.Router("/api/delete-message", &controllers.ApiController{}, "POST:DeleteMessage") | ||||
| @@ -19,8 +19,10 @@ import ( | |||||
| "errors" | "errors" | ||||
| "fmt" | "fmt" | ||||
| "io/ioutil" | "io/ioutil" | ||||
| "math/rand" | |||||
| "strconv" | "strconv" | ||||
| "strings" | "strings" | ||||
| "time" | |||||
| "github.com/google/uuid" | "github.com/google/uuid" | ||||
| ) | ) | ||||
| @@ -139,3 +141,17 @@ func DecodeBase64(s string) string { | |||||
| return string(res) | return string(res) | ||||
| } | } | ||||
| func GetRandomName() string { | |||||
| rand.Seed(time.Now().UnixNano()) | |||||
| const charset = "0123456789abcdefghijklmnopqrstuvwxyz" | |||||
| result := make([]byte, 6) | |||||
| for i := range result { | |||||
| result[i] = charset[rand.Intn(len(charset))] | |||||
| } | |||||
| return string(result) | |||||
| } | |||||
| func GetId(owner, name string) string { | |||||
| return fmt.Sprintf("%s/%s", owner, name) | |||||
| } | |||||
| @@ -21,3 +21,17 @@ func GetCurrentTime() string { | |||||
| tm := time.Unix(timestamp, 0) | tm := time.Unix(timestamp, 0) | ||||
| return tm.Format(time.RFC3339) | return tm.Format(time.RFC3339) | ||||
| } | } | ||||
| func GetCurrentTimeEx(timestamp string) string { | |||||
| tm := time.Now() | |||||
| inputTime, err := time.Parse(time.RFC3339, timestamp) | |||||
| if err != nil { | |||||
| panic(err) | |||||
| } | |||||
| if !tm.After(inputTime) { | |||||
| tm = inputTime.Add(1 * time.Millisecond) | |||||
| } | |||||
| return tm.Format("2006-01-02T15:04:05.999Z07:00") | |||||
| } | |||||
| @@ -16,7 +16,7 @@ import React, {Component} from "react"; | |||||
| import {Link, Redirect, Route, Switch, withRouter} from "react-router-dom"; | import {Link, Redirect, Route, Switch, withRouter} from "react-router-dom"; | ||||
| import {StyleProvider, legacyLogicalPropertiesTransformer} from "@ant-design/cssinjs"; | import {StyleProvider, legacyLogicalPropertiesTransformer} from "@ant-design/cssinjs"; | ||||
| import {Avatar, Button, Card, ConfigProvider, Drawer, Dropdown, FloatButton, Layout, Menu} from "antd"; | import {Avatar, Button, Card, ConfigProvider, Drawer, Dropdown, FloatButton, Layout, Menu} from "antd"; | ||||
| import {BarsOutlined, DownOutlined, LogoutOutlined, SettingOutlined} from "@ant-design/icons"; | |||||
| import {BarsOutlined, CommentOutlined, DownOutlined, LogoutOutlined, SettingOutlined} from "@ant-design/icons"; | |||||
| import "./App.less"; | import "./App.less"; | ||||
| import * as Setting from "./Setting"; | import * as Setting from "./Setting"; | ||||
| import * as AccountBackend from "./backend/AccountBackend"; | import * as AccountBackend from "./backend/AccountBackend"; | ||||
| @@ -45,6 +45,7 @@ import ChatEditPage from "./ChatEditPage"; | |||||
| import ChatListPage from "./ChatListPage"; | import ChatListPage from "./ChatListPage"; | ||||
| import MessageListPage from "./MessageListPage"; | import MessageListPage from "./MessageListPage"; | ||||
| import MessageEditPage from "./MessageEditPage"; | import MessageEditPage from "./MessageEditPage"; | ||||
| import ChatPage from "./ChatPage"; | |||||
| const {Header, Footer, Content} = Layout; | const {Header, Footer, Content} = Layout; | ||||
| @@ -198,14 +199,19 @@ class App extends Component { | |||||
| items.push(Setting.getItem(<><SettingOutlined /> {i18next.t("account:My Account")}</>, | items.push(Setting.getItem(<><SettingOutlined /> {i18next.t("account:My Account")}</>, | ||||
| "/account" | "/account" | ||||
| )); | )); | ||||
| items.push(Setting.getItem(<><CommentOutlined /> {i18next.t("account:Chats & Messages")}</>, | |||||
| "/chat" | |||||
| )); | |||||
| items.push(Setting.getItem(<><LogoutOutlined /> {i18next.t("account:Sign Out")}</>, | items.push(Setting.getItem(<><LogoutOutlined /> {i18next.t("account:Sign Out")}</>, | ||||
| "/logout" | "/logout" | ||||
| )); | )); | ||||
| const onClick = ({e}) => { | |||||
| const onClick = (e) => { | |||||
| if (e.key === "/account") { | if (e.key === "/account") { | ||||
| Setting.openLink(Setting.getMyProfileUrl(this.state.account)); | Setting.openLink(Setting.getMyProfileUrl(this.state.account)); | ||||
| } else if (e.key === "/logout") { | } else if (e.key === "/logout") { | ||||
| this.signout(); | this.signout(); | ||||
| } else if (e.key === "/chat") { | |||||
| this.props.history.push("/chat"); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -374,10 +380,15 @@ class App extends Component { | |||||
| <Route exact path="/chats/:chatName" render={(props) => this.renderSigninIfNotSignedIn(<ChatEditPage account={this.state.account} {...props} />)} /> | <Route exact path="/chats/:chatName" render={(props) => this.renderSigninIfNotSignedIn(<ChatEditPage account={this.state.account} {...props} />)} /> | ||||
| <Route exact path="/messages" render={(props) => this.renderSigninIfNotSignedIn(<MessageListPage account={this.state.account} {...props} />)} /> | <Route exact path="/messages" render={(props) => this.renderSigninIfNotSignedIn(<MessageListPage account={this.state.account} {...props} />)} /> | ||||
| <Route exact path="/messages/:messageName" render={(props) => this.renderSigninIfNotSignedIn(<MessageEditPage account={this.state.account} {...props} />)} /> | <Route exact path="/messages/:messageName" render={(props) => this.renderSigninIfNotSignedIn(<MessageEditPage account={this.state.account} {...props} />)} /> | ||||
| <Route exact path="/chat" render={(props) => this.renderSigninIfNotSignedIn(<ChatPage account={this.state.account} {...props} />)} /> | |||||
| </Switch> | </Switch> | ||||
| ); | ); | ||||
| } | } | ||||
| isWithoutCard() { | |||||
| return Setting.isMobile() || window.location.pathname === "/chat"; | |||||
| } | |||||
| renderContent() { | renderContent() { | ||||
| const onClick = ({key}) => { | const onClick = ({key}) => { | ||||
| this.props.history.push(key); | this.props.history.push(key); | ||||
| @@ -420,9 +431,12 @@ class App extends Component { | |||||
| } | } | ||||
| </Header> | </Header> | ||||
| <Content style={{display: "flex", flexDirection: "column"}}> | <Content style={{display: "flex", flexDirection: "column"}}> | ||||
| <Card className="content-warp-card"> | |||||
| {this.renderRouter()} | |||||
| </Card> | |||||
| {this.isWithoutCard() ? | |||||
| this.renderRouter() : | |||||
| <Card className="content-warp-card"> | |||||
| {this.renderRouter()} | |||||
| </Card> | |||||
| } | |||||
| </Content> | </Content> | ||||
| {this.renderFooter()} | {this.renderFooter()} | ||||
| </Layout> | </Layout> | ||||
| @@ -93,6 +93,7 @@ img { | |||||
| border-radius: 7px; | border-radius: 7px; | ||||
| float: right; | float: right; | ||||
| cursor: pointer; | cursor: pointer; | ||||
| margin-right: 3px; | |||||
| &:hover { | &:hover { | ||||
| background-color: #f5f5f5; | background-color: #f5f5f5; | ||||
| @@ -0,0 +1,54 @@ | |||||
| // Copyright 2023 The casbin Authors. All Rights Reserved. | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| import React from "react"; | |||||
| import {Button, Result} from "antd"; | |||||
| import i18next from "i18next"; | |||||
| class BaseListPage extends React.Component { | |||||
| constructor(props) { | |||||
| super(props); | |||||
| this.state = { | |||||
| classes: props, | |||||
| data: [], | |||||
| loading: false, | |||||
| searchText: "", | |||||
| searchedColumn: "", | |||||
| isAuthorized: true, | |||||
| }; | |||||
| } | |||||
| render() { | |||||
| if (!this.state.isAuthorized) { | |||||
| return ( | |||||
| <Result | |||||
| status="403" | |||||
| title="403 Unauthorized" | |||||
| subTitle={i18next.t("general:Sorry, you do not have permission to access this page or logged in status invalid.")} | |||||
| extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>} | |||||
| /> | |||||
| ); | |||||
| } | |||||
| return ( | |||||
| <div> | |||||
| { | |||||
| this.renderTable(this.state.data) | |||||
| } | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| } | |||||
| export default BaseListPage; | |||||
| @@ -0,0 +1,219 @@ | |||||
| // Copyright 2023 The casbin Authors. All Rights Reserved. | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| import React from "react"; | |||||
| import {Alert, Avatar, Input, List, Spin} from "antd"; | |||||
| import {CopyOutlined, DislikeOutlined, LikeOutlined, SendOutlined} from "@ant-design/icons"; | |||||
| import i18next from "i18next"; | |||||
| const {TextArea} = Input; | |||||
| class ChatBox extends React.Component { | |||||
| constructor(props) { | |||||
| super(props); | |||||
| this.state = { | |||||
| inputValue: "", | |||||
| }; | |||||
| this.listContainerRef = React.createRef(); | |||||
| } | |||||
| componentDidUpdate(prevProps) { | |||||
| if (prevProps.messages !== this.props.messages && this.props.messages !== undefined && this.props.messages !== null) { | |||||
| this.scrollToListItem(this.props.messages.length); | |||||
| } | |||||
| } | |||||
| handleKeyDown = (e) => { | |||||
| if (e.key === "Enter" && !e.shiftKey) { | |||||
| e.preventDefault(); | |||||
| if (this.state.inputValue !== "") { | |||||
| this.send(this.state.inputValue); | |||||
| this.setState({inputValue: ""}); | |||||
| } | |||||
| } | |||||
| }; | |||||
| scrollToListItem(index) { | |||||
| const listContainerElement = this.listContainerRef.current; | |||||
| if (!listContainerElement) { | |||||
| return; | |||||
| } | |||||
| const targetItem = listContainerElement.querySelector( | |||||
| `#chatbox-list-item-${index}` | |||||
| ); | |||||
| if (!targetItem) { | |||||
| return; | |||||
| } | |||||
| const scrollDistance = targetItem.offsetTop - listContainerElement.offsetTop; | |||||
| listContainerElement.scrollTo({ | |||||
| top: scrollDistance, | |||||
| behavior: "smooth", | |||||
| }); | |||||
| } | |||||
| send = (text) => { | |||||
| this.props.sendMessage(text); | |||||
| this.setState({inputValue: ""}); | |||||
| }; | |||||
| renderText(text) { | |||||
| const lines = text.split("\n").map((line, index) => ( | |||||
| <React.Fragment key={index}> | |||||
| {line} | |||||
| <br /> | |||||
| </React.Fragment> | |||||
| )); | |||||
| return <div>{lines}</div>; | |||||
| } | |||||
| renderList() { | |||||
| if (this.props.messages === undefined || this.props.messages === null) { | |||||
| return ( | |||||
| <div style={{display: "flex", justifyContent: "center", alignItems: "center"}}> | |||||
| <Spin size="large" tip={i18next.t("login:Loading")} style={{paddingTop: "20%"}} /> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| return ( | |||||
| <React.Fragment> | |||||
| <div ref={this.listContainerRef} style={{position: "relative", maxHeight: "calc(100vh - 140px)", overflowY: "auto"}}> | |||||
| <List | |||||
| itemLayout="horizontal" | |||||
| dataSource={[...this.props.messages, {}]} | |||||
| renderItem={(item, index) => { | |||||
| if (Object.keys(item).length === 0 && item.constructor === Object) { | |||||
| return <List.Item id={`chatbox-list-item-${index}`} style={{ | |||||
| height: "160px", | |||||
| backgroundColor: index % 2 === 0 ? "white" : "rgb(247,247,248)", | |||||
| borderBottom: "1px solid rgb(229, 229, 229)", | |||||
| position: "relative", | |||||
| }} />; | |||||
| } | |||||
| return ( | |||||
| <List.Item id={`chatbox-list-item-${index}`} style={{ | |||||
| backgroundColor: index % 2 === 0 ? "white" : "rgb(247,247,248)", | |||||
| borderBottom: "1px solid rgb(229, 229, 229)", | |||||
| position: "relative", | |||||
| }}> | |||||
| <div style={{width: "800px", margin: "0 auto", position: "relative"}}> | |||||
| <List.Item.Meta | |||||
| avatar={<Avatar style={{width: "30px", height: "30px", borderRadius: "3px"}} src={item.author === `${this.props.account.owner}/${this.props.account.name}` ? this.props.account.avatar : "https://cdn.casbin.com/casdoor/resource/built-in/admin/gpt.png"} />} | |||||
| title={ | |||||
| <div style={{fontSize: "16px", fontWeight: "normal", lineHeight: "24px", marginTop: "-15px", marginLeft: "5px", marginRight: "80px"}}> | |||||
| { | |||||
| !item.text.includes("#ERROR#") ? this.renderText(item.text) : ( | |||||
| <Alert message={item.text.slice("#ERROR#: ".length)} type="error" showIcon /> | |||||
| ) | |||||
| } | |||||
| </div> | |||||
| } | |||||
| /> | |||||
| <div style={{position: "absolute", top: "0px", right: "0px"}}> | |||||
| <CopyOutlined style={{color: "rgb(172,172,190)", margin: "5px"}} /> | |||||
| <LikeOutlined style={{color: "rgb(172,172,190)", margin: "5px"}} /> | |||||
| <DislikeOutlined style={{color: "rgb(172,172,190)", margin: "5px"}} /> | |||||
| </div> | |||||
| </div> | |||||
| </List.Item> | |||||
| ); | |||||
| }} | |||||
| /> | |||||
| </div> | |||||
| <div style={{ | |||||
| position: "absolute", | |||||
| bottom: 0, | |||||
| left: 0, | |||||
| right: 0, | |||||
| height: "120px", | |||||
| background: "linear-gradient(transparent 0%, rgba(255, 255, 255, 0.8) 50%, white 100%)", | |||||
| pointerEvents: "none", | |||||
| }} /> | |||||
| </React.Fragment> | |||||
| ); | |||||
| } | |||||
| renderInput() { | |||||
| return ( | |||||
| <div | |||||
| style={{ | |||||
| position: "fixed", | |||||
| bottom: "90px", | |||||
| width: "100%", | |||||
| display: "flex", | |||||
| justifyContent: "center", | |||||
| }} | |||||
| > | |||||
| <div style={{position: "relative", width: "760px", marginLeft: "-280px"}}> | |||||
| <TextArea | |||||
| placeholder={"Send a message..."} | |||||
| autoSize={{maxRows: 8}} | |||||
| value={this.state.inputValue} | |||||
| onChange={(e) => this.setState({inputValue: e.target.value})} | |||||
| onKeyDown={this.handleKeyDown} | |||||
| style={{ | |||||
| fontSize: "16px", | |||||
| fontWeight: "normal", | |||||
| lineHeight: "24px", | |||||
| width: "770px", | |||||
| height: "48px", | |||||
| borderRadius: "6px", | |||||
| borderColor: "rgb(229,229,229)", | |||||
| boxShadow: "0 0 15px rgba(0, 0, 0, 0.1)", | |||||
| paddingLeft: "17px", | |||||
| paddingRight: "17px", | |||||
| paddingTop: "12px", | |||||
| paddingBottom: "12px", | |||||
| }} | |||||
| suffix={<SendOutlined style={{color: "rgb(210,210,217"}} onClick={() => this.send(this.state.inputValue)} />} | |||||
| autoComplete="off" | |||||
| /> | |||||
| <SendOutlined | |||||
| style={{ | |||||
| color: this.state.inputValue === "" ? "rgb(210,210,217)" : "rgb(142,142,160)", | |||||
| position: "absolute", | |||||
| bottom: "17px", | |||||
| right: "17px", | |||||
| }} | |||||
| onClick={() => this.send(this.state.inputValue)} | |||||
| /> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| render() { | |||||
| return ( | |||||
| <div> | |||||
| { | |||||
| this.renderList() | |||||
| } | |||||
| { | |||||
| this.renderInput() | |||||
| } | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| } | |||||
| export default ChatBox; | |||||
| @@ -0,0 +1,180 @@ | |||||
| // Copyright 2023 The casbin Authors. All Rights Reserved. | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| import React from "react"; | |||||
| import {Button, Menu} from "antd"; | |||||
| import {DeleteOutlined, LayoutOutlined, PlusOutlined} from "@ant-design/icons"; | |||||
| class ChatMenu extends React.Component { | |||||
| constructor(props) { | |||||
| super(props); | |||||
| const items = this.chatsToItems(this.props.chats); | |||||
| const openKeys = items.map((item) => item.key); | |||||
| this.state = { | |||||
| openKeys: openKeys, | |||||
| selectedKeys: ["0-0"], | |||||
| }; | |||||
| } | |||||
| chatsToItems(chats) { | |||||
| const categories = {}; | |||||
| chats.forEach((chat) => { | |||||
| if (!categories[chat.category]) { | |||||
| categories[chat.category] = []; | |||||
| } | |||||
| categories[chat.category].push(chat); | |||||
| }); | |||||
| const selectedKeys = this.state === undefined ? [] : this.state.selectedKeys; | |||||
| return Object.keys(categories).map((category, index) => { | |||||
| return { | |||||
| key: `${index}`, | |||||
| icon: <LayoutOutlined />, | |||||
| label: category, | |||||
| children: categories[category].map((chat, chatIndex) => { | |||||
| const globalChatIndex = chats.indexOf(chat); | |||||
| const isSelected = selectedKeys.includes(`${index}-${chatIndex}`); | |||||
| return { | |||||
| key: `${index}-${chatIndex}`, | |||||
| index: globalChatIndex, | |||||
| label: ( | |||||
| <div | |||||
| className="menu-item-container" | |||||
| style={{ | |||||
| display: "flex", | |||||
| justifyContent: "space-between", | |||||
| alignItems: "center", | |||||
| }} | |||||
| > | |||||
| {chat.displayName} | |||||
| {isSelected && ( | |||||
| <DeleteOutlined | |||||
| className="menu-item-delete-icon" | |||||
| style={{ | |||||
| visibility: "visible", | |||||
| color: "inherit", | |||||
| transition: "color 0.3s", | |||||
| }} | |||||
| onMouseEnter={(e) => { | |||||
| e.currentTarget.style.color = "rgba(89,54,213,0.6)"; | |||||
| }} | |||||
| onMouseLeave={(e) => { | |||||
| e.currentTarget.style.color = "inherit"; | |||||
| }} | |||||
| onMouseDown={(e) => { | |||||
| e.currentTarget.style.color = "rgba(89,54,213,0.4)"; | |||||
| }} | |||||
| onMouseUp={(e) => { | |||||
| e.currentTarget.style.color = "rgba(89,54,213,0.6)"; | |||||
| }} | |||||
| onClick={(e) => { | |||||
| e.stopPropagation(); | |||||
| if (this.props.onDeleteChat) { | |||||
| this.props.onDeleteChat(globalChatIndex); | |||||
| } | |||||
| }} | |||||
| /> | |||||
| )} | |||||
| </div> | |||||
| ), | |||||
| }; | |||||
| }), | |||||
| }; | |||||
| }); | |||||
| } | |||||
| onSelect = (info) => { | |||||
| const [categoryIndex, chatIndex] = info.selectedKeys[0].split("-").map(Number); | |||||
| const selectedItem = this.chatsToItems(this.props.chats)[categoryIndex].children[chatIndex]; | |||||
| this.setState({ | |||||
| selectedKeys: [`${categoryIndex}-${chatIndex}`], | |||||
| }); | |||||
| if (this.props.onSelectChat) { | |||||
| this.props.onSelectChat(selectedItem.index); | |||||
| } | |||||
| }; | |||||
| getRootSubmenuKeys(items) { | |||||
| return items.map((item, index) => `${index}`); | |||||
| } | |||||
| setSelectedKeyToNewChat(chats) { | |||||
| const items = this.chatsToItems(chats); | |||||
| const openKeys = items.map((item) => item.key); | |||||
| this.setState({ | |||||
| openKeys: openKeys, | |||||
| selectedKeys: ["0-0"], | |||||
| }); | |||||
| } | |||||
| onOpenChange = (keys) => { | |||||
| const items = this.chatsToItems(this.props.chats); | |||||
| const rootSubmenuKeys = this.getRootSubmenuKeys(items); | |||||
| const latestOpenKey = keys.find((key) => this.state.openKeys.indexOf(key) === -1); | |||||
| if (rootSubmenuKeys.indexOf(latestOpenKey) === -1) { | |||||
| this.setState({openKeys: keys}); | |||||
| } else { | |||||
| this.setState({openKeys: latestOpenKey ? [latestOpenKey] : []}); | |||||
| } | |||||
| }; | |||||
| render() { | |||||
| const items = this.chatsToItems(this.props.chats); | |||||
| return ( | |||||
| <div> | |||||
| <Button | |||||
| icon={<PlusOutlined />} | |||||
| style={{ | |||||
| width: "calc(100% - 8px)", | |||||
| height: "40px", | |||||
| margin: "4px", | |||||
| borderColor: "rgb(229,229,229)", | |||||
| }} | |||||
| onMouseEnter={(e) => { | |||||
| e.currentTarget.style.borderColor = "rgba(89,54,213,0.6)"; | |||||
| }} | |||||
| onMouseLeave={(e) => { | |||||
| e.currentTarget.style.borderColor = "rgba(0, 0, 0, 0.1)"; | |||||
| }} | |||||
| onMouseDown={(e) => { | |||||
| e.currentTarget.style.borderColor = "rgba(89,54,213,0.4)"; | |||||
| }} | |||||
| onMouseUp={(e) => { | |||||
| e.currentTarget.style.borderColor = "rgba(89,54,213,0.6)"; | |||||
| }} | |||||
| onClick={this.props.onAddChat} | |||||
| > | |||||
| New Chat | |||||
| </Button> | |||||
| <Menu | |||||
| style={{maxHeight: "calc(100vh - 140px - 40px - 8px)", overflowY: "auto"}} | |||||
| mode="inline" | |||||
| openKeys={this.state.openKeys} | |||||
| selectedKeys={this.state.selectedKeys} | |||||
| onOpenChange={this.onOpenChange} | |||||
| onSelect={this.onSelect} | |||||
| items={items} | |||||
| /> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| } | |||||
| export default ChatMenu; | |||||
| @@ -0,0 +1,286 @@ | |||||
| // Copyright 2023 The casbin Authors. All Rights Reserved. | |||||
| // | |||||
| // Licensed under the Apache License, Version 2.0 (the "License"); | |||||
| // you may not use this file except in compliance with the License. | |||||
| // You may obtain a copy of the License at | |||||
| // | |||||
| // http://www.apache.org/licenses/LICENSE-2.0 | |||||
| // | |||||
| // Unless required by applicable law or agreed to in writing, software | |||||
| // distributed under the License is distributed on an "AS IS" BASIS, | |||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
| // See the License for the specific language governing permissions and | |||||
| // limitations under the License. | |||||
| import React from "react"; | |||||
| import {Spin} from "antd"; | |||||
| import moment from "moment"; | |||||
| import ChatMenu from "./ChatMenu"; | |||||
| import ChatBox from "./ChatBox"; | |||||
| import * as Setting from "./Setting"; | |||||
| import * as ChatBackend from "./backend/ChatBackend"; | |||||
| import * as MessageBackend from "./backend/MessageBackend"; | |||||
| import i18next from "i18next"; | |||||
| import BaseListPage from "./BaseListPage"; | |||||
| class ChatPage extends BaseListPage { | |||||
| constructor(props) { | |||||
| super(props); | |||||
| this.menu = React.createRef(); | |||||
| } | |||||
| UNSAFE_componentWillMount() { | |||||
| this.setState({ | |||||
| loading: true, | |||||
| }); | |||||
| this.fetch(); | |||||
| } | |||||
| newChat(chat) { | |||||
| const randomName = Setting.getRandomName(); | |||||
| return { | |||||
| owner: "admin", // this.props.account.applicationName, | |||||
| name: `chat_${randomName}`, | |||||
| createdTime: moment().format(), | |||||
| updatedTime: moment().format(), | |||||
| // organization: this.props.account.owner, | |||||
| displayName: `New Chat - ${randomName}`, | |||||
| type: "AI", | |||||
| category: chat !== undefined ? chat.category : "Chat Category - 1", | |||||
| user1: `${this.props.account.owner}/${this.props.account.name}`, | |||||
| user2: "", | |||||
| users: [`${this.props.account.owner}/${this.props.account.name}`], | |||||
| messageCount: 0, | |||||
| }; | |||||
| } | |||||
| newMessage(text) { | |||||
| const randomName = Setting.getRandomName(); | |||||
| return { | |||||
| owner: this.props.account.owner, // this.props.account.messagename, | |||||
| name: `message_${randomName}`, | |||||
| createdTime: moment().format(), | |||||
| // organization: this.props.account.owner, | |||||
| chat: this.state.chatName, | |||||
| replyTo: "", | |||||
| author: `${this.props.account.owner}/${this.props.account.name}`, | |||||
| text: text, | |||||
| }; | |||||
| } | |||||
| sendMessage(text) { | |||||
| const newMessage = this.newMessage(text); | |||||
| MessageBackend.addMessage(newMessage) | |||||
| .then((res) => { | |||||
| if (res.status === "ok") { | |||||
| this.getMessages(this.state.chatName); | |||||
| } else { | |||||
| Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`); | |||||
| } | |||||
| }) | |||||
| .catch(error => { | |||||
| Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`); | |||||
| }); | |||||
| } | |||||
| getMessages(chatName) { | |||||
| MessageBackend.getChatMessages(chatName) | |||||
| .then((res) => { | |||||
| this.setState({ | |||||
| messages: res.data, | |||||
| }); | |||||
| if (res.data.length > 0) { | |||||
| const lastMessage = res.data[res.data.length - 1]; | |||||
| if (lastMessage.author === "AI" && lastMessage.replyTo !== "" && lastMessage.text === "") { | |||||
| let text = ""; | |||||
| MessageBackend.getMessageAnswer(lastMessage.owner, lastMessage.name, (data) => { | |||||
| if (data === "") { | |||||
| data = "\n"; | |||||
| } | |||||
| const lastMessage2 = Setting.deepCopy(lastMessage); | |||||
| text += data; | |||||
| lastMessage2.text = text; | |||||
| res.data[res.data.length - 1] = lastMessage2; | |||||
| this.setState({ | |||||
| messages: res.data, | |||||
| }); | |||||
| }, (error) => { | |||||
| Setting.showMessage("error", `${i18next.t("general:Failed to get answer")}: ${error}`); | |||||
| const lastMessage2 = Setting.deepCopy(lastMessage); | |||||
| lastMessage2.text = `#ERROR#: ${error}`; | |||||
| res.data[res.data.length - 1] = lastMessage2; | |||||
| this.setState({ | |||||
| messages: res.data, | |||||
| }); | |||||
| }); | |||||
| } | |||||
| } | |||||
| Setting.scrollToDiv(`chatbox-list-item-${res.data.length}`); | |||||
| }); | |||||
| } | |||||
| addChat(chat) { | |||||
| const newChat = this.newChat(chat); | |||||
| ChatBackend.addChat(newChat) | |||||
| .then((res) => { | |||||
| if (res.status === "ok") { | |||||
| Setting.showMessage("success", i18next.t("general:Successfully added")); | |||||
| this.setState({ | |||||
| chatName: newChat.name, | |||||
| messages: null, | |||||
| }); | |||||
| this.getMessages(newChat.name); | |||||
| this.fetch({}, false); | |||||
| } else { | |||||
| Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`); | |||||
| } | |||||
| }) | |||||
| .catch(error => { | |||||
| Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`); | |||||
| }); | |||||
| } | |||||
| deleteChat(chats, i, chat) { | |||||
| ChatBackend.deleteChat(chat) | |||||
| .then((res) => { | |||||
| if (res.status === "ok") { | |||||
| Setting.showMessage("success", i18next.t("general:Successfully deleted")); | |||||
| const data = Setting.deleteRow(this.state.data, i); | |||||
| const j = Math.min(i, data.length - 1); | |||||
| if (j < 0) { | |||||
| this.setState({ | |||||
| chatName: undefined, | |||||
| messages: [], | |||||
| data: data, | |||||
| }); | |||||
| } else { | |||||
| const focusedChat = data[j]; | |||||
| this.setState({ | |||||
| chatName: focusedChat.name, | |||||
| messages: null, | |||||
| data: data, | |||||
| }); | |||||
| this.getMessages(focusedChat.name); | |||||
| } | |||||
| } else { | |||||
| Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`); | |||||
| } | |||||
| }) | |||||
| .catch(error => { | |||||
| Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`); | |||||
| }); | |||||
| } | |||||
| getCurrentChat() { | |||||
| return this.state.data.filter(chat => chat.name === this.state.chatName)[0]; | |||||
| } | |||||
| renderTable(chats) { | |||||
| const onSelectChat = (i) => { | |||||
| const chat = chats[i]; | |||||
| this.setState({ | |||||
| chatName: chat.name, | |||||
| messages: null, | |||||
| }); | |||||
| this.getMessages(chat.name); | |||||
| }; | |||||
| const onAddChat = () => { | |||||
| const chat = this.getCurrentChat(); | |||||
| this.addChat(chat); | |||||
| }; | |||||
| const onDeleteChat = (i) => { | |||||
| const chat = chats[i]; | |||||
| this.deleteChat(chats, i, chat); | |||||
| }; | |||||
| if (this.state.loading) { | |||||
| return ( | |||||
| <div style={{display: "flex", justifyContent: "center", alignItems: "center"}}> | |||||
| <Spin size="large" tip={i18next.t("login:Loading")} style={{paddingTop: "10%"}} /> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| return ( | |||||
| <div style={{display: "flex", height: "calc(100vh - 136px)"}}> | |||||
| <div style={{width: "250px", height: "100%", backgroundColor: "white", borderRight: "1px solid rgb(245,245,245)", borderBottom: "1px solid rgb(245,245,245)"}}> | |||||
| <ChatMenu ref={this.menu} chats={chats} onSelectChat={onSelectChat} onAddChat={onAddChat} onDeleteChat={onDeleteChat} /> | |||||
| </div> | |||||
| <div style={{flex: 1, height: "100%", backgroundColor: "white", position: "relative"}}> | |||||
| { | |||||
| (this.state.messages === undefined || this.state.messages === null) ? null : ( | |||||
| <div style={{ | |||||
| position: "absolute", | |||||
| top: -50, | |||||
| left: 0, | |||||
| right: 0, | |||||
| bottom: 0, | |||||
| backgroundImage: "url(https://cdn.casbin.org/img/casdoor-logo_1185x256.png)", | |||||
| backgroundPosition: "center", | |||||
| backgroundRepeat: "no-repeat", | |||||
| backgroundSize: "200px auto", | |||||
| backgroundBlendMode: "luminosity", | |||||
| filter: "grayscale(80%) brightness(140%) contrast(90%)", | |||||
| opacity: 0.5, | |||||
| pointerEvents: "none", | |||||
| }}> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| <ChatBox messages={this.state.messages} sendMessage={(text) => {this.sendMessage(text);}} account={this.props.account} /> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| fetch = (params = {}, setLoading = true) => { | |||||
| let field = params.searchedColumn, value = params.searchText; | |||||
| const sortField = params.sortField, sortOrder = params.sortOrder; | |||||
| if (params.category !== undefined && params.category !== null) { | |||||
| field = "category"; | |||||
| value = params.category; | |||||
| } else if (params.type !== undefined && params.type !== null) { | |||||
| field = "type"; | |||||
| value = params.type; | |||||
| } | |||||
| if (setLoading) { | |||||
| this.setState({loading: true}); | |||||
| } | |||||
| ChatBackend.getChats("admin", -1, field, value, sortField, sortOrder) | |||||
| .then((res) => { | |||||
| if (res.status === "ok") { | |||||
| this.setState({ | |||||
| loading: false, | |||||
| data: res.data, | |||||
| messages: [], | |||||
| searchText: params.searchText, | |||||
| searchedColumn: params.searchedColumn, | |||||
| }); | |||||
| const chats = res.data; | |||||
| if (this.state.chatName === undefined && chats.length > 0) { | |||||
| const chat = chats[0]; | |||||
| this.getMessages(chat.name); | |||||
| this.setState({ | |||||
| chatName: chat.name, | |||||
| }); | |||||
| } | |||||
| if (!setLoading) { | |||||
| this.menu.current.setSelectedKeyToNewChat(chats); | |||||
| } | |||||
| } | |||||
| }); | |||||
| }; | |||||
| } | |||||
| export default ChatPage; | |||||
| @@ -630,3 +630,12 @@ export function getOption(label, value) { | |||||
| value, | value, | ||||
| }; | }; | ||||
| } | } | ||||
| export function scrollToDiv(divId) { | |||||
| if (divId) { | |||||
| const ele = document.getElementById(divId); | |||||
| if (ele) { | |||||
| ele.scrollIntoView({behavior: "smooth"}); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -21,8 +21,8 @@ export function getGlobalChats() { | |||||
| }).then(res => res.json()); | }).then(res => res.json()); | ||||
| } | } | ||||
| export function getChats(owner) { | |||||
| return fetch(`${Setting.ServerUrl}/api/get-chats?owner=${owner}`, { | |||||
| export function getChats(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") { | |||||
| return fetch(`${Setting.ServerUrl}/api/get-chats?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, { | |||||
| method: "GET", | method: "GET", | ||||
| credentials: "include", | credentials: "include", | ||||
| }).then(res => res.json()); | }).then(res => res.json()); | ||||
| @@ -28,6 +28,30 @@ export function getMessages(owner) { | |||||
| }).then(res => res.json()); | }).then(res => res.json()); | ||||
| } | } | ||||
| export function getChatMessages(chat) { | |||||
| return fetch(`${Setting.ServerUrl}/api/get-messages?chat=${chat}`, { | |||||
| method: "GET", | |||||
| credentials: "include", | |||||
| }).then(res => res.json()); | |||||
| } | |||||
| export function getMessageAnswer(owner, name, onMessage, onError) { | |||||
| const eventSource = new EventSource(`${Setting.ServerUrl}/api/get-message-answer?id=${owner}/${encodeURIComponent(name)}`); | |||||
| eventSource.addEventListener("message", (e) => { | |||||
| onMessage(e.data); | |||||
| }); | |||||
| eventSource.addEventListener("myerror", (e) => { | |||||
| onError(e.data); | |||||
| eventSource.close(); | |||||
| }); | |||||
| eventSource.addEventListener("end", (e) => { | |||||
| eventSource.close(); | |||||
| }); | |||||
| } | |||||
| export function getMessage(owner, name) { | export function getMessage(owner, name) { | ||||
| return fetch(`${Setting.ServerUrl}/api/get-message?id=${owner}/${encodeURIComponent(name)}`, { | return fetch(`${Setting.ServerUrl}/api/get-message?id=${owner}/${encodeURIComponent(name)}`, { | ||||
| method: "GET", | method: "GET", | ||||