diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6769724 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.vscode/* +game/flask_session/* +Chatbox/flask_session/* \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..99eac10 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.pythonPath": "C:\\Users\\tranq\\AppData\\Local\\Programs\\Python\\Python39\\python.exe" +} \ No newline at end of file diff --git a/Chatbox/__pycache__/bot.cpython-39.pyc b/Chatbox/__pycache__/bot.cpython-39.pyc new file mode 100644 index 0000000..fb70ffb Binary files /dev/null and b/Chatbox/__pycache__/bot.cpython-39.pyc differ diff --git a/Chatbox/__pycache__/sessionManager.cpython-39.pyc b/Chatbox/__pycache__/sessionManager.cpython-39.pyc new file mode 100644 index 0000000..937e005 Binary files /dev/null and b/Chatbox/__pycache__/sessionManager.cpython-39.pyc differ diff --git a/Chatbox/bot.py b/Chatbox/bot.py new file mode 100644 index 0000000..12ba9b1 --- /dev/null +++ b/Chatbox/bot.py @@ -0,0 +1,14 @@ +class botmessage(object): + def __init__(self, sessId): + self.sessID = sessId + self.botOutput = sessId + + def botmes(self, userInput): + if userInput == "Hello": + #mess = botmessage(sessionID) + botOutput = "Hi " + self.sessID + return botOutput + else: + #messtwo = botmessage(sessionID) + botOutput = "Bye " + self.sessID + return botOutput \ No newline at end of file diff --git a/Chatbox/chatbox_flask.py b/Chatbox/chatbox_flask.py new file mode 100644 index 0000000..b9a955b --- /dev/null +++ b/Chatbox/chatbox_flask.py @@ -0,0 +1,183 @@ +import uuid +from flask import Flask, render_template, jsonify, request, session, redirect, url_for +from flask_session import Session + +import os + +app = Flask(__name__) +app.secret_key = os.urandom(24) +app.config["SESSION_TYPE"] = "filesystem" +Session(app) + +# separate route to deal with tutorial +@app.route("/tutorial", methods=["GET", "POST"]) +def tutorial(): + # tutorial action followed by a hint message + actionList = [{"name":"pointAt", "args":"aspirin"}, {"name":"pointAt", "args":"monday noon"}] + hintList = ["first click on an aspirin pill to grab", "now click on the highlighted position on a calendar to drop the pill"] + + if request.method == "POST": + pos = int(request.form["position"]) # get current step in list of tutorial actions + response = request.form["response"] + proceed = False + + # only proceeds if user remove from container or add to grid + # TODO: make it more dynamic + if "remove_from_container aspirin" in response or "add_to_grid" in response: + proceed = True + + # check if the current action is the last or not + if pos < len(actionList): + output = {"action": actionList[pos], "hint": hintList[pos], "proceed": proceed} + else: + output = None + return jsonify(output) + +# route for communicating back and forth between UI and server +@app.route("/message", methods=["GET", "POST"]) +def message(): + if request.method == "POST": + print(request.form["botm"]) + message = request.form["botm"] + + # data to send to UI (may be combined into 1 variable) + action = {} + hint = "" + start = "" + state = "speaking" + time_mapping = ["none", "morning", "noon", "afternoon", "evening"] + day_mapping = {"mon":"Monday", "tue":"Tuesday", "wed":"Wednesday", "thu":"Thursday", "fri":"Friday", +"sat":"Saturday", "sun":"Sunday"} + + # functions to check for invalid user action (may be moved to a separate route) + # check if number of 1 type medication is too large for a time in a day + def numCheck(): + print(session.get("calendar_count")) + for position, medCount in session.get("calendar_count").items(): + for med, count in medCount.items(): + if count > 2: + return (med, position, count) + return None + + # check if ibuprofen and aspirin are in the same position (used together) + def interactionCheck(): + for position, medCount in session.get("calendar_count").items(): + if "ibuprofen" in medCount and "aspirin" in medCount and medCount["ibuprofen"] > 0 and medCount["aspirin"] > 0: + return position + return None + + # manually respond to user action/message + if "hello" in message: + start = "hello" + elif "bye" in message or "goodbye" in message: + state = "sleeping" + start = "see ya later!" + elif "thank you" in message: + start = "you are welcome!" + elif "no" in message and session.get("toUser")[-1]["state"] == "questioning": # may include problem/question type? + state = "speaking" + hint = "here is an image to help" + action = {"name":"showImage", "args":"static/pill_interaction.png"} + elif "what" in message and session.get("toUser")[-1]["state"] == "questioning": + state = "questioning" + hint = "Do you remember the interaction for the blue pill?" + elif "add_to_grid" in message: + messageList = message[1:-1].split(" ") + med = messageList[1] + position = messageList[2][:3].lower() + str(time_mapping.index(messageList[-1])) + + # increase number of pill in calendar position + if position not in session.get("calendar_count"): + session.get("calendar_count")[position] = {med: 1} + else: + if med in session.get("calendar_count")[position]: + session.get("calendar_count")[position][med] += 1 + else: + session.get("calendar_count")[position][med] = 1 + + # check if ibuprofen is used in the morning + if med == "ibuprofen" and messageList[-1] == "morning": + hint = "ibuprofen should not be used in the morning" + action = {"name":"pointAt", "args": messageList[2] + " " + messageList[-1]} + else: + # check if there are no position with more than 2 pills of any type + overdose = numCheck() + if overdose != None: + day = day_mapping[overdose[1][:3]] + time = time_mapping[int(overdose[1][-1])] + hint = "too many " + overdose[0] + " on " + day + " " + time + action = {"name":"pointAt", "args":day + " " + time} + # check for interaction conflict is there is any + else: + incompatible = interactionCheck() + print(incompatible) + if incompatible != None: + day = day_mapping[incompatible[:3]] + time = time_mapping[int(incompatible[-1])] + state = "questioning" + hint = "huhhhhhh????" + + elif "remove_from_grid" in message: + messageList = message[1:-1].split(" ") + med = messageList[1] + position = messageList[2][:3].lower() + str(time_mapping.index(messageList[-1])) + # decrease number of pills of a type in a calendar positon + session.get("calendar_count")[position][med] -= 1 + if (session.get("calendar_count")[position][med] < 0): + session.get("calendar_count")[position][med] = 0 + + output = {"start": start, "action": action, "hint": hint, "state":state, "userInput":message} + # keep track of past conversation between user and agent + session.get("fromUser").append(message) + session.get("toUser").append(output) + return jsonify(output) + +#################### PREVIOUS PROJECT CODE ####################### +#Session Manager +# @app.route('/processOne', methods=['GET', 'POST']) +# def processOne(): +# # Session Manager +# # @app.route('/process', methods=['GET', 'POST']) +# # def process(): +# userm = request.form['botm'] +# botm = sessionManage(userm, session['user']) +# state = botm['state'] +# hint = botm['hint'] + +# return jsonify(botm) + +# @app.route('/processTwo', methods=['GET', 'POST']) +# def processTwo(): +# print ("process two being called") +# message = newSessionTwo(session['user']) +# output = {'start': message} +# print (message) +# return jsonify(output) +############################################################# + +# initializes the game at the beginning +@app.route('/startgame', methods=['GET', 'POST']) +def startgame(): + # message = newSession(session['user']) + session["calendar_count"] = {} + session["fromUser"] = [] + session["toUser"] = [] + + # configurations for med sorting game + if "events" not in session: + session["events"] = [{"name": "exercise", "day": "mon", "time":"1"}, {"name":"appointment", "day":"thu", "time":"2"}, + {"name":"work", "day":"sat","time":"3"}] + + if "medications" not in session: + session["medications"] = [{"name":"ibuprofen", "color":"red", "number":"11"}, {"name":"aspirin", "color":"blue", "number":"15"}, + {"name":"albuterol", "color":"green", "number":"7"}] + + output = {"events": session.get("events"), "medications": session.get("medications"), "hint":"Game started! Do you want to see the tutorial?"} + return jsonify(output) + +@app.route('/', methods=['GET']) +def chatbox(): + return render_template("index.html") + +if __name__ == "__main__": + app.run(debug=True) diff --git a/Chatbox/sessionManager.py b/Chatbox/sessionManager.py new file mode 100644 index 0000000..0b1c6f2 --- /dev/null +++ b/Chatbox/sessionManager.py @@ -0,0 +1,38 @@ +import sys, os +cur = os.path.dirname(os.path.realpath(__file__)) + "/" +sys.path.insert(0, cur + "../") + +# from Chatbox.bot import botmessage +# import planners.pyhop.pyhop +# import game.game +# from model.maze import operators +# from model.maze import methods +# import csv + +# sessionDictionary = {} + +# def newSession(sessionID): +# ph = planners.pyhop.pyhop.Pyhop('hoppity') +# #if (trial == 0): +# session = game.game.GenericPyhopGame('model/lightbulb/game.config', ph) +# sessionDictionary[sessionID] = session +# output = session.start_game(sessionID) + + +# return output + +# def newSessionTwo(sessionID): +# ph = planners.pyhop.pyhop.Pyhop('hoppity') +# session = game.game.GenericPyhopGame('model/maze/game.config', ph) +# sessionDictionary[sessionID] = session +# output = session.start_game(sessionID) + +# return output + +# def sessionManage(userInput, sessionID): +# if sessionID in sessionDictionary: +# session = sessionDictionary[sessionID] +# output = session.handle_user_input(userInput) +# else: +# output = "No game initialized" +# return output diff --git a/Chatbox/static/avatar.js b/Chatbox/static/avatar.js new file mode 100644 index 0000000..ad8ae2b --- /dev/null +++ b/Chatbox/static/avatar.js @@ -0,0 +1,38 @@ +function animateAvatar(state) { + switch(state) { + // add new questioning state in which some question marks appear over avatar's head + case "questioning": + $("#eye1, #eye2").removeClass('eye-down eye-sleep').addClass('eye'); + $(".ball").css("animation", "bounce 2s ease-out 3"); + $(".shadow").css("animation", "shadow-bounce 2s ease-out 3"); + $("#questionMark").css("visibility", "visible").css("animation", "wobble 1s 3 both"); + break; + case "speaking": + $("#eye1, #eye2").removeClass('eye-down eye-sleep').addClass('eye'); + $(".ball").css("animation", "bounce 2s ease-out 2"); + $(".shadow").css("animation", "shadow-bounce 2s ease-out 2"); + $("#questionMark").css("visibility", "hidden"); + break; + case "thinking": + $("#eye1, #eye2").removeClass('eye eye-sleep').addClass('eye-down'); + $(".ball").css("animation", "lil-bounce 2s linear infinite"); + $(".shadow").css("animation", "shadow-lil-bounce 2s linear infinite"); + $("#questionMark").css("visibility", "hidden"); + break; + case "listening": + $("#eye1, #eye2").removeClass('eye-down eye-sleep').addClass('eye'); + $(".ball").css("animation", "breathe 3s linear infinite"); + $(".shadow").css("animation", "none"); + $("#questionMark").css("visibility", "hidden"); + break; + case "sleeping": + $("#eye1, #eye2").removeClass('eye eye-down').addClass('eye-sleep'); + $(".ball").css("animation", "to-roll 1s, roll 5s linear infinite"); + $(".shadow").css("animation", "shadow-roll 5s linear infinite"); + $("#questionMark").css("visibility", "hidden"); + break; + default: + // TODO output to debug log + console.log("unknown state: " + state); + } + } \ No newline at end of file diff --git a/Chatbox/static/avatarSpace.css b/Chatbox/static/avatarSpace.css new file mode 100644 index 0000000..066208e --- /dev/null +++ b/Chatbox/static/avatarSpace.css @@ -0,0 +1,250 @@ +.ball { + display: block; + background: radial-gradient(circle at 195px 105px, #330099, #000); + border-radius: 50%; + height: 300px; + width: 300px; + margin: 0 auto; + position: relative; + top: 175px; + transition: transform 1s; + } + .ball:before { + content: ""; + position: absolute; + background: radial-gradient(circle at 50% 120%, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.4) 70%); + border-radius: 50%; + bottom: 3.5%; + left: 4.5%; + opacity: 0.4; + height: 100%; + width: 95%; + filter: blur(5px); + z-index: 2; + } + .ball:after { + content: ""; + width: 100%; + height: 100%; + position: absolute; + top: -1%; + left: 15%; + border-radius: 50%; + background: radial-gradient(circle at 20% 20%, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.8) 14%, rgba(255, 255, 255, 0) 24%); + transform: translateX(80px) translateY(-90px) skewX(-20deg); + filter: blur(10px); + } + + .shadow { + position: relative; + width: 260px; + height: 16px; + background: #999; + opacity: .5; + border-radius: 100%; + margin: 0 auto; + top: 170px; + z-index: -1; + } + + #eye1 { + left: -45px; + top: -20px; + transform: skewX(-5deg) skewY(-2deg); + } + + #eye2 { + left: 75px; + top: -20px; + transform: skewX(5deg) skewY(2deg); + } + + #questionMark { + position: relative; + color: red; + font-size: 70px; + bottom: 80px; + left: 275px; + visibility: hidden; + z-index: 1; + } + + .wobble { + visibility: visible; + animation: wobble 1s 1s 3; + } + + .eye { + width: 30%; + height: 30%; + margin: 30%; + border-radius: 50%; + background: radial-gradient(circle at 50% 50%, #208ab4 0%, #6fbfff 30%, #4381b2 100%); + position: absolute; + } + .eye:before { + content: ""; + display: block; + position: absolute; + width: 37.5%; + height: 37.5%; + border-radius: 50%; + top: 31.25%; + left: 31.25%; + background: black; + } + .eye:after { + content: ""; + display: block; + position: absolute; + width: 31.25%; + height: 31.25%; + border-radius: 50%; + top: 18.75%; + left: 48.75%; + background: rgba(255, 255, 255, 0.2); + } + + .eye-down { + width: 30%; + height: 30%; + margin: 30%; + border-radius: 50%; + background: radial-gradient(circle at 50% 70%, #208ab4 0%, #6fbfff 30%, #330099 100%); + position: absolute; + } + .eye-down:before { + content: ""; + display: block; + position: absolute; + width: 37.5%; + height: 37.5%; + border-radius: 50%; + top: 41.25%; + left: 31.25%; + background: black; + animation: look-down 100s ease-out infinite; + } + .eye-down:after { + content: ""; + display: block; + position: absolute; + width: 31.25%; + height: 31.25%; + border-radius: 50%; + top: 33.75%; + left: 48.75%; + background: rgba(255, 255, 255, 0.2); + } + + .eye-sleep { + width: 30%; + height: 30%; + margin: 30%; + border-radius: 50%; + background: radial-gradient(circle at 50% 70%, #4400aa 0%, #3700aa 30%, #330099 100%); + position: absolute; + transition: background 2s; + } + .eye-sleep:before { + content: ""; + display: block; + position: absolute; + width: 87.5%; + height: 37.5%; + border: solid 3px; + border-color: transparent transparent black transparent; + border-radius: 0 0 50% 50%; + top: 11.25%; + left: 0; + } + + /***** ANIMATIONS *****/ + /*** Speaking ***/ + @keyframes bounce { + 0% { transform: scale(1); } + 40% { transform: translateY(-20px) scaleY(1.05) scaleX(0.95); } + 80% { transform: scaleY(0.95) scaleX(1.05); } + 100% { transform: scale(1); } + } + @keyframes shadow-bounce { + 0% { transform: scale(1); } + 40% { transform: margin: 0% 0% 0% 0%; opacity: 0.1; transform: scaleX(0.95) translateX(-5px);} + 80% { transform: margin: 0% 20% 0% 20%; opacity: 0.5; transform: scaleX(1.05);} + 100% { transform: scale(1); } + } + + /*** Thinking ***/ + @keyframes lil-bounce { + 0% { transform: scale(1); } + 40% { transform: translateY(-5px) scaleY(1.05) scaleX(0.95); } + 80% { transform: scaleY(0.95) scaleX(1.05); } + 100% { transform: scale(1); } + } + @keyframes shadow-lil-bounce { + 0% { transform: scale(1); } + 40% { transform: margin: 0% 10% 0% 10%; opacity: 0.4; transform: scaleX(0.98) translateX(-2px);} + 80% { transform: margin: 0% 20% 0% 20%; opacity: 0.5; transform: scaleX(1.05);} + 100% { transform: scale(1); } + } + /* eyes */ + @keyframes look-down { + 5% { transform: translateY(35%) translateX(-5%); } + 10% { transform: translateY(35%) translateX(-10%); } + 15% { transform: translateY(35%) translateX(-5%); } + 25% { transform: translateY(35%) translateX(-10%) ; } + 50% { transform: translateY(35%) translateX(0%); } + 65% { transform: translateY(35%) translateX(-5%); } + 75% { transform: translateY(35%) translateX(0%) ; } + 100% { transform: translateY(35%) translateX(-5%); } + } + + /*** Sleeping ***/ + @keyframes roll { + 0% { transform: rotate(-5deg) scale(1); background: radial-gradient(circle at 130px 70px, #320088, #000); } + 50% { transform: rotate(-15deg) scale(0.9) translateY(12px); background: radial-gradient(circle at 130px 70px, #320084, #000); } + 100% { transform: rotate(-5deg) scale(1); background: radial-gradient(circle at 130px 70px, #320088, #000); } + } + @keyframes shadow-roll { + 0% { transform: scale(0.95); } + 50% { transform: translateX(-5px) scale(0.9);} + 100% { transform: scale(0.95); } + } + + /*** Sleeping ***/ + @keyframes to-roll { + 0% { transform: scale(1); } + 100% { transform: rotate(-5deg) scale(1); + background: radial-gradient(circle at 130px 70px, #330088, #000); } + } + + /*** Listening ***/ + @keyframes breathe { + 0% { transform: scale(1); } + 75% { transform: scale(1.03) translateY(-2px) ; } + 100% { transform: scale(1); } + } + + /*** Questioning ***/ + @keyframes wobble { + 0%, + 100% { + transform: translateX(0%); + /* transform-origin: 50% 50%; */ + } + 15% { + transform: translateX(-30px) rotate(-6deg); + } + 30% { + transform: translateX(15px) rotate(6deg); + } + 45% { + transform: translateX(-15px) rotate(-3.6deg); + } + 60% { + transform: translateX(9px) rotate(2.4deg); + } + 75% { + transform: translateX(-6px) rotate(-1.2deg); + } + } \ No newline at end of file diff --git a/Chatbox/static/chat.css b/Chatbox/static/chat.css new file mode 100644 index 0000000..8cb59b7 --- /dev/null +++ b/Chatbox/static/chat.css @@ -0,0 +1,83 @@ +#title { + font-size: 25px; + padding: 10px; + background-color: #4E2A84; + color: white; + border-radius: 20px 20px 0 0; + } + + #topSpace { + border: 2px solid gray; + border-width: 0 2px; + height: 650px; + } + + #userContainer { + min-height: 60px; + border: 1px solid gray; + border-width: 1px 2px; + padding: 1% 5%; + } + + .newuserDiv { + border-radius: 15px 15px 2px 15px; + border: 2px solid skyblue; + padding: 5px 10px; + float: right; + max-width: 70%; + margin: 5px 15px 5px auto; + background-color: white; + } + + .newbotDiv { + border-radius: 15px 15px 15px 2px; + border: 2px solid lightgrey; + padding: 5px 10px; + float: left; + max-width: 70%; + margin: 5px auto 5px 15px; + white-space: pre-line; + background-color: white; + } + + #chatSpace { + overflow-y: scroll; + padding: 15px; + background-color: #ece9f3; + max-height: 100%; + font-size: x-large; + } + + #user, #user::-webkit-input-placeholder { + font-size: x-large; + } + + #submitBtn { + font-size: x-large; + } + + #debug { + padding: 15px; + } + + /* TODO put this in an init.css and use bootstrap */ + .toggleDebug { + width: 0; + height: 0; + border-left: 10px solid transparent; + border-right: 10px solid transparent; + border-top: 10px solid #999; + border-bottom: none; + padding: 0px; + margin: 5px; + } + + /* TODO put this in an init.css and use bootstrap */ + .debugOn { + width: 0; + height: 0; + border-left: 10px solid transparent; + border-right: 10px solid transparent; + border-bottom: 10px solid #999; + border-top: none; + } \ No newline at end of file diff --git a/Chatbox/static/chatbox.js b/Chatbox/static/chatbox.js new file mode 100644 index 0000000..6688efa --- /dev/null +++ b/Chatbox/static/chatbox.js @@ -0,0 +1,116 @@ +function message(botm) { + // if there's user input, add to chatbox + var userInput = $('#user').val(); + if (userInput) { + addUserInput(userInput); + + // Run tutorial if user ask for tutorial + // TODO: move tutorial check to backend + if (userInput.includes("how to play")) { + tutorial(); + } + + // reset message box + $("#user").parent().parent()[0].reset(); + } + + // Output user action to chatbox + // else if (botm["userInput"]) { + // addUserInput(botm["userInput"]) + // } + + // create a row div + var botRow = document.createElement("div"); + $(botRow).addClass("row"); + + // create a newbotDiv div + var newBotDiv = document.createElement("div"); + $(newBotDiv).addClass("newbotDiv"); + + // set text to bot output + $(newBotDiv).append(botm["start"]); + + if (botm["action"]) { + doAction(botm["action"]); + } + + if (botm['state']){ + // $(newBotDiv).append(botm["state"] + "\n"); + animateAvatar(botm["state"]); + } + // if the agent has messages to the user, add to the chatSpace + // represent sayText action + $(newBotDiv).append(botm["hint"]); + $(newBotDiv).append(botm["end"]); + + // add newbotDiv as child of row and add row to child of chatSpace (only if there is text inside) + if ($(newBotDiv).text() != "") { + $(botRow).append($(newBotDiv)); + $("#chatSpace").append($(botRow)); + $("#chatSpace").get(0).scrollIntoView({ behavior: 'smooth' }); + } + + // TODO add debugging log + // TODO add avatar status +} + +function addUserInput(userInput) { + // create a row div + var userRow = document.createElement("div"); + $(userRow).addClass("row"); + + // create a newuserDiv div and set text to user input + var newUserDiv = document.createElement("div"); + $(newUserDiv).addClass("newuserDiv") + .html(userInput) + // add newuserDiv as child of row, and add row as child of chatSpace + $(userRow).append($(newUserDiv)); + $("#chatSpace").append($(userRow)); +} + +function updateScroll() { + $("#chatSpace").scrollTop($("#chatSpace")[0].scrollHeight); +} + +$(document).ready(function () { + + // first thing to do when page fully loads: process configurations received from backend + $.post($SCRIPT_ROOT + "/startgame", { }, + function (data) { + console.log("start game") + medInit(data); + message(data); + updateScroll(); + }); + + // TODO move this to an init file + animateAvatar("sleeping"); + + // TODO also put this in an init file + $("#btnSleeping").click(function() {animateAvatar('sleeping')}); + $("#btnThinking").click(function() {animateAvatar('thinking')}); + $("#btnListening").click(function() {animateAvatar('listening')}); + $("#btnSpeaking").click(function() {animateAvatar('speaking')}); + $("#btnQuestioning").click(function() {animateAvatar('questioning')}); + + // TODO also put this in an init file + $(".toggleDebug").click(function() { + $("#debug").toggle("slow", function(){}); + $(".toggleDebug").toggleClass("debugOn"); + }); + + // this used to be a