diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..f1d0a9b200 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,5 @@ +# Codeowners for chatgpt-on-azure + +# Global owners of repo +# One of these owners needs to approve every pull request +* @bdleavitt @kjpap \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 15c7f60228..e4c7a7c89c 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,33 +1,20 @@ - -> Please provide us with the following information: -> --------------------------------------------------------------- - ### This issue is for a: (mark with an `x`) ``` -- [ ] bug report -> please search issues before submitting -- [ ] feature request -- [ ] documentation issue or request -- [ ] regression (a behavior that used to work and stopped in a new release) +[ ] bug report -> please search issues before submitting +[ ] feature request (including template changes) +[ ] documentation issue or request +[ ] regression (a behavior that used to work and stopped in a new release) ``` -### Minimal steps to reproduce -> - -### Any log messages given by the failure +### Description +Please include any relevant information, including repro steps for a bug (with log messages, version, OS, etc..) > ### Expected/desired behavior -> - -### OS and Version? -> Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?) - -### Versions -> +Please include any acceptance criteria here (should look like tasks to complete for the issue to be resolved) +- [ ] +### Design/Wiki Reference +Please add URLs to the design/wiki. Please note that README should be updated with relevant information as part of feature development to be code complete. ### Mention any other details that might be useful - -> --------------------------------------------------------------- -> Thanks! We'll be in touch soon. +> \ No newline at end of file diff --git a/.gitignore b/.gitignore index 02031bcda7..111b016f2f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # Azure az webapp deployment details -.azure +.azure* *_env # Byte-compiled / optimized / DLL files @@ -7,6 +7,8 @@ __pycache__/ *.py[cod] *$py.class +scratch/ + # C extensions *.so @@ -144,4 +146,5 @@ cython_debug/ # NPM npm-debug.log* node_modules -static/ \ No newline at end of file +static/ +app/frontend/package-lock.json diff --git a/README.md b/README.md index e01dc36b2f..c8cd5ea413 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ChatGPT + Enterprise data with Azure OpenAI and Cognitive Search +# ChatGPT + Enterprise data with Azure OpenAI... Now with "Vanilla" Chat experience and Conversation History (powered by CosmosDB) [![Open in GitHub Codespaces](https://img.shields.io/static/v1?style=for-the-badge&label=GitHub+Codespaces&message=Open&color=brightgreen&logo=github)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=599293758&machine=standardLinux32gb&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=WestUs2) [![Open in Remote - Containers](https://img.shields.io/static/v1?style=for-the-badge&label=Remote%20-%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/azure-samples/azure-search-openai-demo) @@ -77,6 +77,30 @@ It will look like the following: > NOTE: You can also use existing Search and Storage Accounts. See `./infra/main.parameters.json` for list of environment variables to pass to `azd env set` to configure those existing resources. +#### For local development +If you are modifying this app locally, you can greatly speed up the time to change and test the app by allowing the app to "hot reload". To do this open two separate terminal sessions. + +**Start the backend flask app with hot reload** +Run `debug.ps1` or `debug.sh` +``` +cd ./app +pwsh ./debug.ps1 +``` +or in bash +``` +cd ./app +bash ./debug.sh +``` + +**Start the frontend app with hot reload** +Run the following command to enable hot reload on the frontend +``` +cd ./app/frontend +npm run dev +``` +This will create a VITE server instance that will reload when code changes. You can access the frontend app locally (i.e. http://localhost:5173/). The VITE server is set to proxy API calls to the flask application using the settings in `frontend/vite.config.ts` (i.e. https://localhost:5000). + + #### Deploying or re-deploying a local clone of the repo: * Simply run `azd up` @@ -123,4 +147,4 @@ Once in the web app: If you see this error while running `azd deploy`: `read /tmp/azd1992237260/backend_env/lib64: is a directory`, then delete the `./app/backend/backend_env folder` and re-run the `azd deploy` command. This issue is being tracked here: https://github.com/Azure/azure-dev/issues/1237 -If the web app fails to deploy and you receive a '404 Not Found' message in your browser, run 'azd deploy'. +If the web app fails to deploy and you receive a '404 Not Found' message in your browser, run 'azd deploy'. \ No newline at end of file diff --git a/app/backend/app.py b/app/backend/app.py index 074f984125..6dbee18310 100644 --- a/app/backend/app.py +++ b/app/backend/app.py @@ -3,6 +3,11 @@ import time import logging import openai +import json +from datetime import datetime +from history.cosmosdbservice import CosmosConversationClient +from auth.auth_utils import get_authenticated_user_details + from flask import Flask, request, jsonify from azure.identity import DefaultAzureCredential from azure.search.documents import SearchClient @@ -10,8 +15,13 @@ from approaches.readretrieveread import ReadRetrieveReadApproach from approaches.readdecomposeask import ReadDecomposeAsk from approaches.chatreadretrieveread import ChatReadRetrieveReadApproach +from approaches.chatconversation import ChatConversationReadApproach from azure.storage.blob import BlobServiceClient +## Logging level for development, set to logging.INFO or logging.DEBUG for more verbose logging +logger = logging.getLogger ('werkzeug') # grabs underlying WSGI logger +logger.setLevel (logging.INFO) # set log level to INFO + # Replace these with your own values, either in environment variables or directly here AZURE_STORAGE_ACCOUNT = os.environ.get("AZURE_STORAGE_ACCOUNT") or "mystorageaccount" AZURE_STORAGE_CONTAINER = os.environ.get("AZURE_STORAGE_CONTAINER") or "content" @@ -20,6 +30,12 @@ AZURE_OPENAI_SERVICE = os.environ.get("AZURE_OPENAI_SERVICE") or "myopenai" AZURE_OPENAI_GPT_DEPLOYMENT = os.environ.get("AZURE_OPENAI_GPT_DEPLOYMENT") or "davinci" AZURE_OPENAI_CHATGPT_DEPLOYMENT = os.environ.get("AZURE_OPENAI_CHATGPT_DEPLOYMENT") or "chat" +AZURE_OPENAI_API_VERSION = os.environ.get("AZURE_OPENAI_API_VERSION") or "2023-05-15" +AZURE_COSMOSDB_DATABASE = os.environ.get("AZURE_COSMOSDB_DATABASE") or "db_conversation_history" +AZURE_COSMOSDB_ACCOUNT = os.environ.get("AZURE_COSMOSDB_ACCOUNT") +AZURE_COSMOSDB_CONVERSATIONS_CONTAINER = os.environ.get("AZURE_COSMOSDB_CONVERSATIONS_CONTAINER") or "conversations" +AZURE_COSMOSDB_MESSAGES_CONTAINER = os.environ.get("AZURE_COSMOSDB_MESSAGES_CONTAINER") or "messages" +AZURE_COSMOSDB_ACCOUNT_KEY = os.environ.get("AZURE_COSMOSDB_ACCOUNT_KEY") or None KB_FIELDS_CONTENT = os.environ.get("KB_FIELDS_CONTENT") or "content" KB_FIELDS_CATEGORY = os.environ.get("KB_FIELDS_CATEGORY") or "category" @@ -34,7 +50,7 @@ # Used by the OpenAI SDK openai.api_type = "azure" openai.api_base = f"https://{AZURE_OPENAI_SERVICE}.openai.azure.com" -openai.api_version = "2022-12-01" +openai.api_version = AZURE_OPENAI_API_VERSION # Comment these two lines out if using keys, set your API key in the OPENAI_API_KEY environment variable instead openai.api_type = "azure_ad" @@ -60,9 +76,29 @@ } chat_approaches = { - "rrr": ChatReadRetrieveReadApproach(search_client, AZURE_OPENAI_CHATGPT_DEPLOYMENT, AZURE_OPENAI_GPT_DEPLOYMENT, KB_FIELDS_SOURCEPAGE, KB_FIELDS_CONTENT) + "rrr": ChatReadRetrieveReadApproach(search_client, AZURE_OPENAI_CHATGPT_DEPLOYMENT, AZURE_OPENAI_GPT_DEPLOYMENT, KB_FIELDS_SOURCEPAGE, KB_FIELDS_CONTENT) +} +## BDL: added another set of approaches for vanilla chatgpt +chatconversation_approaches = { + 'chatconversation': ChatConversationReadApproach(AZURE_OPENAI_CHATGPT_DEPLOYMENT) } +# Initialize a CosmosDB client with AAD auth and containers +cosmos_endpoint = f'https://{AZURE_COSMOSDB_ACCOUNT}.documents.azure.com:443/' +# credential = azure_credential + +if not AZURE_COSMOSDB_ACCOUNT_KEY: + credential = azure_credential +else: + credential = AZURE_COSMOSDB_ACCOUNT_KEY + +cosmos_conversation_client = CosmosConversationClient( + cosmosdb_endpoint=cosmos_endpoint, + credential=credential, + database_name=AZURE_COSMOSDB_DATABASE, + container_name=AZURE_COSMOSDB_CONVERSATIONS_CONTAINER +) + app = Flask(__name__) @app.route("/", defaults={"path": "index.html"}) @@ -94,7 +130,8 @@ def ask(): except Exception as e: logging.exception("Exception in /ask") return jsonify({"error": str(e)}), 500 - + +## BDL: this is the original chat function @app.route("/chat", methods=["POST"]) def chat(): ensure_openai_token() @@ -109,6 +146,243 @@ def chat(): logging.exception("Exception in /chat") return jsonify({"error": str(e)}), 500 + +## BDL: method for creating conversations and storing history messages in cosmos +@app.route("/conversation/add", methods=["POST"]) +def add_conversation(): + authenticated_user = get_authenticated_user_details(request_headers=request.headers) + user_id = authenticated_user['user_principal_id'] + generate_title = False + ensure_openai_token() + + approach = request.json["approach"] + + ## check request for conversation_id + conversation_id = request.json.get("conversation_id", None) + + ## check to see if a conversation title should be generated + generate_title = request.json.get("generate_title", False) + + try: + impl = chatconversation_approaches.get(approach) + if not impl: + return jsonify({"error": "unknown approach"}), 400 + ## BDL TODO: should all of this conversation history be moved to the parent "approach" class so it can be shared across all approaches? + # check for the conversation_id, if the conversation is not set, we will create a new one + if not conversation_id: + generate_title = True ## if this is a new conversation, we will generate a title + conversation_dict = cosmos_conversation_client.create_conversation(user_id=user_id) + conversation_id = conversation_dict['id'] + + ## Format the incoming message object in the "chat/completions" messages format + ## then write it to the conversation history in cosmos + message_prompt = request.json["history"][-1]["user"] + msg = {"role": "user", "content": message_prompt} + resp = cosmos_conversation_client.create_message( + conversation_id=conversation_id, + user_id=user_id, + input_message=msg + ) + + # Submit prompt to Chat Completions for response + r = impl.run(request.json["history"], request.json.get("overrides") or {}) + + ## Format the incoming message object in the "chat/completions" messages format + ## then write it to the conversation history in cosmos + msg = {"role": "assistant", "content": r['answer']} + resp = cosmos_conversation_client.create_message( + conversation_id=conversation_id, + user_id=user_id, + input_message=msg + ) + + if generate_title: + ## Generate a title for the conversation + generate_conversation_title(user_id=user_id, conversation_id=conversation_id, overwrite_title=True) + + ## we need to return the conversation_id in the response so the client can keep track of it + r['conversation_id'] = conversation_id + # returns the response from the bot + return jsonify(r) + + except Exception as e: + logging.exception("Exception in /conversation") + return jsonify({"error": str(e)}), 500 + +## Conversation routes needed read, delete, update +@app.route("/conversation/delete", methods=["POST"]) +def delete_conversation(): + ## get the user id from the request headers + authenticated_user = get_authenticated_user_details(request_headers=request.headers) + user_id = authenticated_user['user_principal_id'] + + ## check request for conversation_id + conversation_id = request.json.get("conversation_id", None) + if not conversation_id: + return jsonify({"error": "conversation_id is required"}), 400 + + ## delete the conversation messages from cosmos first + deleted_messages = cosmos_conversation_client.delete_messages(conversation_id, user_id) + + ## Now delete the conversation + deleted_conversation = cosmos_conversation_client.delete_conversation(user_id, conversation_id) + + #BDL TODO: add some error handling here + return jsonify({"message": "Successfully deleted conversation and messages", "conversation_id": conversation_id}), 200 + +@app.route("/conversation/update", methods=["POST"]) +def update_conversation(): + ## check request for conversation_id + conversation_id = request.json.get("conversation_id", None) + return jsonify({"error": "not implemented"}), 501 + +@app.route("/conversation/list", methods=["POST"]) +def list_conversations(): + authenticated_user = get_authenticated_user_details(request_headers=request.headers) + user_id = authenticated_user['user_principal_id'] + + ## get the conversations from cosmos + conversations = cosmos_conversation_client.get_conversations(user_id) + if not conversations: + return jsonify({"error": f"No conversations for {user_id} were found"}), 404 + + ## return the conversation ids + + return jsonify(conversations), 200 + +@app.route("/conversation/read", methods=["POST"]) +def get_conversation(): + authenticated_user = get_authenticated_user_details(request_headers=request.headers) + user_id = authenticated_user['user_principal_id'] + + ## check request for conversation_id + conversation_id = request.json.get("conversation_id", None) + + if not conversation_id: + return jsonify({"error": "conversation_id is required"}), 400 + + ## get the conversation object and the related messages from cosmos + conversation = cosmos_conversation_client.get_conversation(user_id, conversation_id) + ## return the conversation id and the messages in the bot frontend format + if not conversation: + return jsonify({"error": f"Conversation {conversation_id} was not found. It either does not exist or the logged in user does not have access to it."}), 404 + + # get the messages for the conversation from cosmos + conversation_messages = cosmos_conversation_client.get_messages(user_id, conversation_id) + if not conversation_messages: + return jsonify({"error": f"No messages for {conversation_id} were found"}), 404 + + ## format the messages in the bot frontend format + messages = format_messages(conversation_messages, input_format='cosmos', output_format='botfrontend') + + return jsonify({"conversation_id": conversation_id, "messages": messages}), 200 + +## add a route to generate a title for a conversation +@app.route("/conversation/gen_title", methods=["POST"]) +def gen_title(): + ## lookup the conversation in cosmos + authenticated_user = get_authenticated_user_details(request_headers=request.headers) + user_id = authenticated_user['user_principal_id'] + + ## check request for conversation_id + conversation_id = request.json.get("conversation_id", None) + + overwrite_existing_title = request.json.get("overwrite_existing_title", False) + + try: + conversation_title_response_dict = generate_conversation_title(user_id, conversation_id, overwrite_title=overwrite_existing_title) + except Exception as e: + return jsonify({"error": f"Error generating title for conversation {conversation_id}: {str(e)}"}), 500 + finally: + return jsonify(conversation_title_response_dict) + + +def generate_conversation_title(user_id, conversation_id, overwrite_title=False): + ## get the conversation from cosmos + conversation_dict = cosmos_conversation_client.get_conversation(user_id, conversation_id) + if not conversation_dict: + ## raise an error + raise Exception(f"Conversation {conversation_id} was not found. It either does not exist or the logged in user does not have access to it.") + + ## check if the conversation already has a title + conversation_title = conversation_dict.get('title', None) + + if not overwrite_title and conversation_title: + raise Exception(f"Conversation {conversation_id} already has a title and overwrite_title flag was set to False.") + + ## otherwise go for it and create the title! + ## get the messages for the conversation from cosmos + conversation_messages = cosmos_conversation_client.get_messages(user_id, conversation_id) + if not conversation_messages: + raise Exception(f"No messages for {conversation_id} were found") + + ## generate a title for the conversation + title = create_conversation_title(conversation_messages) + conversation_dict['title'] = title + conversation_dict['updatedAt'] = datetime.utcnow().isoformat() + + ## update the conversation in cosmos + resp = cosmos_conversation_client.upsert_conversation(conversation_dict) + + return resp + + +def create_conversation_title(conversation_messages): + ## make sure the messages are sorted by _ts descending + messages = format_messages(conversation_messages, input_format='cosmos' ,output_format='chatcompletions') + + title_prompt = 'Summarize the conversation so far into a 4-word or less title. Do not use any quotation marks or punctuation. Respond with a json object in the format {{"title": string}}. Do not include any other commentary or description.' + + messages.append({'role': 'user', 'content': title_prompt}) + + ensure_openai_token() + + try: + ## Submit prompt to Chat Completions for response + completion = openai.ChatCompletion.create( + engine=AZURE_OPENAI_CHATGPT_DEPLOYMENT, + messages=messages, + temperature=1, + max_tokens=64 + ) + title = json.loads(completion['choices'][0]['message']['content'])['title'] + + return title + except Exception as e: + return jsonify({"error": f"Error generating title for the conversation: {e}"}), 500 + +def format_messages(messages, input_format='cosmos', output_format='chatcompletions'): + + if input_format == 'cosmos': + ## Convert to the chat/completions format from cosmos + if output_format == 'chatcompletions': + chat_messages = [{'role': msg['role'], 'content': msg['content']} for msg in messages] + return chat_messages + ## Convert to the bot frontend format from cosmos + elif output_format == 'botfrontend': + ## the botfrontend format is pairs of {"user": inputtext, "bot": outputtext} + ## the cosmos format is a list of messages with a role and content. + ## form pairs of user and bot messages from the cosmos messages list + + botfrontend_messages = [] + last_role = None + for i, message in enumerate(messages): + if last_role is None: + last_role = message['role'] + elif last_role == message['role']: + # we have a situation where there are two messages in a row from the same role + # this will cause issues with the frontend due to their chosen format + # for now, we will just skip the second message + last_role = message['role'] + continue + if message['role'] == 'user': + botfrontend_messages.append({"user": message['content']}) + elif message['role'] == 'assistant': + botfrontend_messages[-1]["bot"] = message['content'] + last_role = message['role'] + + return botfrontend_messages + def ensure_openai_token(): global openai_token if openai_token.expires_on < int(time.time()) - 60: diff --git a/app/backend/approaches/chatconversation.py b/app/backend/approaches/chatconversation.py new file mode 100644 index 0000000000..cb7ed9e852 --- /dev/null +++ b/app/backend/approaches/chatconversation.py @@ -0,0 +1,52 @@ +import openai +from approaches.approach import Approach + +# Simple ChatGPT experience that calls the Azure OpenAI APIs directly. The input history is converted to the chat/completions "messages" format and submitted to the model. +class ChatConversationReadApproach(Approach): + + def __init__(self, chatgpt_deployment: str): ## BDL: removed search_client, gpt_deployment, sourcepage_field, content_field as they're not needed for vanilla chatgptread + self.chatgpt_deployment = chatgpt_deployment + + def run(self, history: list[dict], overrides: dict) -> any: + + ## add a system prompt to the messages + ## then convert the input history to the chat/completions "messages" format + + ## if the system override for the prompt is set, use that for the system prompt. Otherwise, use the default + prompt_override = overrides.get("prompt_template") + if prompt_override is None: + system_prompt = "You are an AI chatbot that responds to whatever the user says." + else: + system_prompt = prompt_override + + ## Use the "ChatCompletions" format for the input prompt messages. + messages = [ + {"role": "system", "content": f"{system_prompt}"}, + ] + + for utterance_dict in history: + ## check if the user key exists in the dict + if "user" in utterance_dict: + messages.append({"role": "user", "content": utterance_dict["user"]}) + ## then check if the "bot" key exists in the dict + if "bot" in utterance_dict: + messages.append({"role": "system", "content": utterance_dict["bot"]}) + + openai.api_version = "2023-03-15-preview" + + completion = openai.ChatCompletion.create( + engine=self.chatgpt_deployment, + messages=messages, + temperature=overrides.get("temperature") or 0.7, + max_tokens=1024 + ) + + return {"data_points": "There were no documents searched. This is vanilla", "answer": completion['choices'][0]['message']['content'], "thoughts": f"I have no thoughts, I'm a robot"} + + def get_chat_history_as_text(self, history, include_last_turn=True, approx_max_tokens=1000) -> str: + history_text = "" + for h in reversed(history if include_last_turn else history[:-1]): + history_text = """<|im_start|>user""" +"\n" + h["user"] + "\n" + """<|im_end|>""" + "\n" + """<|im_start|>assistant""" + "\n" + (h.get("bot") + """<|im_end|>""" if h.get("bot") else "") + "\n" + history_text + if len(history_text) > approx_max_tokens*4: + break + return history_text \ No newline at end of file diff --git a/app/backend/auth/auth_utils.py b/app/backend/auth/auth_utils.py new file mode 100644 index 0000000000..785fcb4bc4 --- /dev/null +++ b/app/backend/auth/auth_utils.py @@ -0,0 +1,20 @@ +def get_authenticated_user_details(request_headers): + user_object = {} + + ## check the headers for the Principal-Id (the guid of the signed in user) + if "X-Ms-Client-Principal-Id" not in request_headers.keys(): + ## if it's not, assume we're in development mode and return a default user + from auth.sample_user import sample_user + raw_user_object = sample_user + else: + ## if it is, get the user details from the EasyAuth headers + raw_user_object = {k:v for k,v in request_headers.items()} + + user_object['user_principal_id'] = raw_user_object['X-Ms-Client-Principal-Id'] + user_object['user_name'] = raw_user_object['X-Ms-Client-Principal-Name'] + user_object['auth_provider'] = raw_user_object['X-Ms-Client-Principal-Idp'] + user_object['auth_token'] = raw_user_object['X-Ms-Token-Aad-Id-Token'] + user_object['client_principal_b64'] = raw_user_object['X-Ms-Client-Principal'] + user_object['aad_id_token'] = raw_user_object["X-Ms-Token-Aad-Id-Token"] + + return user_object \ No newline at end of file diff --git a/app/backend/auth/sample_user.py b/app/backend/auth/sample_user.py new file mode 100644 index 0000000000..3e220d5484 --- /dev/null +++ b/app/backend/auth/sample_user.py @@ -0,0 +1,40 @@ +sample_user = { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate, br", + "Accept-Language": "en", + "Client-Ip": "22.222.222.2222:64379", + "Content-Length": "192", + "Content-Type": "application/json", + "Cookie": "AppServiceAuthSession=/AuR5ENU+pmpoN3jnymP8fzpmVBgphx9uPQrYLEWGcxjIITIeh8NZW7r3ePkG8yBcMaItlh1pX4nzg5TFD9o2mxC/5BNDRe/uuu0iDlLEdKecROZcVRY7QsFdHLjn9KB90Z3d9ZeLwfVIf0sZowWJt03BO5zKGB7vZgL+ofv3QY3AaYn1k1GtxSE9HQWJpWar7mOA64b7Lsy62eY3nxwg3AWDsP3/rAta+MnDCzpdlZMFXcJLj+rsCppW+w9OqGhKQ7uCs03BPeon3qZOdmE8cOJW3+i96iYlhneNQDItHyQqEi1CHbBTSkqwpeOwWP4vcwGM22ynxPp7YFyiRw/X361DGYy+YkgYBkXq1AEIDZ44BCBz9EEaEi0NU+m6yUOpNjEaUtrJKhQywcM2odojdT4XAY+HfTEfSqp0WiAkgAuE/ueCu2JDOfvxGjCgJ4DGWCoYdOdXAN1c+MenT4OSvkMO41YuPeah9qk9ixkJI5s80lv8rUu1J26QF6pstdDkYkAJAEra3RQiiO1eAH7UEb3xHXn0HW5lX8ZDX3LWiAFGOt5DIKxBKFymBKJGzbPFPYjfczegu0FD8/NQPLl2exAX3mI9oy/tFnATSyLO2E8DxwP5wnYVminZOQMjB/I4g3Go14betm0MlNXlUbU1fyS6Q6JxoCNLDZywCoU9Y65UzimWZbseKsXlOwYukCEpuQ5QPT55LuEAWhtYier8LSh+fvVUsrkqKS+bg0hzuoX53X6aqUr7YB31t0Z2zt5TT/V3qXpdyD8Xyd884PqysSkJYa553sYx93ETDKSsfDguanVfn2si9nvDpvUWf6/R02FmQgXiaaaykMgYyIuEmE77ptsivjH3hj/MN4VlePFWokcchF4ciqqzonmICmjEHEx5zpjU2Kwa+0y7J5ROzVVygcnO1jH6ZKDy9bGGYL547bXx/iiYBYqSIQzleOAkCeULrGN2KEHwckX5MpuRaqTpoxdZH9RJv0mIWxbDA0kwGsbMICQd0ZODBkPUnE84qhzvXInC+TL7MbutPEnGbzgxBAS1c2Ct4vxkkjykOeOxTPxqAhxoefwUfIwZZax6A9LbeYX2bsBpay0lScHcA==", + "Disguised-Host": "your_app_service.azurewebsites.net", + "Host": "your_app_service.azurewebsites.net", + "Max-Forwards": "10", + "Origin": "https://your_app_service.azurewebsites.net", + "Referer": "https://your_app_service.azurewebsites.net/", + "Sec-Ch-Ua": "\"Microsoft Edge\";v=\"113\", \"Chromium\";v=\"113\", \"Not-A.Brand\";v=\"24\"", + "Sec-Ch-Ua-Mobile": "?0", + "Sec-Ch-Ua-Platform": "\"Windows\"", + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "same-origin", + "Traceparent": "00-24e9a8d1b06f233a3f1714845ef971a9-3fac69f81ca5175c-00", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.42", + "Was-Default-Hostname": "your_app_service.azurewebsites.net", + "X-Appservice-Proto": "https", + "X-Arr-Log-Id": "4102b832-6c88-4c7c-8996-0edad9e4358f", + "X-Arr-Ssl": "2048|256|CN=Microsoft Azure TLS Issuing CA 02, O=Microsoft Corporation, C=US|CN=*.azurewebsites.net, O=Microsoft Corporation, L=Redmond, S=WA, C=US", + "X-Client-Ip": "22.222.222.222", + "X-Client-Port": "64379", + "X-Forwarded-For": "22.222.222.22:64379", + "X-Forwarded-Proto": "https", + "X-Forwarded-Tlsversion": "1.2", + "X-Ms-Client-Principal": "your_base_64_encoded_token", + "X-Ms-Client-Principal-Id": "00000000-0000-0000-0000-000000000000", + "X-Ms-Client-Principal-Idp": "aad", + "X-Ms-Client-Principal-Name": "testusername@constoso.com", + "X-Ms-Token-Aad-Id-Token": "your_aad_id_token", + "X-Original-Url": "/chatgpt", + "X-Site-Deployment-Id": "your_app_service", + "X-Waws-Unencoded-Url": "/chatgpt", + "yourmom": "goes to college" +} diff --git a/app/backend/backend/package-lock.json b/app/backend/backend/package-lock.json new file mode 100644 index 0000000000..dfb18f1156 --- /dev/null +++ b/app/backend/backend/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "backend", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/app/backend/history/cosmosdbservice.py b/app/backend/history/cosmosdbservice.py new file mode 100644 index 0000000000..5b3fd934e3 --- /dev/null +++ b/app/backend/history/cosmosdbservice.py @@ -0,0 +1,118 @@ +import os +import uuid +from datetime import datetime +from flask import Flask, request +from azure.identity import DefaultAzureCredential +from azure.cosmos import CosmosClient, PartitionKey + +class CosmosConversationClient(): + + def __init__(self, cosmosdb_endpoint: str, credential: any, database_name: str, container_name: str): + self.cosmosdb_endpoint = cosmosdb_endpoint + self.credential = credential + self.database_name = database_name + self.container_name = container_name + self.cosmosdb_client = CosmosClient(self.cosmosdb_endpoint, credential=credential) + self.database_client = self.cosmosdb_client.get_database_client(database_name) + self.container_client = self.database_client.get_container_client(container_name) + + def create_conversation(self, user_id, title = ''): + conversation = { + 'id': str(uuid.uuid4()), + 'type': 'conversation', + 'createdAt': datetime.utcnow().isoformat(), + 'updatedAt': datetime.utcnow().isoformat(), + 'userId': user_id, + 'title': title + } + ## TODO: add some error handling based on the output of the upsert_item call + resp = self.container_client.upsert_item(conversation) + if resp: + return resp + else: + return False + + def upsert_conversation(self, conversation): + resp = self.container_client.upsert_item(conversation) + if resp: + return resp + else: + return False + + def delete_conversation(self, user_id, conversation_id): + conversation = self.container_client.read_item(item=conversation_id, partition_key=user_id) + if conversation: + print("Item exists") + resp = self.container_client.delete_item(item=conversation_id, partition_key=user_id) + return resp + else: + print("Item doesn't exist") + return True + + + def delete_messages(self, conversation_id, user_id): + ## get a list of all the messages in the conversation + messages = self.get_messages(user_id, conversation_id) + response_list = [] + if messages: + for message in messages: + resp = self.container_client.delete_item(item=message['id'], partition_key=user_id) + response_list.append(resp) + return response_list + + + def get_conversations(self, user_id, sort_order = 'DESC'): + query = f"SELECT * FROM c where c.userId = '{user_id}' and c.type='conversation' order by c.updatedAt {sort_order}" + conversations = list(self.container_client.query_items(query=query, + enable_cross_partition_query =True)) + ## if no conversations are found, return None + if len(conversations) == 0: + return None + else: + return conversations + + def get_conversation(self, user_id, conversation_id): + query = f"SELECT * FROM c where c.id = '{conversation_id}' and c.type='conversation' and c.userId = '{user_id}'" + conversation = list(self.container_client.query_items(query=query, + enable_cross_partition_query =True)) + ## if no conversations are found, return None + if len(conversation) == 0: + return None + else: + return conversation[0] + + def create_message(self, conversation_id, user_id, input_message: dict): + message = { + 'id': str(uuid.uuid4()), + 'type': 'message', + 'userId' : user_id, + 'createdAt': datetime.utcnow().isoformat(), + 'updatedAt': datetime.utcnow().isoformat(), + 'conversationId' : conversation_id, + 'role': input_message['role'], + 'content': input_message['content'] + } + print(self.credential) + + resp = self.container_client.upsert_item(message) + if resp: + ## update the parent conversations's updatedAt field with the current message's createdAt datetime value + conversation = self.get_conversation(user_id, conversation_id) + conversation['updatedAt'] = message['createdAt'] + self.upsert_conversation(conversation) + return resp + else: + return False + + + + def get_messages(self, user_id, conversation_id): + query = f"SELECT * FROM c WHERE c.conversationId = '{conversation_id}' AND c.type='message' AND c.userId = '{user_id}' ORDER BY c.timestamp ASC" + messages = list(self.container_client.query_items(query=query, + enable_cross_partition_query =True)) + ## if no messages are found, return false + if len(messages) == 0: + return None + else: + return messages + diff --git a/app/backend/requirements.txt b/app/backend/requirements.txt index 49508fa697..a99d50eb5a 100644 --- a/app/backend/requirements.txt +++ b/app/backend/requirements.txt @@ -1,6 +1,7 @@ azure-identity==1.13.0b3 Flask==2.2.2 langchain==0.0.78 -openai==0.26.4 +openai==0.27.5 azure-search-documents==11.4.0b3 azure-storage-blob==12.14.1 +azure-cosmos==4.3.1 \ No newline at end of file diff --git a/app/debug.ps1 b/app/debug.ps1 new file mode 100644 index 0000000000..5dc814d858 --- /dev/null +++ b/app/debug.ps1 @@ -0,0 +1,55 @@ +Write-Host "" +Write-Host "Loading azd .env file from current environment" +Write-Host "" + +foreach ($line in (& azd env get-values)) { + if ($line -match "([^=]+)=(.*)") { + $key = $matches[1] + $value = $matches[2] -replace '^"|"$' + Set-Item -Path "env:\$key" -Value $value + } +} + +if ($LASTEXITCODE -ne 0) { + Write-Host "Failed to load environment variables from azd environment" + exit $LASTEXITCODE +} + + +Write-Host 'Creating python virtual environment "backend/backend_env"' +$pythonCmd = Get-Command python -ErrorAction SilentlyContinue +if (-not $pythonCmd) { + # fallback to python3 if python not found + $pythonCmd = Get-Command python3 -ErrorAction SilentlyContinue +} +Start-Process -FilePath ($pythonCmd).Source -ArgumentList "-m venv ./backend/backend_env" -Wait -NoNewWindow + +Write-Host "" +Write-Host "Restoring backend python packages" +Write-Host "" + +Set-Location backend +$venvPythonPath = "./backend_env/scripts/python.exe" +if (Test-Path -Path "/usr") { + # fallback to Linux venv path + $venvPythonPath = "./backend_env/bin/python" +} + +Start-Process -FilePath $venvPythonPath -ArgumentList "-m pip install -r requirements.txt" -Wait -NoNewWindow +if ($LASTEXITCODE -ne 0) { + Write-Host "Failed to restore backend python packages" + exit $LASTEXITCODE +} + + +Write-Host "" +Write-Host "Starting backend" +Write-Host "" +Set-Location ../backend + +Start-Process -FilePath $venvPythonPath -ArgumentList "-m flask --app ./app.py --debug run" -Wait -NoNewWindow + +if ($LASTEXITCODE -ne 0) { + Write-Host "Failed to start backend" + exit $LASTEXITCODE +} diff --git a/app/debug.sh b/app/debug.sh new file mode 100755 index 0000000000..35bc7b45a9 --- /dev/null +++ b/app/debug.sh @@ -0,0 +1,43 @@ +#!/bin/sh + +echo "" +echo "Loading azd .env file from current environment" +echo "" + +while IFS='=' read -r key value; do + value=$(echo "$value" | sed 's/^"//' | sed 's/"$//') + export "$key=$value" +done <=4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/browserslist": { "version": "4.21.5", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", @@ -1256,6 +1272,17 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -1284,6 +1311,14 @@ } } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dompurify": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.1.tgz", @@ -1350,6 +1385,38 @@ "node": ">=0.8.0" } }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -1482,6 +1549,25 @@ "node": ">=12" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -1557,6 +1643,11 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -1828,1208 +1919,5 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true } - }, - "dependencies": { - "@ampproject/remapping": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", - "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", - "dev": true, - "requires": { - "@jridgewell/gen-mapping": "^0.1.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", - "dev": true, - "requires": { - "@babel/highlight": "^7.18.6" - } - }, - "@babel/compat-data": { - "version": "7.20.14", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.14.tgz", - "integrity": "sha512-0YpKHD6ImkWMEINCyDAD0HLLUH/lPCefG8ld9it8DJB2wnApraKuhgYTvTY1z7UFIfBTGy5LwncZ+5HWWGbhFw==", - "dev": true - }, - "@babel/core": { - "version": "7.20.12", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz", - "integrity": "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==", - "dev": true, - "requires": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.7", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-module-transforms": "^7.20.11", - "@babel/helpers": "^7.20.7", - "@babel/parser": "^7.20.7", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.20.12", - "@babel/types": "^7.20.7", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.2", - "semver": "^6.3.0" - } - }, - "@babel/generator": { - "version": "7.20.14", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.14.tgz", - "integrity": "sha512-AEmuXHdcD3A52HHXxaTmYlb8q/xMEhoRP67B3T4Oq7lbmSoqroMZzjnGj3+i1io3pdnF8iBYVu4Ilj+c4hBxYg==", - "dev": true, - "requires": { - "@babel/types": "^7.20.7", - "@jridgewell/gen-mapping": "^0.3.2", - "jsesc": "^2.5.1" - }, - "dependencies": { - "@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "dev": true, - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - } - } - } - }, - "@babel/helper-compilation-targets": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz", - "integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.20.5", - "@babel/helper-validator-option": "^7.18.6", - "browserslist": "^4.21.3", - "lru-cache": "^5.1.1", - "semver": "^6.3.0" - } - }, - "@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", - "dev": true - }, - "@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", - "dev": true, - "requires": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", - "dev": true, - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-module-imports": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", - "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", - "dev": true, - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-module-transforms": { - "version": "7.20.11", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz", - "integrity": "sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.20.2", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.19.1", - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.20.10", - "@babel/types": "^7.20.7" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", - "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", - "dev": true - }, - "@babel/helper-simple-access": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", - "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", - "dev": true, - "requires": { - "@babel/types": "^7.20.2" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", - "dev": true, - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", - "dev": true - }, - "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", - "dev": true - }, - "@babel/helper-validator-option": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", - "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", - "dev": true - }, - "@babel/helpers": { - "version": "7.20.13", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.13.tgz", - "integrity": "sha512-nzJ0DWCL3gB5RCXbUO3KIMMsBY2Eqbx8mBpKGE/02PgyRQFcPQLbkQ1vyy596mZLaP+dAfD+R4ckASzNVmW3jg==", - "dev": true, - "requires": { - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.20.13", - "@babel/types": "^7.20.7" - } - }, - "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.20.15", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.15.tgz", - "integrity": "sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg==", - "dev": true - }, - "@babel/plugin-transform-react-jsx-self": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.18.6.tgz", - "integrity": "sha512-A0LQGx4+4Jv7u/tWzoJF7alZwnBDQd6cGLh9P+Ttk4dpiL+J5p7NSNv/9tlEFFJDq3kjxOavWmbm6t0Gk+A3Ig==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-react-jsx-source": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.19.6.tgz", - "integrity": "sha512-RpAi004QyMNisst/pvSanoRdJ4q+jMCWyk9zdw/CyLB9j8RXEahodR6l2GyttDRyEVWZtbN+TpLiHJ3t34LbsQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.19.0" - } - }, - "@babel/runtime": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", - "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", - "requires": { - "regenerator-runtime": "^0.13.11" - } - }, - "@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" - } - }, - "@babel/traverse": { - "version": "7.20.13", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.13.tgz", - "integrity": "sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.7", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.20.13", - "@babel/types": "^7.20.7", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz", - "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==", - "dev": true, - "requires": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", - "to-fast-properties": "^2.0.0" - } - }, - "@emotion/hash": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", - "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" - }, - "@esbuild/android-arm": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.17.tgz", - "integrity": "sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz", - "integrity": "sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==", - "dev": true, - "optional": true - }, - "@esbuild/android-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.17.tgz", - "integrity": "sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-arm64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz", - "integrity": "sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz", - "integrity": "sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-arm64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz", - "integrity": "sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz", - "integrity": "sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz", - "integrity": "sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz", - "integrity": "sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ia32": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz", - "integrity": "sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-loong64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz", - "integrity": "sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-mips64el": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz", - "integrity": "sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ppc64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz", - "integrity": "sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==", - "dev": true, - "optional": true - }, - "@esbuild/linux-riscv64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz", - "integrity": "sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==", - "dev": true, - "optional": true - }, - "@esbuild/linux-s390x": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz", - "integrity": "sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==", - "dev": true, - "optional": true - }, - "@esbuild/linux-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz", - "integrity": "sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==", - "dev": true, - "optional": true - }, - "@esbuild/netbsd-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz", - "integrity": "sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==", - "dev": true, - "optional": true - }, - "@esbuild/openbsd-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz", - "integrity": "sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==", - "dev": true, - "optional": true - }, - "@esbuild/sunos-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz", - "integrity": "sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==", - "dev": true, - "optional": true - }, - "@esbuild/win32-arm64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz", - "integrity": "sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==", - "dev": true, - "optional": true - }, - "@esbuild/win32-ia32": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz", - "integrity": "sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==", - "dev": true, - "optional": true - }, - "@esbuild/win32-x64": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz", - "integrity": "sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==", - "dev": true, - "optional": true - }, - "@fluentui/date-time-utilities": { - "version": "8.5.5", - "resolved": "https://registry.npmjs.org/@fluentui/date-time-utilities/-/date-time-utilities-8.5.5.tgz", - "integrity": "sha512-P/qfyMIF1aWPVaZvgAE0u166Rp1Rfpymv63/NKQT1o56cc5LzfWTzjD2Ey1GyA+tn6dCf7g1ZXTpKo5H+CuM4Q==", - "requires": { - "@fluentui/set-version": "^8.2.5", - "tslib": "^2.1.0" - } - }, - "@fluentui/dom-utilities": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@fluentui/dom-utilities/-/dom-utilities-2.2.5.tgz", - "integrity": "sha512-VGCtAmPU/3uj/QV4Kx7gO/H2vNrhNSB346sE7xM+bBtxj+hf/owaGTvN6/tuZ8HXQu8tjTf8+ubQ3d7D7DUIjA==", - "requires": { - "@fluentui/set-version": "^8.2.5", - "tslib": "^2.1.0" - } - }, - "@fluentui/font-icons-mdl2": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/@fluentui/font-icons-mdl2/-/font-icons-mdl2-8.5.8.tgz", - "integrity": "sha512-HfFEie2hfci+diIFfSiu0CdNZMUCZjvAi0YsY2dVJHb9JTaFavfdVO1XHmMpn0HB+FVpp0tp5eMscPp5qblu1g==", - "requires": { - "@fluentui/set-version": "^8.2.5", - "@fluentui/style-utilities": "^8.9.1", - "@fluentui/utilities": "^8.13.6", - "tslib": "^2.1.0" - } - }, - "@fluentui/foundation-legacy": { - "version": "8.2.28", - "resolved": "https://registry.npmjs.org/@fluentui/foundation-legacy/-/foundation-legacy-8.2.28.tgz", - "integrity": "sha512-NmdbBtWroU/ND7UsaedhdIOKXXFpxmA5I+Is1DjQlAPdvuIwSAkHw+iX73cemcqrVnxPZB84iIvWfNzwquBYew==", - "requires": { - "@fluentui/merge-styles": "^8.5.6", - "@fluentui/set-version": "^8.2.5", - "@fluentui/style-utilities": "^8.9.1", - "@fluentui/utilities": "^8.13.6", - "tslib": "^2.1.0" - } - }, - "@fluentui/keyboard-key": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/@fluentui/keyboard-key/-/keyboard-key-0.4.5.tgz", - "integrity": "sha512-c+B+mdEgj0B6fhYIjznesGi8Al1rTpdFNudpNmFoVjlhCle5qj5RBtM4WaT8XygdzAVQq7oHSXom0vd32+zAZg==", - "requires": { - "tslib": "^2.1.0" - } - }, - "@fluentui/merge-styles": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/@fluentui/merge-styles/-/merge-styles-8.5.6.tgz", - "integrity": "sha512-i9Wy+7V+lKfX+UWRTrrK+3xm4aa8jl9tK2/7Ku696yWJ5v3D6xjRcMevfxUZDrZ3xS4/GRFfWKPHkAjzz/BQoQ==", - "requires": { - "@fluentui/set-version": "^8.2.5", - "tslib": "^2.1.0" - } - }, - "@fluentui/react": { - "version": "8.105.4", - "resolved": "https://registry.npmjs.org/@fluentui/react/-/react-8.105.4.tgz", - "integrity": "sha512-9W40Mdpywv1OmcxDCeP9vaMatdnAYUdX9RKM76QxjQeMGw0PafBgvgnBRhLhhXNacdRs1ByokM6LJ7svb6z4ow==", - "requires": { - "@fluentui/date-time-utilities": "^8.5.5", - "@fluentui/font-icons-mdl2": "^8.5.8", - "@fluentui/foundation-legacy": "^8.2.28", - "@fluentui/merge-styles": "^8.5.6", - "@fluentui/react-focus": "^8.8.14", - "@fluentui/react-hooks": "^8.6.16", - "@fluentui/react-portal-compat-context": "^9.0.4", - "@fluentui/react-window-provider": "^2.2.6", - "@fluentui/set-version": "^8.2.5", - "@fluentui/style-utilities": "^8.9.1", - "@fluentui/theme": "^2.6.22", - "@fluentui/utilities": "^8.13.6", - "@microsoft/load-themed-styles": "^1.10.26", - "tslib": "^2.1.0" - } - }, - "@fluentui/react-focus": { - "version": "8.8.14", - "resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-8.8.14.tgz", - "integrity": "sha512-lOhn00rSsd6gR4Z+RM1YyOZ4QWFvoF8s9Y3YeBh4rrYN5NL9ntMqKIcye6dUXx/P595kwXeKOQgUdH2LalJB4Q==", - "requires": { - "@fluentui/keyboard-key": "^0.4.5", - "@fluentui/merge-styles": "^8.5.6", - "@fluentui/set-version": "^8.2.5", - "@fluentui/style-utilities": "^8.9.1", - "@fluentui/utilities": "^8.13.6", - "tslib": "^2.1.0" - } - }, - "@fluentui/react-hooks": { - "version": "8.6.16", - "resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.6.16.tgz", - "integrity": "sha512-yWEcF7O9ZUn63TMORVBrovRr7NfNAYJ49SvhtoAdEjGIP6Tm3SgcwB0Gyo3ekYubX+XzmKRYfbu6qUFj6o8tGA==", - "requires": { - "@fluentui/react-window-provider": "^2.2.6", - "@fluentui/set-version": "^8.2.5", - "@fluentui/utilities": "^8.13.6", - "tslib": "^2.1.0" - } - }, - "@fluentui/react-icons": { - "version": "2.0.195", - "resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-2.0.195.tgz", - "integrity": "sha512-/jeDtD1U6hM+Kip9Q0aT7iZLAoRtAGXErnXmQrcUDHDWcbFXeNPg4g357CLXAaprqU9UusWO9DdsqrsBhjdTbQ==", - "requires": { - "@griffel/react": "^1.0.0", - "tslib": "^2.1.0" - } - }, - "@fluentui/react-portal-compat-context": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/@fluentui/react-portal-compat-context/-/react-portal-compat-context-9.0.4.tgz", - "integrity": "sha512-qw2lmkxZ2TmgC0pB2dvFyrzVffxBdpCx1BdWRaF+MRGUlTxRtqfybSx3Edsqa6NMewc3J0ThLMFdVFBQ5Yafqw==", - "requires": { - "tslib": "^2.1.0" - } - }, - "@fluentui/react-window-provider": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-2.2.6.tgz", - "integrity": "sha512-bcQM5mdi4ugVb30GNtde8sP173F+l9p7uQfgK/I8O07EfKHUHZeY4wj5arD53s1cUIQI0kxWJ5RD7upZNRQeQA==", - "requires": { - "@fluentui/set-version": "^8.2.5", - "tslib": "^2.1.0" - } - }, - "@fluentui/set-version": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/@fluentui/set-version/-/set-version-8.2.5.tgz", - "integrity": "sha512-DwJq9wIXLc8WkeJ/lqYM4Sv+R0Ccb6cy3cY1Bqaa5POsroVKIfL6W+njvAMOj3LO3+DaXo2aDeiUnnw70M8xIw==", - "requires": { - "tslib": "^2.1.0" - } - }, - "@fluentui/style-utilities": { - "version": "8.9.1", - "resolved": "https://registry.npmjs.org/@fluentui/style-utilities/-/style-utilities-8.9.1.tgz", - "integrity": "sha512-5PQd52UxvRSlOeBaNHRvKoicPAIgd/O43mgSj5T1OmJWRUkm5/8mcc5goHMvvHwB9i7HRuJPw21sQSefPira7g==", - "requires": { - "@fluentui/merge-styles": "^8.5.6", - "@fluentui/set-version": "^8.2.5", - "@fluentui/theme": "^2.6.22", - "@fluentui/utilities": "^8.13.6", - "@microsoft/load-themed-styles": "^1.10.26", - "tslib": "^2.1.0" - } - }, - "@fluentui/theme": { - "version": "2.6.22", - "resolved": "https://registry.npmjs.org/@fluentui/theme/-/theme-2.6.22.tgz", - "integrity": "sha512-Pw8WBGeASqDHR7EliJUL26x07pASFnU5QsRBSBg6ahUxGVXRuOtROhQ3jIKXldK8/HnAIzVEPZqeTwM0yWGBVA==", - "requires": { - "@fluentui/merge-styles": "^8.5.6", - "@fluentui/set-version": "^8.2.5", - "@fluentui/utilities": "^8.13.6", - "tslib": "^2.1.0" - } - }, - "@fluentui/utilities": { - "version": "8.13.6", - "resolved": "https://registry.npmjs.org/@fluentui/utilities/-/utilities-8.13.6.tgz", - "integrity": "sha512-szgbLmg919h9wuQi/QVgZ5oa5qtOtc1VgyR/eMPzMW/pJHU9jc7E0L++eMYa1oaHpdsDrQ4L3wAIo6Yuk1Jczw==", - "requires": { - "@fluentui/dom-utilities": "^2.2.5", - "@fluentui/merge-styles": "^8.5.6", - "@fluentui/set-version": "^8.2.5", - "tslib": "^2.1.0" - } - }, - "@griffel/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@griffel/core/-/core-1.10.0.tgz", - "integrity": "sha512-9yIBFswd6pcxtYsDVngplCHTyZ++cIk0htBOBVjxBKEoTkEmTgSvbIB2kKMiO3OJLrjzwoi9r+s3owugzIZe1w==", - "requires": { - "@emotion/hash": "^0.9.0", - "csstype": "^3.0.10", - "rtl-css-js": "^1.16.1", - "stylis": "^4.0.13", - "tslib": "^2.1.0" - } - }, - "@griffel/react": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@griffel/react/-/react-1.5.5.tgz", - "integrity": "sha512-MpAU0NEpBzNRWUGSlhgz3jzZRC+HbRI+P2lQIzyxoMFgzEB4QFtDnRDBwPLfi/Eoq55NlVmsxn2Pr3jJ/bjhRw==", - "requires": { - "@griffel/core": "^1.10.0", - "tslib": "^2.1.0" - } - }, - "@jridgewell/gen-mapping": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", - "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", - "dev": true, - "requires": { - "@jridgewell/set-array": "^1.0.0", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true - }, - "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, - "@microsoft/load-themed-styles": { - "version": "1.10.295", - "resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.10.295.tgz", - "integrity": "sha512-W+IzEBw8a6LOOfRJM02dTT7BDZijxm+Z7lhtOAz1+y9vQm1Kdz9jlAO+qCEKsfxtUOmKilW8DIRqFw2aUgKeGg==" - }, - "@react-spring/animated": { - "version": "9.7.1", - "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.1.tgz", - "integrity": "sha512-EX5KAD9y7sD43TnLeTNG1MgUVpuRO1YaSJRPawHNRgUWYfILge3s85anny4S4eTJGpdp5OoFV2kx9fsfeo0qsw==", - "requires": { - "@react-spring/shared": "~9.7.1", - "@react-spring/types": "~9.7.1" - } - }, - "@react-spring/core": { - "version": "9.7.1", - "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.1.tgz", - "integrity": "sha512-8K9/FaRn5VvMa24mbwYxwkALnAAyMRdmQXrARZLcBW2vxLJ6uw9Cy3d06Z8M12kEqF2bDlccaCSDsn2bSz+Q4A==", - "requires": { - "@react-spring/animated": "~9.7.1", - "@react-spring/rafz": "~9.7.1", - "@react-spring/shared": "~9.7.1", - "@react-spring/types": "~9.7.1" - } - }, - "@react-spring/rafz": { - "version": "9.7.1", - "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.1.tgz", - "integrity": "sha512-JSsrRfbEJvuE3w/uvU3mCTuWwpQcBXkwoW14lBgzK9XJhuxmscGo59AgJUpFkGOiGAVXFBGB+nEXtSinFsopgw==" - }, - "@react-spring/shared": { - "version": "9.7.1", - "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.1.tgz", - "integrity": "sha512-R2kZ+VOO6IBeIAYTIA3C1XZ0ZVg/dDP5FKtWaY8k5akMer9iqf5H9BU0jyt3Qtxn0qQY7whQdf6MTcWtKeaawg==", - "requires": { - "@react-spring/rafz": "~9.7.1", - "@react-spring/types": "~9.7.1" - } - }, - "@react-spring/types": { - "version": "9.7.1", - "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.1.tgz", - "integrity": "sha512-yBcyfKUeZv9wf/ZFrQszvhSPuDx6Py6yMJzpMnS+zxcZmhXPeOCKZSHwqrUz1WxvuRckUhlgb7eNI/x5e1e8CA==" - }, - "@react-spring/web": { - "version": "9.7.1", - "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.1.tgz", - "integrity": "sha512-6uUE5MyKqdrJnIJqlDN/AXf3i8PjOQzUuT26nkpsYxUGOk7c+vZVPcfrExLSoKzTb9kF0i66DcqzO5fXz/Z1AA==", - "requires": { - "@react-spring/animated": "~9.7.1", - "@react-spring/core": "~9.7.1", - "@react-spring/shared": "~9.7.1", - "@react-spring/types": "~9.7.1" - } - }, - "@remix-run/router": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.3.2.tgz", - "integrity": "sha512-t54ONhl/h75X94SWsHGQ4G/ZrCEguKSRQr7DrjTciJXW0YU1QhlwYeycvK5JgkzlxmvrK7wq1NB/PLtHxoiDcA==" - }, - "@types/dompurify": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.4.0.tgz", - "integrity": "sha512-IDBwO5IZhrKvHFUl+clZxgf3hn2b/lU6H1KaBShPkQyGJUQ0xwebezIPSuiyGwfz1UzJWQl4M7BDxtHtCCPlTg==", - "dev": true, - "requires": { - "@types/trusted-types": "*" - } - }, - "@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" - }, - "@types/react": { - "version": "18.0.27", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.27.tgz", - "integrity": "sha512-3vtRKHgVxu3Jp9t718R9BuzoD4NcQ8YJ5XRzsSKxNDiDonD2MXIT1TmSkenxuCycZJoQT5d2vE8LwWJxBC1gmA==", - "requires": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "@types/react-dom": { - "version": "18.0.10", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.10.tgz", - "integrity": "sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==", - "requires": { - "@types/react": "*" - } - }, - "@types/scheduler": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" - }, - "@types/trusted-types": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz", - "integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==", - "dev": true - }, - "@vitejs/plugin-react": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-3.1.0.tgz", - "integrity": "sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==", - "dev": true, - "requires": { - "@babel/core": "^7.20.12", - "@babel/plugin-transform-react-jsx-self": "^7.18.6", - "@babel/plugin-transform-react-jsx-source": "^7.19.6", - "magic-string": "^0.27.0", - "react-refresh": "^0.14.0" - } - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "browserslist": { - "version": "4.21.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", - "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001449", - "electron-to-chromium": "^1.4.284", - "node-releases": "^2.0.8", - "update-browserslist-db": "^1.0.10" - } - }, - "caniuse-lite": { - "version": "1.0.30001450", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001450.tgz", - "integrity": "sha512-qMBmvmQmFXaSxexkjjfMvD5rnDL0+m+dUMZKoDYsGG8iZN29RuYh9eRoMvKsT6uMAWlyUUGDEQGJJYjzCIO9ew==", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true - }, - "csstype": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "dompurify": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.1.tgz", - "integrity": "sha512-60tsgvPKwItxZZdfLmamp0MTcecCta3avOhsLgPZ0qcWt96OasFfhkeIRbJ6br5i0fQawT1/RBGB5L58/Jpwuw==" - }, - "electron-to-chromium": { - "version": "1.4.286", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.286.tgz", - "integrity": "sha512-Vp3CVhmYpgf4iXNKAucoQUDcCrBQX3XLBtwgFqP9BUXuucgvAV9zWp1kYU7LL9j4++s9O+12cb3wMtN4SJy6UQ==", - "dev": true - }, - "esbuild": { - "version": "0.16.17", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.17.tgz", - "integrity": "sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==", - "dev": true, - "requires": { - "@esbuild/android-arm": "0.16.17", - "@esbuild/android-arm64": "0.16.17", - "@esbuild/android-x64": "0.16.17", - "@esbuild/darwin-arm64": "0.16.17", - "@esbuild/darwin-x64": "0.16.17", - "@esbuild/freebsd-arm64": "0.16.17", - "@esbuild/freebsd-x64": "0.16.17", - "@esbuild/linux-arm": "0.16.17", - "@esbuild/linux-arm64": "0.16.17", - "@esbuild/linux-ia32": "0.16.17", - "@esbuild/linux-loong64": "0.16.17", - "@esbuild/linux-mips64el": "0.16.17", - "@esbuild/linux-ppc64": "0.16.17", - "@esbuild/linux-riscv64": "0.16.17", - "@esbuild/linux-s390x": "0.16.17", - "@esbuild/linux-x64": "0.16.17", - "@esbuild/netbsd-x64": "0.16.17", - "@esbuild/openbsd-x64": "0.16.17", - "@esbuild/sunos-x64": "0.16.17", - "@esbuild/win32-arm64": "0.16.17", - "@esbuild/win32-ia32": "0.16.17", - "@esbuild/win32-x64": "0.16.17" - } - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true - }, - "json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "requires": { - "yallist": "^3.0.2" - } - }, - "magic-string": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", - "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", - "dev": true, - "requires": { - "@jridgewell/sourcemap-codec": "^1.4.13" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", - "dev": true - }, - "node-releases": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", - "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", - "dev": true - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "postcss": { - "version": "8.4.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", - "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", - "dev": true, - "requires": { - "nanoid": "^3.3.4", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, - "prettier": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.3.tgz", - "integrity": "sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw==", - "dev": true - }, - "react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "requires": { - "loose-envify": "^1.1.0" - } - }, - "react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "requires": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - } - }, - "react-refresh": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", - "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", - "dev": true - }, - "react-router": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.8.1.tgz", - "integrity": "sha512-Jgi8BzAJQ8MkPt8ipXnR73rnD7EmZ0HFFb7jdQU24TynGW1Ooqin2KVDN9voSC+7xhqbbCd2cjGUepb6RObnyg==", - "requires": { - "@remix-run/router": "1.3.2" - } - }, - "react-router-dom": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.8.1.tgz", - "integrity": "sha512-67EXNfkQgf34P7+PSb6VlBuaacGhkKn3kpE51+P6zYSG2kiRoumXEL6e27zTa9+PGF2MNXbgIUHTVlleLbIcHQ==", - "requires": { - "@remix-run/router": "1.3.2", - "react-router": "6.8.1" - } - }, - "regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" - }, - "resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, - "requires": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "rollup": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.14.0.tgz", - "integrity": "sha512-o23sdgCLcLSe3zIplT9nQ1+r97okuaiR+vmAPZPTDYB7/f3tgWIYNyiQveMsZwshBT0is4eGax/HH83Q7CG+/Q==", - "dev": true, - "requires": { - "fsevents": "~2.3.2" - } - }, - "rtl-css-js": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", - "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", - "requires": { - "@babel/runtime": "^7.1.2" - } - }, - "scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "requires": { - "loose-envify": "^1.1.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true - }, - "stylis": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz", - "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true - }, - "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" - }, - "typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true - }, - "update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", - "dev": true, - "requires": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - } - }, - "vite": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.1.1.tgz", - "integrity": "sha512-LM9WWea8vsxhr782r9ntg+bhSFS06FJgCvvB0+8hf8UWtvaiDagKYWXndjfX6kGl74keHJUcpzrQliDXZlF5yg==", - "dev": true, - "requires": { - "esbuild": "^0.16.14", - "fsevents": "~2.3.2", - "postcss": "^8.4.21", - "resolve": "^1.22.1", - "rollup": "^3.10.0" - } - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - } } } diff --git a/app/frontend/package.json b/app/frontend/package.json index e8d39c9c0b..ed8d35868f 100644 --- a/app/frontend/package.json +++ b/app/frontend/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", + "dev": "tsc && vite", "build": "tsc && vite build", "watch": "tsc && vite build --watch" }, @@ -15,7 +15,8 @@ "dompurify": "^3.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.8.1" + "react-router-dom": "^6.8.1", + "axios": "1.4.0" }, "devDependencies": { "@types/dompurify": "^2.4.0", diff --git a/app/frontend/src/api/api.ts b/app/frontend/src/api/api.ts index 12da6d9f12..f5d8650c10 100644 --- a/app/frontend/src/api/api.ts +++ b/app/frontend/src/api/api.ts @@ -1,4 +1,4 @@ -import { AskRequest, AskResponse, ChatRequest } from "./models"; +import { AskRequest, AskResponse, ChatRequest, ConversationRequest, ConversationResponse, ConversationListResponse } from "./models"; export async function askApi(options: AskRequest): Promise { const response = await fetch("/ask", { @@ -30,6 +30,7 @@ export async function askApi(options: AskRequest): Promise { return parsedResponse; } +// BDL: this is the original chatApi that I'm copying with the chatConversationAPI export async function chatApi(options: ChatRequest): Promise { const response = await fetch("/chat", { method: "POST", @@ -61,6 +62,88 @@ export async function chatApi(options: ChatRequest): Promise { return parsedResponse; } +// BDL: this is the chatConversationAPI that I'm adding +export async function chatConversationApi(options: ChatRequest): Promise { + console.log("chatConversationApi: options.history: ", options.history); + + const response = await fetch("/conversation/add", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + history: options.history, + approach: options.approach, + overrides: { + semantic_ranker: options.overrides?.semanticRanker, + semantic_captions: options.overrides?.semanticCaptions, + top: options.overrides?.top, + temperature: options.overrides?.temperature, + prompt_template: options.overrides?.promptTemplate, + prompt_template_prefix: options.overrides?.promptTemplatePrefix, + prompt_template_suffix: options.overrides?.promptTemplateSuffix, + exclude_category: options.overrides?.excludeCategory, + suggest_followup_questions: options.overrides?.suggestFollowupQuestions + }, + user: "user", // TODO: add user ID parameter ## BDL: I think we just depend on the backend to capture the authenticated user for now. + conversation_id: options.conversation_id // TODO: add conversation ID + }) + }); + + const parsedResponse: AskResponse = await response.json(); + if (response.status > 299 || !response.ok) { + throw Error(parsedResponse.error || "Unknown error"); + } + + return parsedResponse; +} +//BDL proposed updated conversationApi... +// TODO: need to figure out how to better return types as an enum or switch statement or something. +export async function conversationApi(options: any): Promise { + console.log("conversationApi: options", options); + // parse the route depending on the task + + let route: string; + let body; + + switch (options.route) { + case "/add": + route = `${options.baseroute}/add`; + break; + case "/read": + route = `${options.baseroute}/read`; + body = JSON.stringify({ conversation_id: options.conversation_id }); + break; + case "/list": + route = `${options.baseroute}/list`; + body = JSON.stringify({}); + break; + case "/delete": + route = `${options.baseroute}/delete`; + body = JSON.stringify({ conversation_id: options.conversation_id }); + break; + + default: + throw Error("Invalid route"); + } + + const response = await fetch(route, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: body + }); + + const parsedResponse: any = await response.json(); + + if (response.status > 299 || !response.ok) { + throw Error(parsedResponse.error || "Unknown error"); + } + + return parsedResponse; +} + export function getCitationFilePath(citation: string): string { return `/content/${citation}`; } diff --git a/app/frontend/src/api/models.ts b/app/frontend/src/api/models.ts index bec3dc28bc..0882decf0c 100644 --- a/app/frontend/src/api/models.ts +++ b/app/frontend/src/api/models.ts @@ -1,7 +1,8 @@ export const enum Approaches { RetrieveThenRead = "rtr", ReadRetrieveRead = "rrr", - ReadDecomposeAsk = "rda" + ReadDecomposeAsk = "rda", + ChatConversation = "chatconversation" } export type AskRequestOverrides = { @@ -26,6 +27,7 @@ export type AskResponse = { answer: string; thoughts: string | null; data_points: string[]; + conversation_id: string; error?: string; }; @@ -38,4 +40,42 @@ export type ChatRequest = { history: ChatTurn[]; approach: Approaches; overrides?: AskRequestOverrides; + conversation_id?: string; }; + +//BDL: for interacting with conversations +export type ChatCompletionsFormat = [ + { + role: "system" | "user" | "assistant"; + content: string; + } +]; +export type BotFrontendFormat = [{ user: string; bot: string }]; + +export type ConversationRequest = { + baseroute: "/conversation"; + route: "/add" | "/read" | "/delete" | "/update" | "/list"; + conversation_id?: string | null; + approach?: Approaches; +}; + +export type ConversationResponse = { + conversation_id: string; + messages: BotFrontendFormat; + error?: string; +}; + +export type ConversationListResponse = { + _attachments: string; + _etag: string; + _rid: string; + _self: string; + _ts: Number; + createdAt: string; + id: string; + summary: string; + title: string; + type: "conversation"; + updatedAt: string; + userId: string; +}[]; diff --git a/app/frontend/src/auth/authUtils.tsx b/app/frontend/src/auth/authUtils.tsx new file mode 100644 index 0000000000..e771b66ed9 --- /dev/null +++ b/app/frontend/src/auth/authUtils.tsx @@ -0,0 +1,27 @@ +import axios from "axios"; +// a function that will try to get the user's details from the app service authentication +// if the call to the /.auth/me endpoint fails, assume we are in development and return a dummy user +// from the user_details config file. +const get_auth_user_details = () => { + var user_object = ""; + + try { + // get the user details from the /.auth/me endpoint + let user_object = axios.get("/.auth/me").then(r => { + return r.data[0]; + }); + // return the first user in the response + } catch (error) { + //if it's a 404 error, we are in development and return a dummy user + console.log("Error getting user details from /.auth/me endpoint"); + console.log(error); + console.log("Are we in development?"); + let user_object = import("./dummy_user.json"); + } + + console.log("User details from app service authentication"); + console.log(user_object); + return user_object; +}; + +// get_auth_user_details(); diff --git a/app/frontend/src/auth/dummy_user.json b/app/frontend/src/auth/dummy_user.json new file mode 100644 index 0000000000..5e6cd7edc9 --- /dev/null +++ b/app/frontend/src/auth/dummy_user.json @@ -0,0 +1,63 @@ +{ + "id_token": "your_base_encoded_id_token", + "provider_name": "aad", + "user_claims": [ + { + "typ": "aud", + "val": "your_app_service_principal_guid" + }, + { + "typ": "iss", + "val": "https://login.microsoftonline.com/your_tenant_id/v2.0" + }, + { + "typ": "iat", + "val": "1684338703" + }, + { + "typ": "nbf", + "val": "1684338703" + }, + { + "typ": "exp", + "val": "1684342603" + }, + { + "typ": "aio", + "val": "ignore_me" + }, + { + "typ": "cc", + "val": "just_another_token_of_some_kind" + }, + { + "typ": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", + "val": "testusername@constoso.com" + }, + { + "typ": "name", + "val": "Testy McTesterson" + }, + { + "typ": "http://schemas.microsoft.com/identity/claims/objectidentifier", + "val": "00000000-0000-0000-0000-000000000000" + }, + { + "typ": "preferred_username", + "val": "testusername@constoso.com" + }, + { + "typ": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", + "val": "unique_identifier_for_this_particular_app_and_user" + }, + { + "typ": "http://schemas.microsoft.com/identity/claims/tenantid", + "val": "your_tenant_id" + }, + { + "typ": "ver", + "val": "2.0" + } + ], + "user_id": "testusername@constoso.com" +} \ No newline at end of file diff --git a/app/frontend/src/components/ChatConversationExample/ChatConversationExample.module.css b/app/frontend/src/components/ChatConversationExample/ChatConversationExample.module.css new file mode 100644 index 0000000000..796f2172f5 --- /dev/null +++ b/app/frontend/src/components/ChatConversationExample/ChatConversationExample.module.css @@ -0,0 +1,39 @@ +.examplesNavList { + list-style: none; + padding-left: 0; + display: flex; + flex-wrap: wrap; + gap: 10px; + flex: 1; + justify-content: center; +} + +.example { + word-break: break-word; + background: #dbdbdb; + border-radius: 8px; + display: flex; + flex-direction: column; + padding: 20px; + margin-bottom: 5px; + cursor: pointer; +} + +.example:hover { + box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12); + outline: 2px solid rgba(115, 118, 225, 1); +} + +.exampleText { + margin: 0; + font-size: 22px; + width: 280px; + height: 100px; +} + +@media only screen and (max-height: 780px) { + .exampleText { + font-size: 20px; + height: 80px; + } +} diff --git a/app/frontend/src/components/ChatConversationExample/ChatConversationExample.tsx b/app/frontend/src/components/ChatConversationExample/ChatConversationExample.tsx new file mode 100644 index 0000000000..124128427a --- /dev/null +++ b/app/frontend/src/components/ChatConversationExample/ChatConversationExample.tsx @@ -0,0 +1,15 @@ +import styles from "./ChatConversationExample.module.css"; + +interface Props { + text: string; + value: string; + onClick: (value: string) => void; +} + +export const Example = ({ text, value, onClick }: Props) => { + return ( +
onClick(value)}> +

