From 2906a372d5791307f9f6090953fd46697b4793e0 Mon Sep 17 00:00:00 2001 From: tyler Date: Thu, 2 May 2024 15:30:25 -0400 Subject: [PATCH] Added chatbot functionality --- NOTES.md | 9 + v1/app.go | 259 ++++- .../src/assets/icons/Font-Awesome/robot.png | Bin 0 -> 3516 bytes .../src/assets/icons/twbs/chevron-left.png | Bin 0 -> 2220 bytes v1/frontend/src/assets/index.js | 4 + v1/frontend/src/components/ChatBot.css | 360 +++++++ v1/frontend/src/components/ChatBot.jsx | 962 ++++++++++++++++++ v1/frontend/src/components/Modal.jsx | 2 +- v1/frontend/src/components/PageDetails.css | 2 +- v1/frontend/src/components/PageDetails.jsx | 3 + v1/frontend/src/components/PageSideBar.jsx | 8 +- v1/frontend/src/screens/Dashboard.jsx | 3 +- v1/go.mod | 8 +- v1/go.sum | 16 +- v1/internal/events/api.go | 3 +- v1/internal/events/chat.go | 205 ++++ v1/internal/events/producers.go | 9 + v1/internal/models/account.go | 2 +- v1/internal/models/channel.go | 2 +- v1/internal/models/chatbot.go | 313 ++++++ v1/internal/models/chatbotrule.go | 224 ++++ v1/internal/models/error.go | 6 + v1/internal/models/services.go | 10 + .../rumble-livestream-lib-go/chat.go | 83 +- .../rumble-livestream-lib-go/client.go | 20 +- .../rumble-livestream-lib-go/livestream.go | 2 +- v1/vendor/golang.org/x/net/http2/frame.go | 31 + v1/vendor/golang.org/x/net/http2/pipe.go | 11 +- v1/vendor/golang.org/x/net/http2/server.go | 13 +- v1/vendor/golang.org/x/net/http2/testsync.go | 331 ++++++ v1/vendor/golang.org/x/net/http2/transport.go | 298 ++++-- .../golang.org/x/sys/unix/mmap_nomremap.go | 2 +- .../x/sys/unix/syscall_zos_s390x.go | 8 + .../x/sys/windows/syscall_windows.go | 82 ++ .../golang.org/x/sys/windows/types_windows.go | 24 + .../x/sys/windows/zsyscall_windows.go | 126 ++- v1/vendor/modules.txt | 8 +- 37 files changed, 3274 insertions(+), 175 deletions(-) create mode 100644 v1/frontend/src/assets/icons/Font-Awesome/robot.png create mode 100644 v1/frontend/src/assets/icons/twbs/chevron-left.png create mode 100644 v1/frontend/src/components/ChatBot.css create mode 100644 v1/frontend/src/components/ChatBot.jsx create mode 100644 v1/internal/events/chat.go create mode 100644 v1/internal/models/chatbot.go create mode 100644 v1/internal/models/chatbotrule.go create mode 100644 v1/vendor/golang.org/x/net/http2/testsync.go diff --git a/NOTES.md b/NOTES.md index 6c45dee..5ef6ce9 100644 --- a/NOTES.md +++ b/NOTES.md @@ -1,9 +1,18 @@ # Doing + + Next steps: +- add option to delete bot rules - delete page needs to handle new architecture + - app.producers.Active(*name) instead of app.producers.ApiP.Active(*name) + - app.producers.Stop(*name) - activatePage: verify defer page.activeMu.Unlock does not conflict with display function +For Dashboard page, +- Api or chat producer could error, need to be able to start/restart both handlers on error +- Show user error if api or chat stop/error + On API errors - include backoff multiple, if exceeded then stop API diff --git a/v1/app.go b/v1/app.go index 9d3d1f6..89414e4 100644 --- a/v1/app.go +++ b/v1/app.go @@ -40,10 +40,13 @@ type Page struct { name string } +func (p *Page) staticLiveStreamUrl() string { + return fmt.Sprintf("https://rumble.com%s/live", p.name) +} + // App struct type App struct { - //apiS *ApiService - cancelCtrl context.CancelFunc + cancelProc context.CancelFunc clients map[string]*rumblelivestreamlib.Client clientsMu sync.Mutex displaying string @@ -70,8 +73,6 @@ func NewApp() *App { log.Fatal("error initializing log: ", err) } - //app.apiS = NewApiService(app.logError, app.logInfo) - return app } @@ -94,30 +95,28 @@ func (a *App) log() error { // so we can call the runtime methods func (a *App) startup(wails context.Context) { a.wails = wails - - //a.apiS.Startup(a.apiS.ch) - - // ctx, cancel := context.WithCancel(context.Background()) - // a.cancelCtrl = cancel - - // go a.handle(ctx) } -func (a *App) handle(ctx context.Context) { +func (a *App) process(ctx context.Context) { for { select { case apiE := <-a.producers.ApiP.Ch: - err := a.handleApi(apiE) + err := a.processApi(apiE) if err != nil { a.logError.Println("error handling API event:", err) } + case chatE := <-a.producers.ChatP.Ch: + err := a.processChat(chatE) + if err != nil { + a.logError.Println("error handling chat event:", err) + } case <-ctx.Done(): return } } } -func (a *App) handleApi(event events.Api) error { +func (a *App) processApi(event events.Api) error { if event.Name == "" { return fmt.Errorf("event name is empty") } @@ -134,7 +133,7 @@ func (a *App) handleApi(event events.Api) error { } page.apiSt.activeMu.Lock() - page.apiSt.active = event.Stop + page.apiSt.active = !event.Stop page.apiSt.activeMu.Unlock() if event.Stop { @@ -158,21 +157,17 @@ func (a *App) handleApi(event events.Api) error { return nil } -func (a *App) shutdown(ctx context.Context) { - // if a.apiS != nil && a.apiS.Api != nil { - // err := a.apiS.Shutdown() - // if err != nil { - // a.logError.Println("error shutting down api:", err) - // } +func (a *App) processChat(event events.Chat) error { + return nil +} - // close(a.apiS.ch) - // } +func (a *App) shutdown(ctx context.Context) { err := a.producers.Shutdown() if err != nil { a.logError.Println("error closing event producers:", err) } - a.cancelCtrl() + a.cancelProc() if a.services != nil { err := a.services.Close() @@ -223,8 +218,8 @@ func (a *App) Start() (bool, error) { // runtime.EventsEmit(a.ctx, "StartupMessage", "Checking for updates complete.") ctx, cancel := context.WithCancel(context.Background()) - a.cancelCtrl = cancel - go a.handle(ctx) + a.cancelProc = cancel + go a.process(ctx) signin := true if count > 0 { @@ -238,6 +233,7 @@ func (a *App) initProducers() error { producers, err := events.NewProducers( events.WithLoggers(a.logError, a.logInfo), events.WithApiProducer(), + events.WithChatProducer(), ) if err != nil { return fmt.Errorf("error initializing producers: %v", err) @@ -264,6 +260,7 @@ func (a *App) initServices() error { models.WithAccountService(), models.WithChannelService(), models.WithAccountChannelService(), + models.WithChatbotService(), ) if err != nil { return fmt.Errorf("error initializing services: %v", err) @@ -310,7 +307,9 @@ func (a *App) verifyAccounts() (int, error) { } else { account.Cookies = nil err = a.services.AccountS.Update(&account) - fmt.Errorf("error updating account: %v", err) + if err != nil { + return -1, fmt.Errorf("error updating account: %v", err) + } } } } @@ -319,7 +318,7 @@ func (a *App) verifyAccounts() (int, error) { } func (a *App) AddPage(apiKey string) error { - client := rumblelivestreamlib.Client{StreamKey: apiKey} + client := rumblelivestreamlib.Client{ApiKey: apiKey} resp, err := client.Request() if err != nil { a.logError.Println("error executing api request:", err) @@ -347,6 +346,13 @@ func (a *App) AddPage(apiKey string) error { } } + list, err := a.accountList() + if err != nil { + a.logError.Println("error getting account list:", err) + return fmt.Errorf("Error logging in. Try again.") + } + runtime.EventsEmit(a.wails, "PageSideBarAccounts", list) + return nil } @@ -833,7 +839,6 @@ func (a *App) DeleteAccount(id int64) error { a.logError.Println("error getting account list:", err) return fmt.Errorf("Error deleting account. Try again.") } - runtime.EventsEmit(a.wails, "PageSideBarAccounts", list) return nil @@ -876,7 +881,6 @@ func (a *App) DeleteChannel(id int64) error { a.logError.Println("error getting account list:", err) return fmt.Errorf("Error deleting channel. Try again.") } - runtime.EventsEmit(a.wails, "PageSideBarAccounts", list) return nil @@ -958,7 +962,6 @@ func (a *App) PageStatus(name string) { active := false isLive := false - // resp := a.api.Response(name) a.pagesMu.Lock() defer a.pagesMu.Unlock() page, exists := a.pages[name] @@ -1002,7 +1005,7 @@ func (a *App) UpdateAccountApi(id int64, apiKey string) error { } } - client := rumblelivestreamlib.Client{StreamKey: apiKey} + client := rumblelivestreamlib.Client{ApiKey: apiKey} resp, err := client.Request() if err != nil { a.logError.Println("error executing api request:", err) @@ -1047,7 +1050,7 @@ func (a *App) UpdateChannelApi(id int64, apiKey string) error { } } - client := rumblelivestreamlib.Client{StreamKey: apiKey} + client := rumblelivestreamlib.Client{ApiKey: apiKey} resp, err := client.Request() if err != nil { a.logError.Println("error executing api request:", err) @@ -1067,3 +1070,193 @@ func (a *App) UpdateChannelApi(id int64, apiKey string) error { return nil } + +func (a *App) DeleteChatbot(chatbot *models.Chatbot) error { + if chatbot == nil || chatbot.ID == nil { + return fmt.Errorf("Invalid chatbot. Try again.") + } + + cb, err := a.services.ChatbotS.ByID(*chatbot.ID) + if err != nil { + a.logError.Println("error getting chatbot by ID:", err) + return fmt.Errorf("Error deleting chatbot. Try again.") + } + if cb == nil { + return fmt.Errorf("Chatbot does not exist.") + } + + err = a.services.ChatbotS.Delete(chatbot) + if err != nil { + a.logError.Println("error deleting chatbot:", err) + return fmt.Errorf("Error deleting chatbot. Try again.") + } + + list, err := a.chatbotList() + if err != nil { + a.logError.Println("error getting chatbot list:", err) + return fmt.Errorf("Error deleting chatbot. Try again.") + } + runtime.EventsEmit(a.wails, "ChatbotList", list) + + return nil +} + +func (a *App) NewChatbot(chatbot *models.Chatbot) error { + if chatbot == nil || chatbot.Name == nil { + return fmt.Errorf("Invalid chatbot. Try again.") + } + + cb, err := a.services.ChatbotS.ByName(*chatbot.Name) + if err != nil { + a.logError.Println("error getting chatbot by name:", err) + return fmt.Errorf("Error creating chatbot. Try again.") + } + if cb != nil { + return fmt.Errorf("Chatbot name already exists.") + } + + _, err = a.services.ChatbotS.Create(chatbot) + if err != nil { + a.logError.Println("error creating chatbot:", err) + return fmt.Errorf("Error creating chatbot. Try again.") + } + + list, err := a.chatbotList() + if err != nil { + a.logError.Println("error getting chatbot list:", err) + return fmt.Errorf("Error creating chatbot. Try again.") + } + runtime.EventsEmit(a.wails, "ChatbotList", list) + + return nil +} + +func (a *App) UpdateChatbot(chatbot *models.Chatbot) error { + if chatbot == nil || chatbot.ID == nil || chatbot.Name == nil { + return fmt.Errorf("Invalid chatbot. Try again.") + } + + cb, err := a.services.ChatbotS.ByID(*chatbot.ID) + if err != nil { + a.logError.Println("error getting chatbot by ID:", err) + return fmt.Errorf("Error updating chatbot. Try again.") + } + if cb == nil { + return fmt.Errorf("Chatbot does not exist.") + } + + cbByName, err := a.services.ChatbotS.ByName(*chatbot.Name) + if err != nil { + a.logError.Println("error getting chatbot by Name:", err) + return fmt.Errorf("Error updating chatbot. Try again.") + } + if cbByName != nil && *cbByName.ID != *cb.ID { + return fmt.Errorf("Chatbot name already exists.") + } + + err = a.services.ChatbotS.Update(chatbot) + if err != nil { + a.logError.Println("error updating chatbot:", err) + return fmt.Errorf("Error updating chatbot. Try again.") + } + + list, err := a.chatbotList() + if err != nil { + a.logError.Println("error getting chatbot list:", err) + return fmt.Errorf("Error updating chatbot. Try again.") + } + runtime.EventsEmit(a.wails, "ChatbotList", list) + + return nil +} + +func (a *App) ChatbotList() ([]models.Chatbot, error) { + list, err := a.chatbotList() + if err != nil { + a.logError.Println("error getting chatbot list:", err) + return nil, fmt.Errorf("Error retrieving chatbots. Try restarting.") + } + + return list, nil +} + +func (a *App) chatbotList() ([]models.Chatbot, error) { + list, err := a.services.ChatbotS.All() + if err != nil { + return nil, fmt.Errorf("error querying all chatbots: %v", err) + } + + return list, err +} + +type ChatbotRule struct { + Message *ChatbotRuleMessage `json:"message"` + SendAs *ChatbotRuleSender `json:"send_as"` + Trigger *ChatbotRuleTrigger `json:"trigger"` +} + +type ChatbotRuleMessage struct { + FromFile *ChatbotRuleMessageFile `json:"from_file"` + FromText string `json:"from_text"` +} + +type ChatbotRuleMessageFile struct { + Filepath string `json:"filepath"` + RandomRead bool `json:"random_read"` +} + +type ChatbotRuleSender struct { + Username string `json:"username"` + ChannelID *int `json:"channel_id"` +} + +type ChatbotRuleTrigger struct { + OnCommand *ChatbotRuleTriggerCommand `json:"on_command"` + OnEvent *ChatbotRuleTriggerEvent `json:"on_event"` + OnTimer *time.Duration `json:"on_timer"` +} + +type ChatbotRuleTriggerCommand struct { + Command string `json:"command"` + Restrict *ChatbotRuleTriggerCommandRestriction `json:"restrict"` + Timeout time.Duration `json:"timeout"` +} + +type ChatbotRuleTriggerCommandRestriction struct { + Bypass *ChatbotRuleTriggerCommandRestrictionBypass `json:"bypass"` + ToAdmin bool `json:"to_admin"` + ToFollower bool `json:"to_follower"` + ToMod bool `json:"to_mod"` + ToStreamer bool `json:"to_streamer"` + ToSubscriber bool `json:"to_subscriber"` + ToRant int `json:"to_rant"` +} + +type ChatbotRuleTriggerCommandRestrictionBypass struct { + IfAdmin bool `json:"if_admin"` + IfMod bool `json:"if_mod"` + IfStreamer bool `json:"if_streamer"` +} + +type ChatbotRuleTriggerEvent struct { + OnFollow bool `json:"on_follow"` + OnSubscribe bool `json:"on_subscribe"` + OnRaid bool `json:"on_raid"` + OnRant int `json:"on_rant"` +} + +func (a *App) OpenFileDialog() (string, error) { + home, err := os.UserHomeDir() + if err != nil { + a.logError.Println("error getting home directory:", err) + return "", fmt.Errorf("Error opening file explorer. Try again.") + } + + filepath, err := runtime.OpenFileDialog(a.wails, runtime.OpenDialogOptions{DefaultDirectory: home}) + if err != nil { + a.logError.Println("error opening file dialog:", err) + return "", fmt.Errorf("Error opening file explorer. Try again.") + } + + return filepath, err +} diff --git a/v1/frontend/src/assets/icons/Font-Awesome/robot.png b/v1/frontend/src/assets/icons/Font-Awesome/robot.png new file mode 100644 index 0000000000000000000000000000000000000000..fb09b61214ddb6a6d73cb7144afba17a2a814841 GIT binary patch literal 3516 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 literal 0 HcmV?d00001 diff --git a/v1/frontend/src/assets/icons/twbs/chevron-left.png b/v1/frontend/src/assets/icons/twbs/chevron-left.png new file mode 100644 index 0000000000000000000000000000000000000000..51caf267fe06e97184836e897e7daa73a0a55ecb GIT binary patch literal 2220 zcma)-YdF*Y8^=Gh#n&9#&>RvYawvylk`*(@IgVCTawwYoYg01C@WrZMM2QMr4k1D# zVMwisl&FF>e&zTemT!TsdEuGjk<)yqWzt$_vrC{SD-eOGqT zUqR9eDELuhMCcs_ua`pY8{CcA^tk?@;(ZRI0dvXCOs@Ajgw=) zJ1y{ml%46ku@2YX-QfkMVU{qf-KSBHL)l{;Ev@DoIPZqClCZI+qkOx9sDi6e1xQ|G z;KrN`ppqK4UIQJA6T54T$|=TvdGv@%wwTLx)${0@KIL+)vN55*YK~u4VsY7u5I%+} z=sQueX}gJL;Ks4kM$TeGfUZ8K$9T^tQ`7xV;Eq(58!Vp9O7!ni zUp2BhgsBtsGC^3v=(%ia+U>qBd)s+RHpYItUd3&sNO>oRp%jKVz`aQ2oTLcuhNopWjZkua3`f!{~UKWW}A4a*OwwS#7^r&o)~D?FWnv&LkQyRfU`{UeKg&(5%NQtN zhy%OJv$B-1QjsEnX`OaQgIyt_z=M z5`guK`Aw^aXY}jO>Ide4NEq}^D@-{#7AS%B4s!&LjGE?6)4C`)zBAQ2L$feygyqq$ zBfS*`BTu)Df0(4+>ObT30jP{S%`XCNT_k=zj>dX zyY99D9Ko(z68kb6j=0MUv_LX5DimlSQNuq4z!C-VCz`-GLr(+2Z#n|bk7Zk;+0)HH zZP+*>RGK}~nyj1HW@7x}DIU009?e1WXLEoDlgE;lgz6v+$5>39v+u%y zHj6CO9ePg#=(Wk-YK$-q?>5Nh+f=jPkJsNPBWY1pVfzlS1deblJ8Oz2Cy_v2+2wo@ zMAVk-uBt;i+k}p$$zQgXhq0{!RV>9C1+NfjFAH z-b$G4r04fczi;rx`s(g!RrGId}7rqO$ z8n6yn)oQ>Z3yaddO=wuOQ+;#(_E=bhZTnofnw~Wp@coUtCV@2dy0x=mCLED$riy(V z?JSF7^TLl;+uFdQuFkdut`7oLxFhGCi<$yy>QmWEVQk4s4d>G}(Pqv4J*#=qE=CD5 zH1Xo`8RYygU}y|%eR%es#w;1saFOOY@6U46-Rrz&C3*9HTj|T*!rs2#si5;eXLPh% zDqeSYs5T>)kIl_D#&H)34QPpP+UpBuGsc5rBE=KQ?>n$`-b^uABAFZZOw7qHkEXN7 zH>?>u8hH%FZEQ=5HKJ+?KJ94vyrsfRT=3`Dz_YYZ6P?0~XcZar_I;KUmW1N+d3jDn z|M+87*{NWgX6S)hL7oh!;?YX5nAx5l9;X?vdEFn@QG^!p2jb2bBa&a>T(qlHS7SKw z=C#BQLr{sG8N14QXtP^OfrOdpzW7a0A_b8zpU(HtTDKCkXG)4nP!PHH?2thQ4iCjO z>e+~HB9f`0#RHTQnVU{(%g=KsQ+O78R*^7o5IxlaD`z4q2D@v}c<3nR@NTXm9wG$H z2)Rml=!e6lYJU?F#;x;Xz8`^PLbfYRG}`&KHWtRy(7s+WqWz?2;-zuU@zdbneYy>YN7>CsDBiZAqQ?}i~yBc)OGxuGe$s# z*QawNP)J{Nc~?9g#M?A3v4)>u+NRJ+U0;HNA_ZOVp*};}Pzy3OUOR8pEz_TBmI4<$rj^`IU z5RtBv&P^sdi8trQlI~~U>_r6A$`WLZFhApp)ib08@>r8A82s<|AGY5bKov!YJX$b< zmYkkce4rAqp#}LjE)b^@p(XB>bc?+)Ec5(;cCPg;pLb`}UoFIqnsYxXunc|fa2ukSWOO2{uic4 B<7NN= literal 0 HcmV?d00001 diff --git a/v1/frontend/src/assets/index.js b/v1/frontend/src/assets/index.js index cbe6f02..7faea08 100644 --- a/v1/frontend/src/assets/index.js +++ b/v1/frontend/src/assets/index.js @@ -1,4 +1,5 @@ import chess_rook from './icons/Font-Awesome/chess-rook.png'; +import chevron_left from './icons/twbs/chevron-left.png'; import chevron_right from './icons/twbs/chevron-right.png'; import circle_green_background from './icons/twbs/circle-green-background.png'; import circle_red_background from './icons/twbs/circle-red-background.png'; @@ -10,6 +11,7 @@ import heart from './icons/twbs/heart-fill.png'; import pause from './icons/twbs/pause-circle-green.png'; import play from './icons/twbs/play-circle-green.png'; import plus_circle from './icons/twbs/plus-circle-fill.png'; +import robot from './icons/Font-Awesome/robot.png'; import star from './icons/twbs/star-fill.png'; import thumbs_down from './icons/twbs/hand-thumbs-down-fill.png'; import thumbs_up from './icons/twbs/hand-thumbs-up-fill.png'; @@ -17,6 +19,7 @@ import x_lg from './icons/twbs/x-lg.png'; import logo from './logo/logo.png'; export const ChessRook = chess_rook; +export const ChevronLeft = chevron_left; export const ChevronRight = chevron_right; export const CircleGreenBackground = circle_green_background; export const CircleRedBackground = circle_red_background; @@ -29,6 +32,7 @@ export const Logo = logo; export const Pause = pause; export const Play = play; export const PlusCircle = plus_circle; +export const Robot = robot; export const Star = star; export const ThumbsDown = thumbs_down; export const ThumbsUp = thumbs_up; diff --git a/v1/frontend/src/components/ChatBot.css b/v1/frontend/src/components/ChatBot.css new file mode 100644 index 0000000..4542233 --- /dev/null +++ b/v1/frontend/src/components/ChatBot.css @@ -0,0 +1,360 @@ +.chatbot { + background-color: #344453; + display: flex; + flex-direction: column; + height: 100%; + min-width: 500px; + width: 100%; +} + +.chatbot-header { + align-items: center; + border-bottom: 1px solid #061726; + box-sizing: border-box; + display: flex; + flex-direction: row; + min-height: 55px; + justify-content: space-between; + padding: 10px 20px; + width: 100%; +} + +.chatbot-header-button { + align-items: center; + background-color: #344453; + border: none; + display: flex; + justify-content: center; + padding-left: 10px; + padding-right: 0px; +} + +.chatbot-header-button:hover { + cursor: pointer; +} + +.chatbot-header-button-icon { + height: 24px; + width: 24px; +} + +.chatbot-header-left { + align-items: center; + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.chatbot-header-icon { + height: 28px; + width: 28px; + padding-right: 10px; +} + +.chatbot-header-icon-back { + height: 28px; + width: 28px; +} + +.chatbot-header-icon-back:hover { + /* background-color: #415568; */ + cursor: pointer; +} + +.chatbot-header-right { + align-items: center; + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.chatbot-header-title { + color: #eee; + font-family: sans-serif; + font-size: 20px; + font-weight: bold; + max-width: 250px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.chatbot-list { + display: flex; + flex-direction: column; + height: 100%; + overflow-y: auto; + padding: 0px 10px; +} + +.chatbot-list-button { + align-items: center; + background-color: #344453; + border: none; + border-radius: 3px; + display: flex; + justify-content: start; + padding: 15px 10px; + width: 100%; +} + +.chatbot-list-button:hover { + background-color: #415568; + cursor: pointer; +} + +.chatbot-list-item { +} + +.chatbot-list-item-name { + color: #eee; + display: inline-block; + font-family: sans-serif; + font-size: 18px; + font-weight: bold; + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + /* width: 100%; */ +} + +.chatbot-modal-form { + align-items: center; + display: flex; + flex-direction: column; + justify-content: center; + width: 100%; +} + +.chatbot-modal-input { + background-color: #061726; + border: none; + border-radius: 5px; + box-sizing: border-box; + color: white; + font-family: monospace; + font-size: 16px; + outline: none; + padding: 10px; + resize: none; + width: 100%; +} + +.chatbot-modal-label { + color: white; + font-family: sans-serif; + font-size: 14px; + font-weight: bold; + margin-bottom: 10px; + margin-top: 10px; + width: 100%; +} + +.chatbot-modal-label-warning { + color: #f23160; + font-family: sans-serif; + font-size: 14px; + font-weight: bold; + margin-bottom: 10px; + margin-top: 10px; + width: 100%; +} + +.chatbot-modal-description { + color: #eee; + font-family: sans-serif; + font-size: 14px; + font-weight: bold; + margin-bottom: 10px; + margin-top: 10px; + text-align: center; + width: 100%; +} + +.chatbot-modal-description-warning { + color: #f23160; + font-family: sans-serif; + font-size: 14px; + font-weight: bold; + margin-bottom: 10px; + margin-top: 10px; + text-align: center; + width: 100%; +} + +.chatbot-modal-pages { + background-color: white; + /* border: 1px solid #D6E0EA; */ + border-radius: 5px; + height: 100%; + overflow: auto; + width: 80%; +} + +.chatbot-modal-page { + align-items: center; + display: flex; +} + +.chatbot-modal-page-selected { + background-color: #85c742; +} + +.chatbot-modal-page-button { + background-color: white; + border: none; + border-radius: 5px; + color: #061726; + font-family: sans-serif; + font-size: 18px; + font-weight: bold; + overflow: hidden; + padding: 10px 10px; + text-align: left; + white-space: nowrap; + width: 100%; +} + +.chatbot-modal-page-button:hover { + background-color: #85c742; + cursor: pointer; +} + +.chatbot-modal-setting { + align-items: center; + box-sizing: border-box; + display: flex; + flex-direction: row; + justify-content: space-between; + padding-top: 10px; + width: 100%; +} + +.chatbot-modal-setting-description { + color: #eee; + font-family: sans-serif; + font-size: 16px; +} + +.chatbot-modal-textarea { + border: none; + border-radius: 5px; + box-sizing: border-box; + font-family: monospace; + font-size: 16px; + outline: none; + padding: 10px; + resize: none; + width: 100%; +} + +.chatbot-modal-toggle-switch { + position: relative; + display: inline-block; + width: 50px; + height: 24px; +} + +.chatbot-modal-toggle-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.chatbot-modal-toggle-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #495a6a; + -webkit-transition: .4s; + transition: .4s; +} + +.chatbot-modal-toggle-slider:before { + position: absolute; + content: ""; + height: 16px; + width: 16px; + left: 4px; + bottom: 4px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; +} + +.choose-file { + align-items: center; + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; +} + +.choose-file-button-box { + min-width: 100px; + width: 100px; +} + +.choose-file-button { + background-color: #85c742; + border: none; + border-radius: 5px; + color: #061726; + cursor: pointer; + font-size: 16px; + text-decoration: none; + /* width: 200px; */ + width: 100%; +} + +.choose-file-path { + color: #eee; + font-family: monospace; + font-size: 16px; + overflow: scroll; + margin-left: 5px; + white-space: nowrap; +} + +input:checked + .chatbot-modal-toggle-slider { + background-color: #85c742; +} + +input:checked + .chatbot-modal-toggle-slider:before { + -webkit-transform: translateX(26px); + -ms-transform: translateX(26px); + transform: translateX(26px); +} +/* Rounded sliders */ +.chatbot-modal-toggle-slider.round { + border-radius: 34px; +} + +.chatbot-modal-toggle-slider.round:before { + border-radius: 50%; +} + +.timer-input { + border: none; + border-radius: 34px; + box-sizing: border-box; + font-family: monospace; + font-size: 24px; + outline: none; + padding: 5px 10px 5px 10px; + text-align: right; +} + +.timer-input-zero::placeholder { + text-align: center; +} + +.timer-input-value::placeholder { + color: black; + opacity: 1; + text-align: center; +} \ No newline at end of file diff --git a/v1/frontend/src/components/ChatBot.jsx b/v1/frontend/src/components/ChatBot.jsx new file mode 100644 index 0000000..b5a6fa0 --- /dev/null +++ b/v1/frontend/src/components/ChatBot.jsx @@ -0,0 +1,962 @@ +import { useEffect, useState } from 'react'; +import { Modal, SmallModal } from './Modal'; +import { + AccountList, + ChatbotList, + DeleteChatbot, + NewChatbot, + OpenFileDialog, + UpdateChatbot, +} from '../../wailsjs/go/main/App'; +import { EventsOn } from '../../wailsjs/runtime/runtime'; +import { ChevronLeft, ChevronRight, Gear, PlusCircle, Robot } from '../assets'; +import './ChatBot.css'; + +function ChatBot(props) { + const [chatbots, setChatbots] = useState([]); + const [deleteChatbot, setDeleteChatbot] = useState(false); + const [editChatbot, setEditChatbot] = useState(null); + const [error, setError] = useState(''); + const [openChatbot, setOpenChatbot] = useState(null); + const [openNewChatbot, setOpenNewChatbot] = useState(false); + const [openNewRule, setOpenNewRule] = useState(false); + const [chatbotSettings, setChatbotSettings] = useState(true); + + useEffect(() => { + EventsOn('ChatbotList', (event) => { + setChatbots(event); + if (openChatbot !== null) { + for (const chatbot of event) { + if (chatbot.id === openChatbot.id) { + setOpenChatbot(chatbot); + } + } + } + }); + }, []); + + useEffect(() => { + ChatbotList() + .then((response) => { + setChatbots(response); + }) + .catch((error) => { + setError(error); + }); + }, []); + + const open = (chatbot) => { + setOpenChatbot(chatbot); + }; + + const closeEdit = () => { + setEditChatbot(null); + }; + + const openEdit = (chatbot) => { + setEditChatbot(chatbot); + }; + + const openNew = () => { + setOpenNewChatbot(true); + }; + + const sortChatbots = () => { + let sorted = [...chatbots].sort((a, b) => + a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1 + ); + + return sorted; + }; + + const confirmDelete = () => { + DeleteChatbot(openChatbot) + .then(() => { + setDeleteChatbot(false); + setEditChatbot(null); + setOpenChatbot(null); + }) + .catch((err) => { + setDeleteChatbot(false); + setError(err); + }); + }; + + return ( + <> + {error !== '' && ( + setError('')} + show={error !== ''} + style={{ minWidth: '300px', maxWidth: '200px', maxHeight: '200px' }} + title={'Error'} + message={error} + submitButton={'OK'} + onSubmit={() => setError('')} + /> + )} + {openNewChatbot && ( + setOpenNewChatbot(false)} + show={setOpenNewChatbot} + submit={NewChatbot} + submitButton={'Create'} + submittingButton={'Creating...'} + title={'New Chatbot'} + /> + )} + {editChatbot !== null && ( + setDeleteChatbot(true)} + show={editChatbot !== null} + submit={UpdateChatbot} + submitButton={'Update'} + submittingButton={'Updating...'} + title={'Edit Chatbot'} + /> + )} + {deleteChatbot && ( + setDeleteChatbot(false)} + onClose={() => setDeleteChatbot(false)} + show={deleteChatbot} + style={{ minWidth: '300px', maxWidth: '200px', maxHeight: '200px' }} + title={'Delete Chatbot'} + message={ + 'Are you sure you want to delete the chatbot? All rules associated with this chatbot will be deleted as well.' + } + submitButton={'OK'} + onSubmit={confirmDelete} + /> + )} + {openNewRule && ( + setOpenNewRule(false)} show={openNewRule} /> + )} +
+ {openChatbot === null ? ( + <> +
+
+ + Bots +
+
+ +
+
+
+ {sortChatbots().map((chatbot, index) => ( + + ))} +
+ + ) : ( +
+
+ setOpenChatbot(null)} + src={ChevronLeft} + /> +
+ {openChatbot.name} +
+ + +
+
+ )} +
+ + ); +} + +export default ChatBot; + +function ChatbotListItem(props) { + return ( +
+ +
+ ); +} + +function ModalChatbot(props) { + const [error, setError] = useState(''); + const [id, setId] = useState(props.chatbot === undefined ? null : props.chatbot.id); + const [loading, setLoading] = useState(false); + const [name, setName] = useState(props.chatbot === undefined ? '' : props.chatbot.name); + const updateName = (event) => { + if (loading) { + return; + } + setName(event.target.value); + }; + const [nameValid, setNameValid] = useState(true); + const [url, setUrl] = useState(props.chatbot === undefined ? '' : props.chatbot.url); + const updateUrl = (event) => { + if (loading) { + return; + } + setUrl(event.target.value); + }; + + useEffect(() => { + if (loading) { + props + .submit({ id: id, name: name, url: url }) + .then(() => { + reset(); + props.onClose(); + }) + .catch((err) => { + setLoading(false); + setError(err); + }); + } + }, [loading]); + + const close = () => { + if (loading) { + return; + } + + reset(); + props.onClose(); + }; + + const reset = () => { + setLoading(false); + setName(''); + setNameValid(true); + }; + + const submit = () => { + if (name == '') { + setNameValid(false); + return; + } + + setLoading(true); + }; + + return ( + <> + {error !== '' && ( + setError('')} + show={error !== ''} + style={{ minWidth: '300px', maxWidth: '200px', maxHeight: '200px' }} + title={'Error'} + message={error} + submitButton={'OK'} + onSubmit={() => setError('')} + /> + )} + +
+ {nameValid ? ( + + ) : ( + + )} + + + +
+
+ + ); +} + +function ModalNewRule(props) { + const [back, setBack] = useState([]); + const [rule, setRule] = useState({}); + const [stage, setStage] = useState('trigger'); + const updateStage = (next, reverse) => { + setBack([...back, { stage: stage, reverse: reverse }]); + setStage(next); + }; + + const goBack = () => { + if (back.length === 0) { + return; + } + + const last = back.at(-1); + setStage(last.stage); + if (last.reverse !== undefined && last.reverse !== null) { + setRule(last.reverse(rule)); + } + setBack(back.slice(0, back.length - 1)); + }; + + const submit = () => {}; + + return ( + <> + {stage === 'trigger' && ( + + )} + {stage === 'trigger-timer' && ( + + )} + {stage === 'message' && ( + + )} + {stage === 'sender' && ( + + )} + {stage === 'review' && ( + + )} + + ); +} + +function ModalNewRuleTrigger(props) { + const next = (stage) => { + const rule = props.rule; + rule.trigger = {}; + props.setRule(rule); + props.setStage(stage, reverse); + }; + + const reverse = (rule) => { + rule.trigger = null; + return rule; + }; + + return ( + +
+ Choose Rule Trigger +
+ + + +
+
+
+
+ ); +} + +function ModalNewRuleTriggerTimer(props) { + const [validTimer, setValidTimer] = useState(true); + const [timer, setTimer] = useState( + props.rule.trigger.on_timer !== undefined && props.rule.trigger.on_timer !== null + ? props.rule.trigger.on_timer + : '' + ); + + const back = () => { + const rule = props.rule; + rule.trigger.on_timer = ''; + props.setRule(rule); + props.onBack(); + }; + + const next = () => { + if (timer === '') { + setValidTimer(false); + return; + } + + const rule = props.rule; + rule.trigger.on_timer = timer; + props.setRule(rule); + props.setStage('message', null); + }; + + return ( + +
+
+ Set Timer + + Chat rule will trigger at the set interval. + +
+
+ {validTimer ? ( + Enter timer + ) : ( + + Enter a valid timer interval. + + )} + +
+
+
+
+ ); +} + +function ModalNewRuleMessage(props) { + const [error, setError] = useState(''); + const [message, setMessage] = useState( + props.rule.message !== undefined && props.rule.message !== null ? props.rule.message : {} + ); + const [refresh, setRefresh] = useState(false); + const [validFile, setValidFile] = useState(true); + const [validText, setValidText] = useState(true); + + const back = () => { + const rule = props.rule; + rule.message = null; + props.setRule(rule); + props.onBack(); + }; + + const next = () => { + if (fromFile()) { + if (message.from_file.filepath === undefined || message.from_file.filepath === '') { + setValidFile(false); + return; + } + } else { + if (message.from_text === undefined || message.from_text === '') { + setValidText(false); + return; + } + } + + const rule = props.rule; + rule.message = message; + props.setRule(rule); + props.setStage('sender', null); + }; + + const chooseFile = () => { + OpenFileDialog() + .then((filepath) => { + if (filepath !== '') { + message.from_file.filepath = filepath; + setMessage(message); + setRefresh(!refresh); + } + }) + .catch((error) => setError(error)); + }; + + const fromFile = () => { + return message.from_file !== undefined && message.from_file !== null; + }; + + const toggleFromFile = () => { + if (fromFile()) { + message.from_file = null; + } else { + message.from_file = {}; + } + + setMessage(message); + setRefresh(!refresh); + }; + + const randomRead = () => { + if (!fromFile()) { + return false; + } + + if (message.from_file.random_read === undefined || message.from_file.random_read === null) { + return false; + } + + return message.from_file.random_read; + }; + + const toggleRandomRead = () => { + if (!fromFile()) { + return; + } + + message.from_file.random_read = !randomRead(); + setMessage(message); + setRefresh(!refresh); + }; + + const updateMessageText = (event) => { + message.from_text = event.target.value; + setMessage(message); + }; + + const updateMessageFilepath = (filepath) => { + if (!fromFile()) { + message.from_file = {}; + } + message.from_file = filepath; + setMessage(message); + }; + + return ( + <> + {error !== '' && ( + setError('')} + show={error !== ''} + style={{ minWidth: '300px', maxWidth: '200px', maxHeight: '200px' }} + title={'Error'} + message={error} + submitButton={'OK'} + onSubmit={() => setError('')} + /> + )} + +
+ Add Message +
+ {fromFile() ? ( + validFile ? ( + + ) : ( + + ) + ) : validText ? ( + + ) : ( + + )} + {fromFile() ? ( +
+
+ +
+ + {message.from_file.filepath} + +
+ ) : ( +