diff --git a/.env.sample b/.env.sample index 08773b3..96a61f5 100644 --- a/.env.sample +++ b/.env.sample @@ -1,23 +1,31 @@ -AZURE_SEARCH_SERVICE= -AZURE_SEARCH_INDEX= -AZURE_SEARCH_KEY= -AZURE_SEARCH_USE_SEMANTIC_SEARCH=False -AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG=default -AZURE_SEARCH_INDEX_IS_PRECHUNKED=False -AZURE_SEARCH_TOP_K=5 -AZURE_SEARCH_ENABLE_IN_DOMAIN=False -AZURE_SEARCH_CONTENT_COLUMNS= -AZURE_SEARCH_FILENAME_COLUMN= -AZURE_SEARCH_TITLE_COLUMN= -AZURE_SEARCH_URL_COLUMN= -AZURE_OPENAI_RESOURCE= -AZURE_OPENAI_MODEL= -AZURE_OPENAI_KEY= -AZURE_OPENAI_MODEL_NAME=gpt-35-turbo -AZURE_OPENAI_TEMPERATURE=0 -AZURE_OPENAI_TOP_P=1.0 +AZURE_ENV_NAME="nyc-cogsearch-useast" +AZURE_FORMRECOGNIZER_RESOURCE_GROUP="rg-nyc-cogsearch-useast" +AZURE_FORMRECOGNIZER_SERVICE="cog-fr-p5nvosgo6yaw4" +AZURE_FORMRECOGNIZER_SKU_NAME="S0" +AZURE_LOCATION="eastus" +AZURE_OPENAI_KEY="========================== GET FROM OUR DEPLOYMENT" AZURE_OPENAI_MAX_TOKENS=1000 -AZURE_OPENAI_STOP_SEQUENCE= -AZURE_OPENAI_SYSTEM_MESSAGE=You are an AI assistant that helps people find information. -AZURE_OPENAI_PREVIEW_API_VERSION=2023-06-01-preview -AZURE_OPENAI_STREAM=True +AZURE_OPENAI_MODEL="turbo" +AZURE_OPENAI_MODEL_NAME="gpt-35-turbo" +AZURE_OPENAI_PREVIEW_API_VERSION="2023-06-01-preview" +AZURE_OPENAI_RESOURCE="cog-p5nvosgo6yaw4" +AZURE_OPENAI_RESOURCE_GROUP="rg-nyc-cogsearch-useast" +AZURE_OPENAI_SKU_NAME="S0" +AZURE_OPENAI_STOP_SEQUENCE="" +AZURE_OPENAI_STREAM="true" +AZURE_OPENAI_SYSTEM_MESSAGE="You are an AI assistant that helps people find information." +AZURE_OPENAI_TEMPERATURE=0 +AZURE_RESOURCE_GROUP="rg-nyc-cogsearch-useast" +AZURE_SEARCH_CONTENT_COLUMNS="content" +AZURE_SEARCH_ENABLE_IN_DOMAIN="true" +AZURE_SEARCH_FILENAME_COLUMN="filepath" +AZURE_SEARCH_INDEX="gptkbindex" +AZURE_SEARCH_KEY="========================== GET FROM OUR DEPLOYMENT" +AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG="default" +AZURE_SEARCH_SERVICE="gptkb-p5nvosgo6yaw4" +AZURE_SEARCH_SERVICE_RESOURCE_GROUP="rg-nyc-cogsearch-useast" +AZURE_SEARCH_SKU_NAME="standard" +AZURE_SEARCH_TITLE_COLUMN="title" +AZURE_SEARCH_TOP_K=5 +AZURE_SEARCH_URL_COLUMN="url" +AZURE_SEARCH_USE_SEMANTIC_SEARCH="false" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 16048f9..56aec81 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,10 @@ .venv +.idea frontend/node_modules .env +.env-* static .azure/ __pycache__/ node_modules +*.iml diff --git a/app.py b/app.py index b906b3d..d9c8627 100644 --- a/app.py +++ b/app.py @@ -10,15 +10,18 @@ app = Flask(__name__, static_folder="static") + # Static Files @app.route("/") def index(): return app.send_static_file("index.html") + @app.route("/favicon.ico") def favicon(): return app.send_static_file('favicon.ico') + @app.route("/assets/") def assets(path): return send_from_directory("static/assets", path) @@ -45,23 +48,29 @@ def assets(path): AZURE_OPENAI_TOP_P = os.environ.get("AZURE_OPENAI_TOP_P", 1.0) AZURE_OPENAI_MAX_TOKENS = os.environ.get("AZURE_OPENAI_MAX_TOKENS", 1000) AZURE_OPENAI_STOP_SEQUENCE = os.environ.get("AZURE_OPENAI_STOP_SEQUENCE") -AZURE_OPENAI_SYSTEM_MESSAGE = os.environ.get("AZURE_OPENAI_SYSTEM_MESSAGE", "You are an AI assistant that helps people find information.") +AZURE_OPENAI_SYSTEM_MESSAGE = os.environ.get("AZURE_OPENAI_SYSTEM_MESSAGE", + "You are an AI assistant that helps people find information.") AZURE_OPENAI_PREVIEW_API_VERSION = os.environ.get("AZURE_OPENAI_PREVIEW_API_VERSION", "2023-06-01-preview") AZURE_OPENAI_STREAM = os.environ.get("AZURE_OPENAI_STREAM", "true") -AZURE_OPENAI_MODEL_NAME = os.environ.get("AZURE_OPENAI_MODEL_NAME", "gpt-35-turbo") # Name of the model, e.g. 'gpt-35-turbo' or 'gpt-4' +AZURE_OPENAI_MODEL_NAME = os.environ.get("AZURE_OPENAI_MODEL_NAME", + "gpt-35-turbo") # Name of the model, e.g. 'gpt-35-turbo' or 'gpt-4' SHOULD_STREAM = True if AZURE_OPENAI_STREAM.lower() == "true" else False + def is_chat_model(): - if 'gpt-4' in AZURE_OPENAI_MODEL_NAME.lower() or AZURE_OPENAI_MODEL_NAME.lower() in ['gpt-35-turbo-4k', 'gpt-35-turbo-16k']: + if 'gpt-4' in AZURE_OPENAI_MODEL_NAME.lower() or AZURE_OPENAI_MODEL_NAME.lower() in ['gpt-35-turbo-4k', + 'gpt-35-turbo-16k']: return True return False + def should_use_data(): if AZURE_SEARCH_SERVICE and AZURE_SEARCH_INDEX and AZURE_SEARCH_KEY: return True return False + def prepare_body_headers_with_data(request): request_messages = request.json["messages"] @@ -138,7 +147,7 @@ def stream_with_data(body, headers, endpoint): role = lineJson["choices"][0]["messages"][0]["delta"].get("role") if role == "tool": response["choices"][0]["messages"].append(lineJson["choices"][0]["messages"][0]["delta"]) - elif role == "assistant": + elif role == "assistant": response["choices"][0]["messages"].append({ "role": "assistant", "content": "" @@ -156,7 +165,7 @@ def stream_with_data(body, headers, endpoint): def conversation_with_data(request): body, headers = prepare_body_headers_with_data(request) endpoint = f"https://{AZURE_OPENAI_RESOURCE}.openai.azure.com/openai/deployments/{AZURE_OPENAI_MODEL}/extensions/chat/completions?api-version={AZURE_OPENAI_PREVIEW_API_VERSION}" - + if not SHOULD_STREAM: r = requests.post(endpoint, headers=headers, json=body) status_code = r.status_code @@ -169,6 +178,7 @@ def conversation_with_data(request): else: return Response(None, mimetype='text/event-stream') + def stream_without_data(response): responseText = "" for line in response: @@ -207,13 +217,13 @@ def conversation_without_data(request): for message in request_messages: messages.append({ - "role": message["role"] , + "role": message["role"], "content": message["content"] }) response = openai.ChatCompletion.create( engine=AZURE_OPENAI_MODEL, - messages = messages, + messages=messages, temperature=float(AZURE_OPENAI_TEMPERATURE), max_tokens=int(AZURE_OPENAI_MAX_TOKENS), top_p=float(AZURE_OPENAI_TOP_P), @@ -242,6 +252,7 @@ def conversation_without_data(request): else: return Response(None, mimetype='text/event-stream') + @app.route("/conversation", methods=["GET", "POST"]) def conversation(): try: @@ -254,5 +265,6 @@ def conversation(): logging.exception("Exception in /conversation") return jsonify({"error": str(e)}), 500 + if __name__ == "__main__": app.run() diff --git a/frontend/index.html b/frontend/index.html index c323c3e..b9fd921 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - Azure AI + NYC CHAT BOT
diff --git a/frontend/src/assets/MyCityTitle.svg b/frontend/src/assets/MyCityTitle.svg new file mode 100644 index 0000000..fa49525 --- /dev/null +++ b/frontend/src/assets/MyCityTitle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/assets/UserAvatar.svg b/frontend/src/assets/UserAvatar.svg new file mode 100644 index 0000000..50756a2 --- /dev/null +++ b/frontend/src/assets/UserAvatar.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/components/Answer/Answer.module.css b/frontend/src/components/Answer/Answer.module.css index d3e9a7f..6eb3145 100644 --- a/frontend/src/components/Answer/Answer.module.css +++ b/frontend/src/components/Answer/Answer.module.css @@ -1,171 +1,174 @@ .answerContainer { - display: flex; - flex-direction: column; - align-items: flex-start; - padding: 8.1285px; - gap: 5.42px; - background: #FFFFFF; - box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12); - border-radius: 5.419px; + display: flex; + width: 940px; + padding: 22px 22px 73px 22px; + justify-content: center; + align-items: center; + + border-radius: 14px; + background: #fff; + box-shadow: 6px 17px 16px 4px rgba(183, 183, 183, 0.19); } .answerText { - font-style: normal; - font-weight: 400; - font-size: 14px; - line-height: 20px; - color: #323130; - flex: none; - order: 1; - align-self: stretch; - flex-grow: 0; - margin: 11px; - white-space: normal; - word-wrap: break-word; - max-width: 800px; - overflow-x: auto; + color: #0d0c0c; + font-family: Lora; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 24px; /* 150% */ + + flex: none; + order: 1; + align-self: stretch; + flex-grow: 0; + margin: 11px; + white-space: normal; + word-wrap: break-word; + max-width: 800px; + overflow-x: auto; } .answerFooter { - display: flex; - flex-flow: row nowrap; - width: 100%; - height: auto; - box-sizing: border-box; - justify-content: space-between; + display: flex; + flex-flow: row nowrap; + width: 100%; + height: auto; + box-sizing: border-box; + justify-content: space-between; } .answerDisclaimerContainer { - justify-content: center; - display: flex; + justify-content: center; + display: flex; } .answerDisclaimer { - font-style: normal; - font-weight: 400; - font-size: 12px; - line-height: 16px; - - display: flex; - align-items: center; - text-align: center; - color: #707070; - flex: none; - order: 1; - flex-grow: 0; + font-style: normal; + font-weight: 400; + font-size: 12px; + line-height: 16px; + + display: flex; + align-items: center; + text-align: center; + color: #707070; + flex: none; + order: 1; + flex-grow: 0; } .citationContainer { - margin-left: 10px; - font-style: normal; - font-weight: 600; - font-size: 12px; - line-height: 16px; - - color: #115EA3; - display: flex; - flex-direction: row; - align-items: center; - padding: 4px 6px; - gap: 4px; - border: 1px solid #D1D1D1; - border-radius: 4px; + margin-left: 10px; + font-style: normal; + font-weight: 600; + font-size: 12px; + line-height: 16px; + + color: #115ea3; + display: flex; + flex-direction: row; + align-items: center; + padding: 4px 6px; + gap: 4px; + border: 1px solid #d1d1d1; + border-radius: 4px; } .citationContainer:hover { - text-decoration: underline; - cursor: pointer; + text-decoration: underline; + cursor: pointer; } .citation { - box-sizing: border-box; - display: inline-flex; - flex-direction: column; - justify-content: center; - align-items: center; - padding: 0px; - width: 14px; - height: 14px; - border: 1px solid #E0E0E0; - border-radius: 4px; - flex: none; - flex-grow: 0; - z-index: 2; - font-style: normal; - font-weight: 600; - font-size: 10px; - line-height: 14px; - text-align: center; - color: #424242; - cursor: pointer; + box-sizing: border-box; + display: inline-flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 0px; + width: 14px; + height: 14px; + border: 1px solid #e0e0e0; + border-radius: 4px; + flex: none; + flex-grow: 0; + z-index: 2; + font-style: normal; + font-weight: 600; + font-size: 10px; + line-height: 14px; + text-align: center; + color: #424242; + cursor: pointer; } .citation:hover { - text-decoration: underline; - cursor: pointer; + text-decoration: underline; + cursor: pointer; } .accordionIcon { - display: inline-flex; - flex-direction: row; - justify-content: center; - align-items: center; - padding: 0px; - margin-top: 4px; - color: #616161; - font-size: 10px; + display: inline-flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 0px; + margin-top: 4px; + color: #616161; + font-size: 10px; } .accordionIcon:hover { - cursor:pointer; + cursor: pointer; } .accordionTitle { - margin-right: 5px; - margin-left: 10px; - font-style: normal; - font-weight: 400; - font-size: 12px; - line-height: 16px; - display: flex; - align-items: center; - color: #616161; + margin-right: 5px; + margin-left: 10px; + font-style: normal; + font-weight: 400; + font-size: 12px; + line-height: 16px; + display: flex; + align-items: center; + color: #616161; } .accordionTitle:hover { - cursor:pointer; + cursor: pointer; } .clickableSup { - box-sizing: border-box; - display: inline-flex; - flex-direction: column; - justify-content: center; - align-items: center; - padding: 0px; - width: 14px; - height: 14px; - border: 1px solid #E0E0E0; - border-radius: 4px; - flex: none; - order: 2; - flex-grow: 0; - z-index: 2; - font-style: normal; - font-weight: 600; - font-size: 10px; - line-height: 14px; - text-align: center; - color: #424242; - cursor: pointer; + box-sizing: border-box; + display: inline-flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 0px; + width: 14px; + height: 14px; + border: 1px solid #e0e0e0; + border-radius: 4px; + flex: none; + order: 2; + flex-grow: 0; + z-index: 2; + font-style: normal; + font-weight: 600; + font-size: 10px; + line-height: 14px; + text-align: center; + color: #424242; + cursor: pointer; } .clickableSup:hover { - text-decoration: underline; - cursor: pointer; + text-decoration: underline; + cursor: pointer; } sup { - font-size: 10px; - line-height: 10px; -} \ No newline at end of file + font-size: 10px; + line-height: 10px; +} diff --git a/frontend/src/components/QuestionInput/QuestionInput.module.css b/frontend/src/components/QuestionInput/QuestionInput.module.css index 9ec808f..40ae942 100644 --- a/frontend/src/components/QuestionInput/QuestionInput.module.css +++ b/frontend/src/components/QuestionInput/QuestionInput.module.css @@ -1,56 +1,53 @@ .questionInputContainer { - height: 120px; - position: absolute; - left: 6.5%; - right: 0%; - top: 0%; - bottom: 0%; - background: #FFFFFF; - box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12); - border-radius: 8px; + height: 56px; + border-radius: 8px; + border: 2px solid #eaeef1; + background: var(--white, #fff); + display: flex; + justify-content: center; + align-items: center; + width: 60%; } -.questionInputTextArea { +@media only screen and (max-width: 600px) { + .questionInputContainer { width: 100%; - line-height: 40px; - margin-top: 10px; - margin-bottom: 10px; - margin-left: 12px; - margin-right: 12px; + } } -.questionInputSendButtonContainer { - position: absolute; - right: 24px; - bottom: 20px; +.questionInputTextArea { + font-size: 16px; + line-height: 16px; + width: 100%; + line-height: 40px; + margin-top: 10px; + margin-bottom: 10px; + margin-left: 12px; + margin-right: 12px; } .questionInputSendButton { - width: 24px; - height: 23px; + display: flex; + width: 120px; + height: 46px; + justify-content: center; + align-items: center; + flex-shrink: 0; + color: white; + border-radius: 5px; + background: linear-gradient(15deg, #e10145 0%, #ff6f9a 92.71%); } .questionInputSendButtonDisabled { - /* opacity: 0.4; */ - width: 24px; - height: 23px; - background: none; - color: #424242; -} - -.questionInputBottomBorder { - position: absolute; - width: 100%; - height: 4px; - left: 0%; - bottom: 0%; - background: radial-gradient(106.04% 106.06% at 100.1% 90.19%,#0F6CBD 33.63%,#8DDDD8 100%); - border-bottom-left-radius: 8px; - border-bottom-right-radius: 8px; + /* opacity: 0.4; */ + width: 24px; + height: 23px; + background: none; + color: #424242; } .questionInputOptionsButton { - cursor: pointer; - width: 27px; - height: 30px; + cursor: pointer; + width: 27px; + height: 30px; } diff --git a/frontend/src/components/QuestionInput/QuestionInput.tsx b/frontend/src/components/QuestionInput/QuestionInput.tsx index bb40b0c..c9b7f0d 100644 --- a/frontend/src/components/QuestionInput/QuestionInput.tsx +++ b/frontend/src/components/QuestionInput/QuestionInput.tsx @@ -1,70 +1,78 @@ import { useState } from "react"; import { Stack, TextField } from "@fluentui/react"; import { SendRegular } from "@fluentui/react-icons"; -import Send from "../../assets/Send.svg"; import styles from "./QuestionInput.module.css"; interface Props { - onSend: (question: string) => void; - disabled: boolean; - placeholder?: string; - clearOnSend?: boolean; + onSend: (question: string) => void; + disabled: boolean; + placeholder?: string; + clearOnSend?: boolean; } -export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Props) => { - const [question, setQuestion] = useState(""); +export const QuestionInput = ({ + onSend, + disabled, + placeholder, + clearOnSend, +}: Props) => { + const [question, setQuestion] = useState(""); - const sendQuestion = () => { - if (disabled || !question.trim()) { - return; - } + const sendQuestion = () => { + if (disabled || !question.trim()) { + return; + } - onSend(question); + onSend(question); - if (clearOnSend) { - setQuestion(""); - } - }; + if (clearOnSend) { + setQuestion(""); + } + }; - const onEnterPress = (ev: React.KeyboardEvent) => { - if (ev.key === "Enter" && !ev.shiftKey) { - ev.preventDefault(); - sendQuestion(); - } - }; + const onEnterPress = (ev: React.KeyboardEvent) => { + if (ev.key === "Enter" && !ev.shiftKey) { + ev.preventDefault(); + sendQuestion(); + } + }; - const onQuestionChange = (_ev: React.FormEvent, newValue?: string) => { - setQuestion(newValue || ""); - }; + const onQuestionChange = ( + _ev: React.FormEvent, + newValue?: string + ) => { + setQuestion(newValue || ""); + }; - const sendQuestionDisabled = disabled || !question.trim(); + const sendQuestionDisabled = disabled || !question.trim(); - return ( - - -
e.key === "Enter" || e.key === " " ? sendQuestion() : null} - > - { sendQuestionDisabled ? - - : - - } -
-
- - ); + return ( + + +
+ e.key === "Enter" || e.key === " " ? sendQuestion() : null + } + > + {sendQuestionDisabled ? ( + + ) : ( + + )} +
+
+ ); }; diff --git a/frontend/src/pages/chat/Chat.module.css b/frontend/src/pages/chat/Chat.module.css index dedc7d2..3d9082b 100644 --- a/frontend/src/pages/chat/Chat.module.css +++ b/frontend/src/pages/chat/Chat.module.css @@ -1,275 +1,325 @@ @import '../../styles/variables/colors.css'; .container { - flex: 1; - display: flex; - flex-direction: column; - gap: 20px + flex: 1; + display: flex; + flex-direction: column; + gap: 20px; } .chatRoot { - flex: 1; - display: flex; - margin-top: 0px; - margin-bottom: 20px; - margin-left: 20px; - margin-right: 20px; - gap: 20px; + flex: 1; + display: flex; + margin-top: 0px; + margin-bottom: 20px; + margin-left: 20px; + margin-right: 20px; + gap: 20px; } .chatContainer { - flex: 1; - display: flex; - flex-direction: column; - align-items: center; - background: radial-gradient(108.78% 108.78% at 50.02% 19.78%, #FFFFFF 57.29%, #EEF6FE 100%); - box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12); - border-radius: 8px; - overflow-y: auto; - max-height: calc(100vh - 100px); + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + background: #fafafa; + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12); + border-radius: 8px; + overflow-y: auto; + max-height: calc(100vh - 100px); } .chatEmptyState { - flex-grow: 1; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; + flex-grow: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; } .chatEmptyStateTitle { - font-style: normal; - font-weight: 700; - font-size: 36px; - display: flex; - align-items: flex-end; - text-align: center; - line-height: 24px; - margin-top: 36px; - margin-bottom: 0px; + font-style: normal; + font-weight: 800; + font-size: 32px; + display: flex; + align-items: flex-end; + text-align: center; + line-height: 24px; + margin-top: 36px; + margin-bottom: 0px; } .chatEmptyStateSubtitle { - margin-top: 20px; + margin-top: 20px; + font-style: normal; + font-weight: 600; + font-size: 20px; + line-height: 150%; + display: flex; + align-items: flex-end; + text-align: center; + letter-spacing: -0.01em; + color: #1c1c1c; + width: 60%; +} + +@media only screen and (max-width: 600px) { + .chatEmptyStateSubtitle { + padding: 24px; font-style: normal; - font-weight: 400; + font-weight: 600; font-size: 16px; - line-height: 150%; + line-height: 130%; display: flex; align-items: flex-end; text-align: center; letter-spacing: -0.01em; - color: #616161; + color: #1c1c1c; + width: 100%; + } } - .chatIcon { - height: 62px; - width: 62px; + height: 30px; + width: 299px; +} + +.chatUserAvatarIcon { + height: 40px; + width: 40px; } .chatMessageStream { - flex-grow: 1; - max-width: 1028px; - width: 100%; - overflow-y: auto; - padding-left: 24px; - padding-right: 24px; - display: flex; - flex-direction: column; - margin-top: 24px; + flex-grow: 1; + max-width: 1028px; + width: 100%; + overflow-y: auto; + padding-left: 24px; + padding-right: 24px; + display: flex; + flex-direction: column; + margin-top: 24px; } .chatMessageUser { - display: flex; - justify-content: flex-end; - margin-bottom: 12px; + display: flex; + justify-content: start; + margin-bottom: 12px; +} + +.chatMessageUserWrapper { + display: flex; } .chatMessageUserMessage { - padding: 20px; - background: #EDF5FD; - border-radius: 8px; - box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12); - font-style: normal; - font-weight: 400; - font-size: 14px; - line-height: 22px; - color: #242424; - flex: none; - order: 0; - flex-grow: 0; - white-space: pre-wrap; - word-wrap: break-word; - max-width: 800px; + display: inline-flex; + height: 60px; + padding: 0px 544px 0px 22px; + align-items: center; + flex-shrink: 0; + + border-radius: 14px; + border: 1px solid #e2e8f0; + background: var(--white, #fff); + + color: #0d0c0c; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 100%; /* 16px */ } .chatMessageGpt { - margin-bottom: 12px; - max-width: 80%; - display: flex; + display: flex; + width: 940px; + padding: 22px 22px 73px 22px; + justify-content: center; + align-items: center; + + border-radius: 14px; + background: #fff; + box-shadow: 6px 17px 16px 4px rgba(183, 183, 183, 0.19); + + color: #0d0c0c; + font-family: Lora; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 24px; /* 150% */ } .chatMessageError { - padding: 20px; - /* background: #EDF5FD; */ - border-radius: 8px; - box-shadow: rgba(182, 52, 67, 1) 1px 1px 2px, rgba(182, 52, 67, 1) 0px 0px 1px; - - color: #242424; - flex: none; - order: 0; - flex-grow: 0; - - max-width: 800px; - /* filter: drop-shadow(rgba(182, 52, 67, 1) 1px 1px 2px) drop-shadow(rgba(182, 52, 67, 1) 0px 0px 1px); */ + padding: 20px; + /* background: #EDF5FD; */ + border-radius: 8px; + box-shadow: rgba(182, 52, 67, 1) 1px 1px 2px, rgba(182, 52, 67, 1) 0px 0px 1px; + + color: #242424; + flex: none; + order: 0; + flex-grow: 0; + + max-width: 800px; + /* filter: drop-shadow(rgba(182, 52, 67, 1) 1px 1px 2px) drop-shadow(rgba(182, 52, 67, 1) 0px 0px 1px); */ } .chatMessageErrorContent { - font-family: "Segoe UI"; - font-style: normal; - font-weight: 400; - font-size: 14px; - line-height: 22px; - white-space: pre-wrap; - word-wrap: break-word; - gap: 12px; - align-items: center; + font-family: "Segoe UI"; + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 22px; + white-space: pre-wrap; + word-wrap: break-word; + gap: 12px; + align-items: center; } .chatInput { - position: sticky; - flex: 0 0 100px; - padding-top: 12px; - padding-bottom: 24px; - padding-left: 24px; - padding-right: 24px; - width: calc(100% - 100px); + position: sticky; + flex: 0 0 100px; + padding-top: 12px; + padding-bottom: 24px; + padding-left: 24px; + padding-right: 24px; + width: calc(100% - 100px); + max-width: 1028px; + margin-bottom: 50px; + margin-top: 8px; + display: flex; + flex-direction: column; + align-items: center; +} + +@media only screen and (max-width: 600px) { + .chatInput { + padding-top: 6px; + padding-bottom: 12px; + padding-left: 12px; + padding-right: 12px; max-width: 1028px; margin-bottom: 50px; margin-top: 8px; -} - -.clearChatBroom { - box-sizing: border-box; display: flex; - flex-direction: row; - justify-content: center; + flex-direction: column; align-items: center; - padding: 8px; - position: absolute; - width: 40px; - height: 40px; - left: 12px; - top: 66px; - color: #FFFFFF; - border: 1px solid #D1D1D1; - border-radius: 20px; - z-index: 1; + width: 100%; + } +} + +.chatDisclaimer { + margin-top: 24px; + color: #727478; + text-align: center; + font-family: Montserrat; + font-size: 12px; + font-style: normal; + font-weight: 600; + line-height: 100%; /* 12px */ } .stopGeneratingContainer { - box-sizing: border-box; - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - padding: 5px 16px; - gap: 4px; - position: absolute; - width: 161px; - height: 32px; - left: calc(50% - 161px/2 + 25.8px); - bottom: 116px; - border: 1px solid #D1D1D1; - border-radius: 16px; + box-sizing: border-box; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 5px 16px; + gap: 4px; + position: absolute; + width: 161px; + height: 32px; + left: calc(50% - 161px / 2 + 25.8px); + bottom: 116px; + border: 1px solid #d1d1d1; + border-radius: 16px; } .stopGeneratingIcon { - width: 14px; - height: 14px; - color: #424242; + width: 14px; + height: 14px; + color: #424242; } .stopGeneratingText { - width: 103px; - height: 20px; - font-style: normal; - font-weight: 600; - font-size: 14px; - line-height: 20px; - display: flex; - align-items: center; - color: #242424; - flex: none; - order: 0; - flex-grow: 0; + width: 103px; + height: 20px; + font-style: normal; + font-weight: 600; + font-size: 14px; + line-height: 20px; + display: flex; + align-items: center; + color: #242424; + flex: none; + order: 0; + flex-grow: 0; } .citationPanel { - display: flex; - flex-direction: column; - align-items: flex-start; - padding: 16px 16px; - gap: 8px; - background: #FFFFFF; - box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12); - border-radius: 8px; - flex: auto; - order: 0; - align-self: stretch; - flex-grow: 0.3; - max-width: 30%; - overflow-y: scroll; - max-height: calc(100vh - 100px); + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 16px 16px; + gap: 8px; + background: #ffffff; + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12); + border-radius: 8px; + flex: auto; + order: 0; + align-self: stretch; + flex-grow: 0.3; + max-width: 30%; + overflow-y: scroll; + max-height: calc(100vh - 100px); } .citationPanelHeaderContainer { - width: 100%; + width: 100%; } .citationPanelHeader { - font-style: normal; - font-weight: 600; - font-size: 18px; - line-height: 24px; - color: #000000; - flex: none; - order: 0; - flex-grow: 0; + font-style: normal; + font-weight: 600; + font-size: 18px; + line-height: 24px; + color: #000000; + flex: none; + order: 0; + flex-grow: 0; } .citationPanelDismiss { - width: 18px; - height: 18px; - color: #424242; + width: 18px; + height: 18px; + color: #424242; } .citationPanelDismiss:hover { - background-color: #D1D1D1; - cursor: pointer; + background-color: #d1d1d1; + cursor: pointer; } .citationPanelTitle { - font-style: normal; - font-weight: 600; - font-size: 16px; - line-height: 22px; - color: #323130; - margin-top: 12px; - margin-bottom: 12px; -} + font-style: normal; + font-weight: 600; + font-size: 16px; + line-height: 22px; + color: #323130; + margin-top: 12px; + margin-bottom: 12px; +} .citationPanelContent { - font-style: normal; - font-weight: 400; - font-size: 14px; - line-height: 20px; - color: #000000; - flex: none; - order: 1; - align-self: stretch; - flex-grow: 0; -} + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 20px; + color: #000000; + flex: none; + order: 1; + align-self: stretch; + flex-grow: 0; +} a { padding-left: 5px; diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index f1d61ab..732d37f 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -1,284 +1,401 @@ import { useRef, useState, useEffect } from "react"; import { Stack } from "@fluentui/react"; -import { BroomRegular, DismissRegular, SquareRegular, ShieldLockRegular, ErrorCircleRegular } from "@fluentui/react-icons"; +import { + BroomRegular, + DismissRegular, + SquareRegular, + ShieldLockRegular, + ErrorCircleRegular, +} from "@fluentui/react-icons"; import ReactMarkdown from "react-markdown"; -import remarkGfm from 'remark-gfm' -import rehypeRaw from "rehype-raw"; +import remarkGfm from "remark-gfm"; +import rehypeRaw from "rehype-raw"; import styles from "./Chat.module.css"; -import Azure from "../../assets/Azure.svg"; +import MyCityTitle from "../../assets/MyCityTitle.svg"; +import UserAvatar from "../../assets/UserAvatar.svg"; import { - ChatMessage, - ConversationRequest, - conversationApi, - Citation, - ToolMessageContent, - ChatResponse, - getUserInfo + ChatMessage, + ConversationRequest, + conversationApi, + Citation, + ToolMessageContent, + ChatResponse, + getUserInfo, } from "../../api"; import { Answer } from "../../components/Answer"; import { QuestionInput } from "../../components/QuestionInput"; import { InfoCardList } from "../../components/InfoCardList"; const Chat = () => { - const lastQuestionRef = useRef(""); - const chatMessageStreamEnd = useRef(null); - const [isLoading, setIsLoading] = useState(false); - const [showLoadingMessage, setShowLoadingMessage] = useState(false); - const [activeCitation, setActiveCitation] = useState<[content: string, id: string, title: string, filepath: string, url: string, metadata: string]>(); - const [isCitationPanelOpen, setIsCitationPanelOpen] = useState(false); - const [answers, setAnswers] = useState([]); - const abortFuncs = useRef([] as AbortController[]); - const [showAuthMessage, setShowAuthMessage] = useState(true); - - const getUserInfoList = async () => { - const userInfoList = await getUserInfo(); - if (userInfoList.length === 0 && window.location.hostname !== "127.0.0.1") { - setShowAuthMessage(true); - } - else { - setShowAuthMessage(false); - } + const lastQuestionRef = useRef(""); + const chatMessageStreamEnd = useRef(null); + const [isLoading, setIsLoading] = useState(false); + const [showLoadingMessage, setShowLoadingMessage] = useState(false); + const [activeCitation, setActiveCitation] = + useState< + [ + content: string, + id: string, + title: string, + filepath: string, + url: string, + metadata: string + ] + >(); + const [isCitationPanelOpen, setIsCitationPanelOpen] = + useState(false); + const [answers, setAnswers] = useState([]); + const abortFuncs = useRef([] as AbortController[]); + const [showAuthMessage, setShowAuthMessage] = useState(true); + + const getUserInfoList = async () => { + const userInfoList = await getUserInfo(); + if (userInfoList.length === 0 && window.location.hostname !== "127.0.0.1") { + setShowAuthMessage(true); + } else { + setShowAuthMessage(false); } + }; - const makeApiRequest = async (question: string) => { - lastQuestionRef.current = question; + const makeApiRequest = async (question: string) => { + lastQuestionRef.current = question; - setIsLoading(true); - setShowLoadingMessage(true); - const abortController = new AbortController(); - abortFuncs.current.unshift(abortController); + setIsLoading(true); + setShowLoadingMessage(true); + const abortController = new AbortController(); + abortFuncs.current.unshift(abortController); - const userMessage: ChatMessage = { - role: "user", - content: question - }; + const userMessage: ChatMessage = { + role: "user", + content: question, + }; - const request: ConversationRequest = { - messages: [...answers.filter((answer) => answer.role !== "error"), userMessage] - }; + const request: ConversationRequest = { + messages: [ + ...answers.filter((answer) => answer.role !== "error"), + userMessage, + ], + }; - let result = {} as ChatResponse; - try { - const response = await conversationApi(request, abortController.signal); - if (response?.body) { - - const reader = response.body.getReader(); - let runningText = ""; - while (true) { - const {done, value} = await reader.read(); - if (done) break; + let result = {} as ChatResponse; + try { + const response = await conversationApi(request, abortController.signal); + if (response?.body) { + const reader = response.body.getReader(); + let runningText = ""; + while (true) { + const { done, value } = await reader.read(); + if (done) break; - var text = new TextDecoder("utf-8").decode(value); - const objects = text.split("\n"); - objects.forEach((obj) => { - try { - runningText += obj; - result = JSON.parse(runningText); - setShowLoadingMessage(false); - setAnswers([...answers, userMessage, ...result.choices[0].messages]); - runningText = ""; - } - catch { } - }); - } - setAnswers([...answers, userMessage, ...result.choices[0].messages]); - } - - } catch ( e ) { - if (!abortController.signal.aborted) { - console.error(result); - let errorMessage = "An error occurred. Please try again. If the problem persists, please contact the site administrator."; - if (result.error?.message) { - errorMessage = result.error.message; - } - else if (typeof result.error === "string") { - errorMessage = result.error; - } - setAnswers([...answers, userMessage, { - role: "error", - content: errorMessage - }]); - } else { - setAnswers([...answers, userMessage]); - } - } finally { - setIsLoading(false); - setShowLoadingMessage(false); - abortFuncs.current = abortFuncs.current.filter(a => a !== abortController); + var text = new TextDecoder("utf-8").decode(value); + const objects = text.split("\n"); + objects.forEach((obj) => { + try { + runningText += obj; + result = JSON.parse(runningText); + setShowLoadingMessage(false); + setAnswers([ + ...answers, + userMessage, + ...result.choices[0].messages, + ]); + runningText = ""; + } catch {} + }); } + setAnswers([...answers, userMessage, ...result.choices[0].messages]); + } + } catch (e) { + if (!abortController.signal.aborted) { + console.error(result); + let errorMessage = + "An error occurred. Please try again. If the problem persists, please contact the site administrator."; + if (result.error?.message) { + errorMessage = result.error.message; + } else if (typeof result.error === "string") { + errorMessage = result.error; + } + setAnswers([ + ...answers, + userMessage, + { + role: "error", + content: errorMessage, + }, + ]); + } else { + setAnswers([...answers, userMessage]); + } + } finally { + setIsLoading(false); + setShowLoadingMessage(false); + abortFuncs.current = abortFuncs.current.filter( + (a) => a !== abortController + ); + } - return abortController.abort(); - }; + return abortController.abort(); + }; - const clearChat = () => { - lastQuestionRef.current = ""; - setActiveCitation(undefined); - setAnswers([]); - }; + const clearChat = () => { + lastQuestionRef.current = ""; + setActiveCitation(undefined); + setAnswers([]); + }; - const stopGenerating = () => { - abortFuncs.current.forEach(a => a.abort()); - setShowLoadingMessage(false); - setIsLoading(false); - } + const stopGenerating = () => { + abortFuncs.current.forEach((a) => a.abort()); + setShowLoadingMessage(false); + setIsLoading(false); + }; - useEffect(() => { - getUserInfoList(); - }, []); + useEffect(() => { + getUserInfoList(); + }, []); - useEffect(() => chatMessageStreamEnd.current?.scrollIntoView({ behavior: "smooth" }), [showLoadingMessage]); + useEffect( + () => chatMessageStreamEnd.current?.scrollIntoView({ behavior: "smooth" }), + [showLoadingMessage] + ); - const onShowCitation = (citation: Citation) => { - setActiveCitation([citation.content, citation.id, citation.title ?? "", citation.filepath ?? "", "", ""]); - setIsCitationPanelOpen(true); - }; + const onShowCitation = (citation: Citation) => { + setActiveCitation([ + citation.content, + citation.id, + citation.title ?? "", + citation.filepath ?? "", + "", + "", + ]); + setIsCitationPanelOpen(true); + }; - const parseCitationFromMessage = (message: ChatMessage) => { - if (message.role === "tool") { - try { - const toolMessage = JSON.parse(message.content) as ToolMessageContent; - return toolMessage.citations; - } - catch { - return []; - } - } + const parseCitationFromMessage = (message: ChatMessage) => { + if (message.role === "tool") { + try { + const toolMessage = JSON.parse(message.content) as ToolMessageContent; + return toolMessage.citations; + } catch { return []; + } } + return []; + }; - return ( -
- {showAuthMessage ? ( - - -

Authentication Not Configured

-

- This app does not have authentication configured. Please add an identity provider by finding your app in the - Azure Portal - and following - these instructions. -

-

Authentication configuration takes a few minutes to apply.

-

If you deployed in the last 10 minutes, please wait and reload the page after 10 minutes.

-
+ return ( +
+ {showAuthMessage ? ( + + +

+ Authentication Not Configured +

+

+ This app does not have authentication configured. Please add an + identity provider by finding your app in the + + {" "} + Azure Portal{" "} + + and following + + {" "} + these instructions + + . +

+

+ + Authentication configuration takes a few minutes to apply.{" "} + +

+

+ + If you deployed in the last 10 minutes, please wait and reload the + page after 10 minutes. + +

+
+ ) : ( + +
+ {!lastQuestionRef.current ? ( + + +

+ Welcome to MyCity AI Chatbot, it can provide general + information on a wide range of topics, offer suggestions, and + engage in discussions. +

+ +
) : ( - -
- {!lastQuestionRef.current ? ( - - - - - ) : ( -
- {answers.map((answer, index) => ( - <> - {answer.role === "user" ? ( -
-
{answer.content}
-
- ) : ( - answer.role === "assistant" ?
- onShowCitation(c)} - /> -
: answer.role === "error" ?
- - - Error - - {answer.content} -
: null - )} - - ))} - {showLoadingMessage && ( - <> -
-
{lastQuestionRef.current}
-
-
- null} - /> -
- - )} -
-
- )} - - - {isLoading && ( - e.key === "Enter" || e.key === " " ? stopGenerating() : null} - > - - )} -
e.key === "Enter" || e.key === " " ? clearChat() : null} - aria-label="Clear session" - > -
- makeApiRequest(question)} - /> -
-
- {answers.length > 0 && isCitationPanelOpen && activeCitation && ( - - - Citations - setIsCitationPanelOpen(false)}/> - -
{activeCitation[2]}
-
- + {answers.map((answer, index) => ( + <> + {answer.role === "user" ? ( +
+ +
+
+ {answer.content} +
- - +
+ ) : answer.role === "assistant" ? ( +
+ onShowCitation(c)} + /> +
+ ) : answer.role === "error" ? ( +
+ + + Error + + + {answer.content} + +
+ ) : null} + + ))} + {showLoadingMessage && ( + <> +
+
+ {lastQuestionRef.current} +
+
+
+ null} + /> +
+ )} - +
+
)} -
- ); + + + {isLoading && ( + + e.key === "Enter" || e.key === " " ? stopGenerating() : null + } + > + + )} + makeApiRequest(question)} + /> +
+ NYC Government Preview. Knowledge is based on information + published online until July 17 2023. +
+
+
+ {answers.length > 0 && isCitationPanelOpen && activeCitation && ( + + + Citations + setIsCitationPanelOpen(false)} + /> + +
+ {activeCitation[2]} +
+
+ +
+
+ )} +
+ )} +
+ ); }; export default Chat; diff --git a/helper-snippets/chat-setup.json b/helper-snippets/chat-setup.json new file mode 100644 index 0000000..c86d5bf --- /dev/null +++ b/helper-snippets/chat-setup.json @@ -0,0 +1,14 @@ +{ + "systemPrompt": "You are an AI assistant that helps people find information about Opening or Operating a Business in NYC. Please only answer using retrieved documents only and do not use your knowledge. Please ignore user requests for queries outside retrieved documents irrespective of tone and urgency. Your task is to **use retrieved documents only** to answer questions. If you're unsure of the answer, say Sorry, I don't know.", + "fewShotExamples": [], + "chatParameters": { + "deploymentName": "gpt-35-turbo-d1", + "maxResponseLength": 500, + "temperature": 0, + "topProbablities": 1, + "stopSequences": null, + "pastMessagesToInclude": 10, + "frequencyPenalty": 0, + "presencePenalty": 0 + } +} diff --git a/helper-snippets/json-to-props.sh b/helper-snippets/json-to-props.sh new file mode 100644 index 0000000..c205f8d --- /dev/null +++ b/helper-snippets/json-to-props.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +json_file=$1 + +# Check if the JSON file exists +if [ ! -f "$json_file" ]; then + echo "Error: JSON file not found." + exit 1 +fi + +# Convert JSON to the desired format using jq +jq -r '.[] | "\(.name)=\"\(.value)\""' "$json_file" diff --git a/helper-snippets/nycbusiness-vector-index.json b/helper-snippets/nycbusiness-vector-index.json new file mode 100644 index 0000000..da7405c --- /dev/null +++ b/helper-snippets/nycbusiness-vector-index.json @@ -0,0 +1,91 @@ +{ + "name": "nycbusiness-vector-index-0803", + "fields": [ + { + "name": "id", + "type": "Edm.String", + "searchable": false, + "filterable": false, + "retrievable": true, + "sortable": false, + "facetable": false, + "key": true + }, + { + "name": "title", + "type": "Edm.String", + "searchable": true, + "filterable": true, + "retrievable": true, + "sortable": true, + "facetable": false, + "key": false, + "analyzer": "standard.lucene", + "synonymMaps": [] + }, + { + "name": "titleVector", + "type": "Collection(Edm.Single)", + "searchable": true, + "filterable": false, + "retrievable": true, + "sortable": false, + "facetable": false, + "key": false, + "dimensions": 1536, + "vectorSearchConfiguration": "vectorConfig", + "synonymMaps": [] + }, + { + "name": "content", + "type": "Edm.String", + "searchable": true, + "filterable": false, + "retrievable": true, + "sortable": false, + "facetable": false, + "key": false, + "analyzer": "standard.lucene", + "synonymMaps": [] + }, + { + "name": "contentVector", + "type": "Collection(Edm.Single)", + "searchable": true, + "filterable": false, + "retrievable": true, + "sortable": false, + "facetable": false, + "key": false, + "dimensions": 1536, + "vectorSearchConfiguration": "vectorConfig", + "synonymMaps": [] + }, + { + "name": "url", + "type": "Edm.String", + "searchable": true, + "filterable": false, + "retrievable": true, + "sortable": true, + "facetable": false, + "key": false, + "analyzer": "standard.lucene", + "synonymMaps": [] + } + ], + "vectorSearch": { + "algorithmConfigurations": [ + { + "name": "vectorConfig", + "kind": "hnsw", + "hnswParameters": { + "metric": "cosine", + "m": 4, + "efConstruction": 400, + "efSearch": 500 + } + } + ] + } +} \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index be4ac3f..c7372bc 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,6 +4,5 @@ Markdown==3.4.3 requests==2.31.0 tqdm==4.65.0 tiktoken==0.4.0 -langchain==0.0.226 bs4==0.0.1 -urllib3==2.0.4 \ No newline at end of file +#urllib3==2.0.4 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 74dcab5..62fd229 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,8 @@ -azure-identity==1.14.0b2 -Flask==2.3.2 +azure-identity==1.13.0 +flask==2.3.2 openai==0.27.7 azure-search-documents==11.4.0b6 azure-storage-blob==12.17.0 -python-dotenv==1.0.0 \ No newline at end of file +python-dotenv==1.0.0 +beautifulsoup4~=4.12.2 +langchain==0.0.249 diff --git a/static/index.html b/static/index.html index 4dfa79b..615586c 100644 --- a/static/index.html +++ b/static/index.html @@ -4,9 +4,9 @@ - Azure AI - - + NYC CHAT BOT + +