{text}

+
+ ); +}; diff --git a/app/frontend/src/components/ChatConversationExample/ChatConversationExampleList.tsx b/app/frontend/src/components/ChatConversationExample/ChatConversationExampleList.tsx new file mode 100644 index 0000000000..34c4e49b41 --- /dev/null +++ b/app/frontend/src/components/ChatConversationExample/ChatConversationExampleList.tsx @@ -0,0 +1,39 @@ +import { Example } from "./ChatConversationExample"; + +import styles from "./ChatConversationExample.module.css"; + +export type ExampleModel = { + text: string; + value: string; +}; + +const EXAMPLES: ExampleModel[] = [ + { + text: "Explain quantum computing in simple terms", + value: "Explain quantum computing in simple terms" + }, + { + text: "Can you write a haiku about working late on a friday night?", + value: "Can you write a haiku about working late on a friday night?" + }, + { + text: "How do I make an HTTP request in Python?", + value: "How do I make an HTTP request in Python?" + } +]; + +interface Props { + onExampleClicked: (value: string) => void; +} + +export const ChatConversationExampleList = ({ onExampleClicked }: Props) => { + return ( +
    + {EXAMPLES.map((x, i) => ( +
  • + +
  • + ))} +
+ ); +}; diff --git a/app/frontend/src/components/ChatConversationExample/index.tsx b/app/frontend/src/components/ChatConversationExample/index.tsx new file mode 100644 index 0000000000..65e6c08408 --- /dev/null +++ b/app/frontend/src/components/ChatConversationExample/index.tsx @@ -0,0 +1,2 @@ +export * from "./ChatConversationExample"; +export * from "./ChatConversationExampleList"; diff --git a/app/frontend/src/components/ClearChatButton/ClearChatButton.tsx b/app/frontend/src/components/ClearChatButton/ClearChatButton.tsx index ed017db88e..e03c7eeda3 100644 --- a/app/frontend/src/components/ClearChatButton/ClearChatButton.tsx +++ b/app/frontend/src/components/ClearChatButton/ClearChatButton.tsx @@ -1,5 +1,5 @@ import { Text } from "@fluentui/react"; -import { Delete24Regular } from "@fluentui/react-icons"; +import { FormNew24Regular } from "@fluentui/react-icons"; import styles from "./ClearChatButton.module.css"; @@ -12,8 +12,8 @@ interface Props { export const ClearChatButton = ({ className, disabled, onClick }: Props) => { return (
- - {"Clear chat"} + + {"New conversation"}
); }; diff --git a/app/frontend/src/components/ConversationList/Conversation.module.css b/app/frontend/src/components/ConversationList/Conversation.module.css new file mode 100644 index 0000000000..32b3c66c1b --- /dev/null +++ b/app/frontend/src/components/ConversationList/Conversation.module.css @@ -0,0 +1,52 @@ +.conversationNavList { + list-style: none; + padding-left: 0; + display: flex; + flex-wrap: wrap; + gap: 2px; + flex: 1; + justify-content: center; + padding-top: 10px; +} + +.conversation { + word-break: break-word; + background: #dbdbdb; + border-radius: 1px; + display: flex; + flex-direction: column; + padding: 2px; + margin-bottom: 1px; + cursor: pointer; + width: 300px; + height: 60px; +} + +.conversation:hover { + box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12); + outline: 2px solid rgba(115, 118, 225, 1); +} + +.conversationText { + margin: 0; + font-size: 12px; +} +.conversationHeader { + margin: 0; + font-size: 14px; + font-weight: bold; +} +.conversationWrapper { + padding-left: 10px; + padding-top: 10px; +} + +@media only screen and (max-height: 780px) { + .conversationText { + font-size: 12px; + } +} +.deleteButton { + padding-top: 10px; + margin-right: 7px; +} diff --git a/app/frontend/src/components/ConversationList/Conversation.tsx b/app/frontend/src/components/ConversationList/Conversation.tsx new file mode 100644 index 0000000000..312af67bbc --- /dev/null +++ b/app/frontend/src/components/ConversationList/Conversation.tsx @@ -0,0 +1,42 @@ +import { ConversationDeleteButton } from "./ConversationDeleteButton"; +import styles from "./Conversation.module.css"; +import { Stack, IStackProps, IStackTokens, Alignment } from "@fluentui/react"; + +interface Props { + conversation_id: string; + conversation_title: string; + userId: string; + updatedAt: string; + createdAt: string; + onClick: (value: string) => void; + onDeleteClick: (value: string) => void; +} + +export const Conversation = ({ conversation_id, conversation_title, createdAt, updatedAt, userId, onClick, onDeleteClick }: Props) => { + //check if the conversation title is undefined + if (conversation_title === "") { + conversation_title = "New Conversation"; + } + + const createdDate = new Date(Date.parse(updatedAt)); + const dateOptions = { + year: "numeric", + month: "short", + day: "numeric", + hour: "numeric", + minute: "numeric", + hour12: true + } as Intl.DateTimeFormatOptions; + + return ( +
onClick(conversation_id)}> + +
+

{conversation_title}

+

Last Updated: {createdDate.toLocaleString("en-US", dateOptions)}

+
+ +
+
+ ); +}; diff --git a/app/frontend/src/components/ConversationList/ConversationDeleteButton.module.css b/app/frontend/src/components/ConversationList/ConversationDeleteButton.module.css new file mode 100644 index 0000000000..fa2ede12fc --- /dev/null +++ b/app/frontend/src/components/ConversationList/ConversationDeleteButton.module.css @@ -0,0 +1,10 @@ +.button_icon { + padding-right: 5px; +} + +.container { + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; +} diff --git a/app/frontend/src/components/ConversationList/ConversationDeleteButton.tsx b/app/frontend/src/components/ConversationList/ConversationDeleteButton.tsx new file mode 100644 index 0000000000..29a9067158 --- /dev/null +++ b/app/frontend/src/components/ConversationList/ConversationDeleteButton.tsx @@ -0,0 +1,18 @@ +import { Delete20Regular } from "@fluentui/react-icons"; + +import styles from "./ConversationDeleteButton.module.css"; + +interface Props { + conversation_id: string; + className?: string; + onClick: (value: string) => void; +} + +export const ConversationDeleteButton = ({ conversation_id, className, onClick }: Props) => { + // todo: add accessble tags for delete button + return ( +
onClick(conversation_id)}> + +
+ ); +}; diff --git a/app/frontend/src/components/ConversationList/ConversationList.tsx b/app/frontend/src/components/ConversationList/ConversationList.tsx new file mode 100644 index 0000000000..5b0fe761f7 --- /dev/null +++ b/app/frontend/src/components/ConversationList/ConversationList.tsx @@ -0,0 +1,38 @@ +import { Conversation } from "./Conversation"; +import { ConversationListResponse } from "../../api"; +import { ConversationDeleteButton } from "./ConversationDeleteButton"; +import { Stack, IStackProps, IStackTokens, Alignment } from "@fluentui/react"; +import styles from "./Conversation.module.css"; + +interface Props { + listOfConversations: ConversationListResponse | null; + onConversationClicked: (value: string) => void; + onDeleteClick: (value: string) => void; +} + +export const ConversationList = ({ listOfConversations, onConversationClicked, onDeleteClick }: Props) => { + if (!listOfConversations) { + return null; + } else { + return ( +
+ + {listOfConversations.map(({ id, title, updatedAt, createdAt, userId }, index) => ( +
+ + {/* */} +
+ ))} +
+
+ ); + } +}; diff --git a/app/frontend/src/components/ConversationList/ConversationListButton.module.css b/app/frontend/src/components/ConversationList/ConversationListButton.module.css new file mode 100644 index 0000000000..fb533bfd84 --- /dev/null +++ b/app/frontend/src/components/ConversationList/ConversationListButton.module.css @@ -0,0 +1,6 @@ +.container { + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; +} diff --git a/app/frontend/src/components/ConversationList/ConversationListButton.tsx b/app/frontend/src/components/ConversationList/ConversationListButton.tsx new file mode 100644 index 0000000000..5ba5a633af --- /dev/null +++ b/app/frontend/src/components/ConversationList/ConversationListButton.tsx @@ -0,0 +1,18 @@ +import { Text } from "@fluentui/react"; +import { BookOpen24Regular } from "@fluentui/react-icons"; + +import styles from "./ConversationListButton.module.css"; + +interface Props { + className?: string; + onClick: () => void; +} + +export const ConversationListButton = ({ className, onClick }: Props) => { + return ( +
+ + {"Conversation List"} +
+ ); +}; diff --git a/app/frontend/src/components/ConversationList/ConversationListPanel.module.css b/app/frontend/src/components/ConversationList/ConversationListPanel.module.css new file mode 100644 index 0000000000..909ac03d44 --- /dev/null +++ b/app/frontend/src/components/ConversationList/ConversationListPanel.module.css @@ -0,0 +1,6 @@ +.thoughtProcess { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; + word-wrap: break-word; + padding-top: 12px; + padding-bottom: 12px; +} diff --git a/app/frontend/src/components/ConversationList/ConversationListPanel.tsx b/app/frontend/src/components/ConversationList/ConversationListPanel.tsx new file mode 100644 index 0000000000..35e6619c37 --- /dev/null +++ b/app/frontend/src/components/ConversationList/ConversationListPanel.tsx @@ -0,0 +1,57 @@ +import { Pivot, PivotItem } from "@fluentui/react"; +import DOMPurify from "dompurify"; + +import styles from "./ConversationListPanel.module.css"; + +import { SupportingContent } from "../SupportingContent"; +import { AskResponse } from "../../api"; +import { ConversationListPanelTabs } from "./ConversationListPanelTabs"; + +interface Props { + className: string; + activeTab: ConversationListPanelTabs; + onActiveTabChanged: (tab: ConversationListPanelTabs) => void; + activeCitation: string | undefined; + citationHeight: string; + answer: AskResponse; +} + +const pivotItemDisabledStyle = { disabled: true, style: { color: "grey" } }; + +export const ConversationListPanel = ({ answer, activeTab, activeCitation, citationHeight, className, onActiveTabChanged }: Props) => { + const isDisabledThoughtProcessTab: boolean = !answer.thoughts; + const isDisabledSupportingContentTab: boolean = !answer.data_points.length; + const isDisabledCitationTab: boolean = !activeCitation; + + const sanitizedThoughts = DOMPurify.sanitize(answer.thoughts!); + + return ( + pivotItem && onActiveTabChanged(pivotItem.props.itemKey! as ConversationListPanelTabs)} + > + +
+
+ + + + +