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 0000000..fb09b61 Binary files /dev/null and b/v1/frontend/src/assets/icons/Font-Awesome/robot.png differ 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 0000000..51caf26 Binary files /dev/null and b/v1/frontend/src/assets/icons/twbs/chevron-left.png differ 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} + +
+ ) : ( +