├── .deployment ├── .devcontainer └── devcontainer.json ├── .github └── workflows │ ├── docker_image_publish.yml │ ├── docker_image_publish_dev.yml │ └── enforce-dev-to-main.yml ├── .gitignore ├── LICENSE ├── README.md ├── RELEASE_NOTES.md ├── SECURITY.md ├── application ├── external_apps │ └── README.md └── single_app │ ├── .DS_Store │ ├── .dockerignore │ ├── Dockerfile │ ├── app.py │ ├── config.py │ ├── example.env │ ├── example_advance_edit_environment_variables.json │ ├── functions_authentication.py │ ├── functions_bing_search.py │ ├── functions_content.py │ ├── functions_documents.py │ ├── functions_group.py │ ├── functions_logging.py │ ├── functions_prompts.py │ ├── functions_search.py │ ├── functions_settings.py │ ├── requirements.txt │ ├── route_backend_chats.py │ ├── route_backend_conversations.py │ ├── route_backend_documents.py │ ├── route_backend_feedback.py │ ├── route_backend_group_documents.py │ ├── route_backend_group_prompts.py │ ├── route_backend_groups.py │ ├── route_backend_models.py │ ├── route_backend_prompts.py │ ├── route_backend_safety.py │ ├── route_backend_settings.py │ ├── route_backend_users.py │ ├── route_frontend_admin_settings.py │ ├── route_frontend_authentication.py │ ├── route_frontend_chats.py │ ├── route_frontend_conversations.py │ ├── route_frontend_feedback.py │ ├── route_frontend_group_workspaces.py │ ├── route_frontend_groups.py │ ├── route_frontend_profile.py │ ├── route_frontend_safety.py │ ├── route_frontend_workspace.py │ ├── static │ ├── .DS_Store │ ├── css │ │ ├── bootstrap-grid.css │ │ ├── bootstrap-grid.css.map │ │ ├── bootstrap-grid.min.css │ │ ├── bootstrap-grid.min.css.map │ │ ├── bootstrap-grid.rtl.css │ │ ├── bootstrap-grid.rtl.css.map │ │ ├── bootstrap-grid.rtl.min.css │ │ ├── bootstrap-grid.rtl.min.css.map │ │ ├── bootstrap-icons.css │ │ ├── bootstrap-icons.json │ │ ├── bootstrap-icons.min.css │ │ ├── bootstrap-icons.scss │ │ ├── bootstrap-reboot.css │ │ ├── bootstrap-reboot.css.map │ │ ├── bootstrap-reboot.min.css │ │ ├── bootstrap-reboot.min.css.map │ │ ├── bootstrap-reboot.rtl.css │ │ ├── bootstrap-reboot.rtl.css.map │ │ ├── bootstrap-reboot.rtl.min.css │ │ ├── bootstrap-reboot.rtl.min.css.map │ │ ├── bootstrap-utilities.css │ │ ├── bootstrap-utilities.css.map │ │ ├── bootstrap-utilities.min.css │ │ ├── bootstrap-utilities.min.css.map │ │ ├── bootstrap-utilities.rtl.css │ │ ├── bootstrap-utilities.rtl.css.map │ │ ├── bootstrap-utilities.rtl.min.css │ │ ├── bootstrap-utilities.rtl.min.css.map │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ ├── bootstrap.min.css.map │ │ ├── bootstrap.rtl.css │ │ ├── bootstrap.rtl.css.map │ │ ├── bootstrap.rtl.min.css │ │ ├── bootstrap.rtl.min.css.map │ │ ├── chats.css │ │ ├── datatables.css │ │ ├── datatables.min.css │ │ ├── jquery.dataTables.min.css │ │ ├── simplemde.css │ │ ├── simplemde.min.css │ │ └── styles.css │ ├── favicon.ico │ ├── fonts │ │ ├── bootstrap-icons.woff │ │ └── bootstrap-icons.woff2 │ ├── images │ │ ├── ai-avatar.png │ │ ├── alert.png │ │ ├── logo.png │ │ └── user-avatar.png │ ├── js │ │ ├── admin │ │ │ └── admin_settings.js │ │ ├── bootstrap │ │ │ ├── bootstrap.bundle.js │ │ │ ├── bootstrap.bundle.js.map │ │ │ ├── bootstrap.bundle.min.js │ │ │ ├── bootstrap.bundle.min.js.map │ │ │ ├── bootstrap.esm.js │ │ │ ├── bootstrap.esm.js.map │ │ │ ├── bootstrap.esm.min.js │ │ │ ├── bootstrap.esm.min.js.map │ │ │ ├── bootstrap.js │ │ │ ├── bootstrap.js.map │ │ │ ├── bootstrap.min.js │ │ │ └── bootstrap.min.js.map │ │ ├── chat │ │ │ ├── bootstrap.bundle.min.js │ │ │ ├── chat-citations.js │ │ │ ├── chat-conversations.js │ │ │ ├── chat-documents.js │ │ │ ├── chat-feedback.js │ │ │ ├── chat-global.js │ │ │ ├── chat-input-actions.js │ │ │ ├── chat-layout.js │ │ │ ├── chat-loading-indicator.js │ │ │ ├── chat-messages.js │ │ │ ├── chat-onload.js │ │ │ ├── chat-prompts.js │ │ │ ├── chat-toast.js │ │ │ ├── chat-utils.js │ │ │ ├── dataTables.responsive.min.js │ │ │ ├── jquery.dataTables.min.js │ │ │ ├── jquery.min.js │ │ │ ├── marked.min.js │ │ │ ├── purify.min.js │ │ │ └── split.min.js │ │ ├── dark-mode.js │ │ ├── datatables │ │ │ ├── datatables.js │ │ │ └── datatables.min.js │ │ ├── group │ │ │ └── manage_group.js │ │ ├── simplemde │ │ │ ├── simplemde.js │ │ │ └── simplemde.min.js │ │ └── workspace │ │ │ ├── workspace-documents.js │ │ │ ├── workspace-init.js │ │ │ ├── workspace-prompts.js │ │ │ └── workspace-utils.js │ ├── json │ │ ├── ai_search-index-group.json │ │ └── ai_search-index-user.json │ └── robots.txt │ └── templates │ ├── acceptable_use_policy.html │ ├── admin_feedback_review.html │ ├── admin_safety_violations.html │ ├── admin_settings.html │ ├── base.html │ ├── chats.html │ ├── group_workspaces.html │ ├── index.html │ ├── manage_group.html │ ├── my_feedback.html │ ├── my_groups.html │ ├── my_safety_violations.html │ ├── profile.html │ └── workspace.html ├── artifacts ├── ai_search_index │ ├── ai_search-index-group.json │ └── ai_search-index-user.json ├── app_service_manifest │ └── manifest-ms_graph.json ├── architecture.vsdx ├── cosmos_examples │ ├── cosmos-conversation-example.json │ ├── cosmos-feedback-example.json │ ├── cosmos-file_processing-example.json │ ├── cosmos-group_documents-example.json │ ├── cosmos-group_prompts.json │ ├── cosmos-message-example.json │ ├── cosmos-safety-example.json │ ├── cosmos-settings-example.json │ ├── cosmos-user_documents-example.json │ ├── cosmos-user_prompts-example.json │ └── cosmos-user_settings-example.json ├── open_api │ └── openapi.yaml └── private_endpoints.vsdx └── images ├── ChatwithSearchingYourDocsDemo.gif ├── UploadDocumentDemo.gif ├── add_role_assignment-job_function.png ├── add_role_assignment-select_member-managed_identity.png ├── add_role_assignment-select_member-service_principal.png ├── admin_settings-enable_and_configure_doc_classification.png ├── admin_settings-enable_audio_file_support.png ├── admin_settings-enable_conversation_archiving.png ├── admin_settings-enable_enhanced_citations.png ├── admin_settings-enable_file_processing_logs.png ├── admin_settings-enable_groups.png ├── admin_settings-enable_user_feedback.png ├── admin_settings-enable_video_file_support.png ├── admin_settings-enable_workspace.png ├── admin_settings-upgrade_available_notification.png ├── admin_settings_page.png ├── advanced_edit_env.png ├── ai_search-missing_index_fields.png ├── app_reg-api_permissions.png ├── app_reg-app_role-create_group.png ├── app_reg-app_roles.png ├── app_reg-authentication.png ├── app_reg_edit_identity.png ├── app_reg_secrets.png ├── app_reg_settings.png ├── architecture.png ├── chat-classification_propagation.png ├── chat-delete_conversation.png ├── chat-feedback-negative.png ├── chat.png ├── clone_the_repo.png ├── content_safety-cosmos_container.png ├── content_safety-in_action.png ├── content_safety-management.png ├── content_safety-settings.png ├── content_safety-taking_action.png ├── content_safety-user_view.png ├── cosmos_container-view_archived_conversation.png ├── cosmos_container-view_archived_messages.png ├── cosmos_container-view_specific_doc_file_processing_logs.png ├── cross_tenant-model_support.png ├── download_remote_settings.png ├── enable_managed_identity.png ├── enterprise_app-add_user_to_role.png ├── feedback_review-list_all.png ├── feedback_review-workflow.png ├── files_to_zip.png ├── group_workspace-doc_list.png ├── icon.png ├── logo.png ├── logo_larger.png ├── manage_group-add_member.png ├── manage_group-group_details_as_owner.png ├── manage_group-update_member_role.png ├── my_feedback-list_all.png ├── my_feedback-view_specific.png ├── my_groups-find_group-request_to_join.png ├── my_groups-group_list.png ├── scale-cosmos.png ├── storage_account-view_doc_for_citation_retrieval.png ├── upload_local_settings_1.png ├── upload_local_settings_2.png ├── visit_app.png ├── workflow-add_your_data.png ├── workflow-content_safety.png ├── workflow-upload_process_audio_file.png ├── workflow-upload_video_file.png ├── workflow-view_and_update_classification.png ├── workspace-doc_list.png ├── workspace-enhanced_citation_tag.png ├── workspace-prompt_edit.png ├── workspace-prompt_list.png └── zip_the_files.png /.deployment: -------------------------------------------------------------------------------- 1 | [config] 2 | project = application/single_app 3 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/python 3 | { 4 | "name": "Python 3", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/python:0-3.11", 7 | "features": { 8 | "ghcr.io/devcontainers/features/python:1": {}, 9 | "ghcr.io/devcontainers-contrib/features/black:2": {} 10 | }, 11 | // Features to add to the dev container. More info: https://containers.dev/features. 12 | // "features": {}, 13 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 14 | // "forwardPorts": [], 15 | // Use 'postCreateCommand' to run commands after the container is created. 16 | "postCreateCommand": "pip3 install --user -r application/single_app/requirements.txt" 17 | // Configure tool-specific properties. 18 | // "customizations": {}, 19 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 20 | // "remoteUser": "root" 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/docker_image_publish.yml: -------------------------------------------------------------------------------- 1 | 2 | name: SimpleChat Docker Image Publish 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | workflow_dispatch: 9 | 10 | jobs: 11 | 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Azure Container Registry Login 18 | uses: Azure/docker-login@v2 19 | with: 20 | # Container registry username 21 | username: ${{ secrets.ACR_USERNAME }} 22 | # Container registry password 23 | password: ${{ secrets.ACR_PASSWORD }} 24 | # Container registry server url 25 | login-server: ${{ secrets.ACR_LOGIN_SERVER }} 26 | 27 | - uses: actions/checkout@v3 28 | - name: Build the Docker image 29 | run: 30 | docker build . --file application/single_app/Dockerfile --tag ${{ secrets.ACR_LOGIN_SERVER }}/simple-chat:$(date +'%Y-%m-%d')_$GITHUB_RUN_NUMBER; 31 | docker tag ${{ secrets.ACR_LOGIN_SERVER }}/simple-chat:$(date +'%Y-%m-%d')_$GITHUB_RUN_NUMBER ${{ secrets.ACR_LOGIN_SERVER }}/simple-chat:latest; 32 | docker push ${{ secrets.ACR_LOGIN_SERVER }}/simple-chat:$(date +'%Y-%m-%d')_$GITHUB_RUN_NUMBER; 33 | docker push ${{ secrets.ACR_LOGIN_SERVER }}/simple-chat:latest; 34 | -------------------------------------------------------------------------------- /.github/workflows/docker_image_publish_dev.yml: -------------------------------------------------------------------------------- 1 | 2 | name: SimpleChat Docker Image Publish (Development) 3 | 4 | on: 5 | push: 6 | branches: 7 | - Development 8 | workflow_dispatch: 9 | 10 | jobs: 11 | 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Azure Container Registry Login 18 | uses: Azure/docker-login@v2 19 | with: 20 | # Container registry username 21 | username: ${{ secrets.ACR_USERNAME }} 22 | # Container registry password 23 | password: ${{ secrets.ACR_PASSWORD }} 24 | # Container registry server url 25 | login-server: ${{ secrets.ACR_LOGIN_SERVER }} 26 | 27 | - uses: actions/checkout@v3 28 | - name: Build the Docker image 29 | run: 30 | docker build . --file application/single_app/Dockerfile --tag ${{ secrets.ACR_LOGIN_SERVER }}/simple-chat:$(date +'%Y-%m-%d')_$GITHUB_RUN_NUMBER; 31 | docker tag ${{ secrets.ACR_LOGIN_SERVER }}/simple-chat:$(date +'%Y-%m-%d')_$GITHUB_RUN_NUMBER ${{ secrets.ACR_LOGIN_SERVER }}/simple-chat:latest; 32 | docker push ${{ secrets.ACR_LOGIN_SERVER }}/simple-chat:$(date +'%Y-%m-%d')_$GITHUB_RUN_NUMBER; 33 | docker push ${{ secrets.ACR_LOGIN_SERVER }}/simple-chat:latest; 34 | -------------------------------------------------------------------------------- /.github/workflows/enforce-dev-to-main.yml: -------------------------------------------------------------------------------- 1 | name: Enforce PRs to main only from development 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - reopened 8 | - synchronize 9 | 10 | jobs: 11 | require-dev-base: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Fail if PR→main doesn’t come from development 15 | if: > 16 | github.event.pull_request.base.ref == 'main' && 17 | github.event.pull_request.head.ref != 'development' 18 | run: | 19 | echo "::error ::Pull requests into 'main' must originate from branch 'development'." 20 | exit 1 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Azure Functions artifacts 3 | bin 4 | obj 5 | appsettings.json 6 | local.settings.json 7 | 8 | # Azurite artifacts 9 | __blobstorage__ 10 | __queuestorage__ 11 | __azurite_db*__.json 12 | .python_packages 13 | __pycache__ 14 | .venv 15 | 16 | # Azure App Service artifacts 17 | .env 18 | .pem 19 | .deployment 20 | 21 | # macOS custom attributes file 22 | .ds_store 23 | 24 | # Configured custom script extensions, powershell scripts, and linux scripts 25 | priv-* 26 | 27 | # Visual Studio Code 28 | .vscode 29 | 30 | # temporary files 31 | flask_session -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | 3 | MIT License 4 | 5 | Copyright (c) 2024 Paul Lizer 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /application/external_apps/README.md: -------------------------------------------------------------------------------- 1 | # Placeholder -------------------------------------------------------------------------------- /application/single_app/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/application/single_app/.DS_Store -------------------------------------------------------------------------------- /application/single_app/.dockerignore: -------------------------------------------------------------------------------- 1 | .env 2 | __pycache__ 3 | flask_session 4 | Dockerfile 5 | .dockerignore -------------------------------------------------------------------------------- /application/single_app/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Python image from the Docker Hub 2 | FROM python:3.12-slim 3 | 4 | # Set the working directory in the container 5 | WORKDIR /app 6 | 7 | # Copy the requirements file into the container 8 | COPY application/single_app/requirements.txt . 9 | 10 | RUN apt-get update && apt-get install -y \ 11 | build-essential \ 12 | python3-dev \ 13 | libffi-dev \ 14 | libssl-dev \ 15 | && apt-get clean 16 | 17 | RUN pip install --upgrade pip setuptools wheel 18 | RUN pip install --no-cache-dir -r requirements.txt 19 | 20 | # Copy the rest of the application code into the container 21 | COPY application/single_app . 22 | 23 | # Expose the port the app runs on 24 | EXPOSE 5000 25 | 26 | # Define the command to run the application 27 | CMD ["flask", "run", "--host=0.0.0.0"] 28 | -------------------------------------------------------------------------------- /application/single_app/app.py: -------------------------------------------------------------------------------- 1 | # app.py 2 | 3 | from config import * 4 | 5 | from functions_authentication import * 6 | from functions_content import * 7 | from functions_documents import * 8 | from functions_search import * 9 | from functions_settings import * 10 | 11 | from route_frontend_authentication import * 12 | from route_frontend_profile import * 13 | from route_frontend_admin_settings import * 14 | from route_frontend_workspace import * 15 | from route_frontend_chats import * 16 | from route_frontend_conversations import * 17 | from route_frontend_groups import * 18 | from route_frontend_group_workspaces import * 19 | from route_frontend_safety import * 20 | from route_frontend_feedback import * 21 | 22 | from route_backend_chats import * 23 | from route_backend_conversations import * 24 | from route_backend_documents import * 25 | from route_backend_groups import * 26 | from route_backend_users import * 27 | from route_backend_group_documents import * 28 | from route_backend_models import * 29 | from route_backend_safety import * 30 | from route_backend_feedback import * 31 | from route_backend_settings import * 32 | from route_backend_prompts import * 33 | from route_backend_group_prompts import * 34 | 35 | # =================== Helper Functions =================== 36 | @app.before_first_request 37 | def before_first_request(): 38 | settings = get_settings() 39 | initialize_clients(settings) 40 | ensure_custom_logo_file_exists(app, settings) 41 | 42 | @app.context_processor 43 | def inject_settings(): 44 | settings = get_settings() 45 | public_settings = sanitize_settings_for_user(settings) 46 | # No change needed if you already return app_settings=public_settings 47 | return dict(app_settings=public_settings) 48 | 49 | @app.template_filter('to_datetime') 50 | def to_datetime_filter(value): 51 | return datetime.fromisoformat(value) 52 | 53 | @app.template_filter('format_datetime') 54 | def format_datetime_filter(value): 55 | return value.strftime('%Y-%m-%d %H:%M') 56 | 57 | @app.after_request 58 | def add_security_headers(response): 59 | response.headers['X-Content-Type-Options'] = 'nosniff' 60 | return response 61 | 62 | # Register a custom Jinja filter for Markdown 63 | def markdown_filter(text): 64 | if not text: 65 | text = "" 66 | 67 | # Convert Markdown to HTML 68 | html = markdown2.markdown(text) 69 | 70 | # Add target="_blank" to all links 71 | html = re.sub(r'(;IngestionEndpoint=;LiveEndpoint=;ApplicationId=", "slotSetting": false }, 3 | { "name": "MICROSOFT_PROVIDER_AUTHENTICATION_SECRET", "value": "", "slotSetting": true }, 4 | { "name": "WEBSITE_AUTH_AAD_ALLOWED_TENANTS", "value": "", "slotSetting": false }, 5 | { "name": "AZURE_COSMOS_ENDPOINT", "value": "", "slotSetting": false }, 6 | { "name": "AZURE_COSMOS_KEY", "value": "", "slotSetting": false }, 7 | { "name": "AZURE_COSMOS_AUTHENTICATION_TYPE", "value": "key", "slotSetting": false }, 8 | { "name": "CLIENT_ID", "value": "", "slotSetting": false }, 9 | { "name": "TENANT_ID", "value": "", "slotSetting": false }, 10 | { "name": "SECRET_KEY", "value": "", "slotSetting": false }, 11 | { "name": "BING_SEARCH_ENDPOINT", "value": "https://api.bing.microsoft.com/", "slotSetting": false }, 12 | { "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", "value": "true", "slotSetting": false }, 13 | { "name": "WEBSITE_HTTPLOGGING_RETENTION_DAYS", "value": "7", "slotSetting": false }, 14 | { "name": "APPINSIGHTS_INSTRUMENTATIONKEY", "value": "", "slotSetting": false }, 15 | { "name": "ApplicationInsightsAgent_EXTENSION_VERSION", "value": "~3", "slotSetting": false }, 16 | { "name": "APPLICATIONINSIGHTSAGENT_EXTENSION_ENABLED", "value": "true", "slotSetting": false }, 17 | { "name": "XDT_MicrosoftApplicationInsights_Mode", "value": "default", "slotSetting": false }, 18 | { "name": "APPINSIGHTS_PROFILERFEATURE_VERSION", "value": "1.0.0", "slotSetting": false }, 19 | { "name": "APPINSIGHTS_SNAPSHOTFEATURE_VERSION", "value": "1.0.0", "slotSetting": false }, 20 | { "name": "SnapshotDebugger_EXTENSION_VERSION", "value": "disabled", "slotSetting": false }, 21 | { "name": "InstrumentationEngine_EXTENSION_VERSION", "value": "disabled", "slotSetting": false }, 22 | { "name": "XDT_MicrosoftApplicationInsights_BaseExtensions", "value": "disabled", "slotSetting": false }, 23 | { "name": "XDT_MicrosoftApplicationInsights_PreemptSdk", "value": "disabled", "slotSetting": false } 24 | ] -------------------------------------------------------------------------------- /application/single_app/functions_bing_search.py: -------------------------------------------------------------------------------- 1 | # functions_bing_search.py 2 | 3 | from config import * 4 | from functions_settings import * 5 | 6 | def get_suggestions(query): 7 | settings = get_settings() 8 | if not settings.get('enable_web_search'): 9 | return [] 10 | 11 | bing_key = settings.get('bing_search_key', '') 12 | if not bing_key: 13 | return [] 14 | 15 | autosuggest_url = f"{bing_search_endpoint}/v7.0/suggestions" 16 | headers = {"Ocp-Apim-Subscription-Key": bing_key} 17 | params = {"q": query} 18 | response = requests.get(autosuggest_url, headers=headers, params=params) 19 | response.raise_for_status() 20 | suggestions = response.json()["suggestionGroups"][0]["searchSuggestions"] 21 | return [s["displayText"] for s in suggestions] 22 | 23 | def get_search_results(query, top_n=10): 24 | settings = get_settings() 25 | if not settings.get('enable_web_search'): 26 | return [] 27 | 28 | bing_key = settings.get('bing_search_key', '') 29 | if not bing_key: 30 | return [] 31 | 32 | search_url = f"{bing_search_endpoint}/v7.0/search" 33 | headers = {"Ocp-Apim-Subscription-Key": bing_key} 34 | params = {"q": query, "count": top_n} 35 | response = requests.get(search_url, headers=headers, params=params) 36 | response.raise_for_status() 37 | results = response.json().get("webPages", {}).get("value", []) 38 | return [{"name": r["name"], "url": r["url"], "snippet": r["snippet"]} for r in results] 39 | 40 | 41 | def process_query_with_bing_and_llm(user_query, top_n=10): 42 | print(f"Original Query: {user_query}") 43 | suggestions = get_suggestions(user_query) 44 | if suggestions: 45 | refined_query = suggestions[0] 46 | print(f"Refined Query (from Autosuggest): {refined_query}") 47 | else: 48 | refined_query = user_query 49 | print("No suggestions available. Using the original query.") 50 | 51 | search_results = get_search_results(refined_query, top_n=top_n) 52 | print(f"Search Results: {search_results}") 53 | 54 | return search_results -------------------------------------------------------------------------------- /application/single_app/functions_group.py: -------------------------------------------------------------------------------- 1 | # functions_group.py 2 | 3 | from config import * 4 | from functions_authentication import * 5 | from functions_settings import * 6 | 7 | 8 | def create_group(name, description): 9 | """Creates a new group. The creator is the Owner by default.""" 10 | user_info = get_current_user_info() 11 | if not user_info: 12 | raise Exception("No user in session") 13 | 14 | new_group_id = str(uuid.uuid4()) 15 | now_str = datetime.utcnow().isoformat() 16 | 17 | group_doc = { 18 | "id": new_group_id, 19 | "name": name, 20 | "description": description, 21 | "owner": 22 | { 23 | "id": user_info["userId"], 24 | "email": user_info["email"], 25 | "displayName": user_info["displayName"] 26 | }, 27 | "admins": [], 28 | "documentManagers": [], 29 | "users": [ 30 | { 31 | "userId": user_info["userId"], 32 | "email": user_info["email"], 33 | "displayName": user_info["displayName"] 34 | } 35 | ], 36 | "pendingUsers": [], 37 | "createdDate": now_str, 38 | "modifiedDate": now_str 39 | } 40 | cosmos_groups_container.create_item(group_doc) 41 | return group_doc 42 | 43 | def search_groups(search_query, user_id): 44 | """ 45 | Return a list of groups the user is in or (optionally) 46 | you can expand to also return public groups. 47 | For simplicity, this only returns groups where the user is a member. 48 | """ 49 | query = query = """ 50 | SELECT * 51 | FROM c 52 | WHERE EXISTS ( 53 | SELECT VALUE u 54 | FROM u IN c.users 55 | WHERE u.userId = @user_id 56 | ) 57 | """ 58 | 59 | params = [ 60 | { "name": "@user_id", "value": user_id } 61 | ] 62 | if search_query: 63 | query += " AND CONTAINS(c.name, @search) " 64 | params.append({"name": "@search", "value": search_query}) 65 | 66 | results = list(cosmos_groups_container.query_items( 67 | query=query, 68 | parameters=params, 69 | enable_cross_partition_query=True 70 | )) 71 | return results 72 | 73 | def get_user_groups(user_id): 74 | """ 75 | Fetch all groups for which this user is a member. 76 | """ 77 | query = query = """ 78 | SELECT * 79 | FROM c 80 | WHERE EXISTS ( 81 | SELECT VALUE x 82 | FROM x IN c.users 83 | WHERE x.userId = @user_id 84 | ) 85 | """ 86 | 87 | params = [{ "name": "@user_id", "value": user_id }] 88 | results = list(cosmos_groups_container.query_items( 89 | query=query, 90 | parameters=params, 91 | enable_cross_partition_query=True 92 | )) 93 | return results 94 | 95 | def find_group_by_id(group_id): 96 | """Retrieve a single group doc by its ID.""" 97 | try: 98 | group_doc = cosmos_groups_container.read_item( 99 | item=group_id, 100 | partition_key=group_id 101 | ) 102 | return group_doc 103 | except exceptions.CosmosResourceNotFoundError: 104 | return None 105 | 106 | def update_active_group_for_user(group_id): 107 | user_id = get_current_user_id() 108 | new_settings = { 109 | "activeGroupOid": group_id 110 | } 111 | update_user_settings(user_id, new_settings) 112 | 113 | def get_user_role_in_group(group_doc, user_id): 114 | """Determine the user's role in the given group doc.""" 115 | if not group_doc: 116 | return None 117 | 118 | if group_doc.get("owner", {}).get("id") == user_id: 119 | return "Owner" 120 | elif user_id in group_doc.get("admins", []): 121 | return "Admin" 122 | elif user_id in group_doc.get("documentManagers", []): 123 | return "DocumentManager" 124 | else: 125 | for u in group_doc.get("users", []): 126 | if u["userId"] == user_id: 127 | return "User" 128 | 129 | return None 130 | 131 | 132 | def map_group_list_for_frontend(groups, current_user_id): 133 | """ 134 | Utility to produce a simplified list of group data 135 | for the front-end, including userRole and isActive. 136 | """ 137 | active_group_id = session.get("active_group") 138 | response = [] 139 | for g in groups: 140 | role = get_user_role_in_group(g, current_user_id) 141 | response.append({ 142 | "id": g["id"], 143 | "name": g["name"], 144 | "description": g.get("description", ""), 145 | "userRole": role, 146 | "isActive": (g["id"] == active_group_id) 147 | }) 148 | return response 149 | 150 | def delete_group(group_id): 151 | """ 152 | Deletes a group from Cosmos DB. Typically only owner can do this. 153 | """ 154 | cosmos_groups_container.delete_item(item=group_id, partition_key=group_id) 155 | 156 | def is_user_in_group(group_doc, user_id): 157 | """ 158 | Helper to check if a user is in the given group's users[] or is the owner. 159 | """ 160 | if group_doc.get("owner", {}).get("id") == user_id: 161 | return True 162 | 163 | for u in group_doc.get("users", []): 164 | if u["userId"] == user_id: 165 | return True 166 | return False -------------------------------------------------------------------------------- /application/single_app/functions_logging.py: -------------------------------------------------------------------------------- 1 | # functions_logging.py 2 | 3 | from config import * 4 | from functions_settings import * 5 | 6 | def add_file_task_to_file_processing_log(document_id, user_id, content): 7 | settings = get_settings() 8 | enable_file_processing_log = settings.get('enable_file_processing_log', True) 9 | 10 | if enable_file_processing_log: 11 | try: 12 | id_value = str(uuid.uuid4()) 13 | log_item = { 14 | "id": id_value, 15 | "document_id": document_id, 16 | "user_id": user_id, 17 | "log": content, 18 | "timestamp": datetime.utcnow().isoformat() 19 | } 20 | cosmos_file_processing_container.create_item(log_item) 21 | except Exception as e: 22 | raise e 23 | 24 | -------------------------------------------------------------------------------- /application/single_app/functions_prompts.py: -------------------------------------------------------------------------------- 1 | # functions_prompts.py 2 | 3 | from config import * 4 | 5 | def get_pagination_params(args): 6 | try: 7 | page = int(args.get('page', 1)) 8 | if page < 1: 9 | page = 1 10 | except (ValueError, TypeError): 11 | page = 1 12 | 13 | try: 14 | page_size = int(args.get('page_size', 10)) 15 | if page_size < 1: 16 | page_size = 10 17 | if page_size > 100: 18 | page_size = 100 19 | except (ValueError, TypeError): 20 | page_size = 10 21 | 22 | return page, page_size 23 | 24 | 25 | def list_prompts(user_id, prompt_type, args, group_id=None): 26 | """ 27 | List prompts for a user or a group with pagination and optional search. 28 | Returns: (items, total_count, page, page_size) 29 | """ 30 | is_group = group_id is not None 31 | cosmos_container = cosmos_group_prompts_container if is_group else cosmos_user_prompts_container 32 | 33 | # Determine filter field and value 34 | id_field = 'group_id' if is_group else 'user_id' 35 | id_value = group_id if is_group else user_id 36 | 37 | page, page_size = get_pagination_params(args) 38 | search_term = args.get('search') 39 | 40 | base_filter = f"c.{id_field} = @id_value AND c.type = @prompt_type" 41 | parameters = [ 42 | {"name": "@id_value", "value": id_value}, 43 | {"name": "@prompt_type", "value": prompt_type} 44 | ] 45 | 46 | # Build count and select queries 47 | count_query = f"SELECT VALUE COUNT(1) FROM c WHERE {base_filter}" 48 | select_query = f"SELECT * FROM c WHERE {base_filter}" 49 | 50 | if search_term: 51 | st = search_term[:100] 52 | select_query += " AND CONTAINS(c.name, @search, true)" 53 | count_query += " AND CONTAINS(c.name, @search, true)" 54 | parameters.append({"name": "@search", "value": st}) 55 | 56 | select_query += " ORDER BY c.updated_at DESC" 57 | offset = (page - 1) * page_size 58 | select_query += f" OFFSET {offset} LIMIT {page_size}" 59 | 60 | # Execute count 61 | total_count = list( 62 | cosmos_container.query_items( 63 | query=count_query, 64 | parameters=parameters, 65 | enable_cross_partition_query=True 66 | ) 67 | ) 68 | total_count = total_count[0] if total_count else 0 69 | 70 | # Execute select 71 | items = list( 72 | cosmos_container.query_items( 73 | query=select_query, 74 | parameters=parameters, 75 | enable_cross_partition_query=True 76 | ) 77 | ) 78 | 79 | return items, total_count, page, page_size 80 | 81 | 82 | def create_prompt_doc(name, content, prompt_type, user_id, group_id=None): 83 | """ 84 | Create a new prompt for a user or a group. 85 | Returns minimal created doc. 86 | """ 87 | now = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ') 88 | prompt_id = str(uuid.uuid4()) 89 | is_group = group_id is not None 90 | cosmos_container = cosmos_group_prompts_container if is_group else cosmos_user_prompts_container 91 | 92 | # Build the document 93 | doc = { 94 | "id": prompt_id, 95 | "name": name.strip(), 96 | "content": content, 97 | "type": prompt_type, 98 | "created_at": now, 99 | "updated_at": now, 100 | "group_id" if is_group else "user_id": group_id if is_group else user_id 101 | } 102 | 103 | created = cosmos_container.create_item(body=doc) 104 | return { 105 | "id": created["id"], 106 | "name": created["name"], 107 | "updated_at": created["updated_at"] 108 | } 109 | 110 | 111 | def get_prompt_doc(user_id, prompt_id, prompt_type, group_id=None): 112 | """ 113 | Retrieve a prompt by ID for a user or group. 114 | Returns the item dict or None. 115 | """ 116 | is_group = group_id is not None 117 | cosmos_container = cosmos_group_prompts_container if is_group else cosmos_user_prompts_container 118 | 119 | # Try direct read 120 | try: 121 | item = cosmos_container.read_item(item=prompt_id, partition_key=prompt_id) 122 | if item.get("type") == prompt_type and ( 123 | item.get("group_id") == group_id if is_group else item.get("user_id") == user_id 124 | ): 125 | return item 126 | except CosmosResourceNotFoundError: 127 | pass 128 | 129 | # Fallback to query 130 | id_field = 'group_id' if is_group else 'user_id' 131 | id_value = group_id if is_group else user_id 132 | query = ( 133 | "SELECT * FROM c WHERE c.id=@pid AND c.{0}=@id AND c.type=@type" 134 | ).format(id_field) 135 | parameters = [ 136 | {"name": "@pid", "value": prompt_id}, 137 | {"name": "@id", "value": id_value}, 138 | {"name": "@type", "value": prompt_type} 139 | ] 140 | 141 | items = list( 142 | cosmos_container.query_items( 143 | query=query, 144 | parameters=parameters, 145 | enable_cross_partition_query=True 146 | ) 147 | ) 148 | return items[0] if items else None 149 | 150 | 151 | def update_prompt_doc(user_id, prompt_id, prompt_type, updates, group_id=None): 152 | """ 153 | Update an existing prompt for a user or a group. 154 | Returns minimal updated doc or None if not found. 155 | """ 156 | item = get_prompt_doc(user_id, prompt_id, prompt_type, group_id) 157 | if not item: 158 | return None 159 | 160 | # Apply updates 161 | for k, v in updates.items(): 162 | item[k] = v 163 | item["updated_at"] = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ') 164 | 165 | is_group = group_id is not None 166 | cosmos_container = cosmos_group_prompts_container if is_group else cosmos_user_prompts_container 167 | updated = cosmos_container.replace_item(item=prompt_id, body=item) 168 | 169 | return { 170 | "id": updated["id"], 171 | "name": updated["name"], 172 | "updated_at": updated["updated_at"] 173 | } 174 | 175 | 176 | def delete_prompt_doc(user_id, prompt_id, group_id=None): 177 | """ 178 | Delete a prompt for a user or a group. 179 | Returns True if deleted, False if not found. 180 | """ 181 | item = get_prompt_doc(user_id, prompt_id, None, group_id) 182 | if not item: 183 | return False 184 | 185 | is_group = group_id is not None 186 | cosmos_container = cosmos_group_prompts_container if is_group else cosmos_user_prompts_container 187 | cosmos_container.delete_item(item=prompt_id, partition_key=prompt_id) 188 | 189 | return True 190 | -------------------------------------------------------------------------------- /application/single_app/functions_search.py: -------------------------------------------------------------------------------- 1 | # functions_search.py 2 | 3 | from config import * 4 | from functions_content import * 5 | 6 | def hybrid_search(query, user_id, document_id=None, top_n=12, doc_scope="all", active_group_id=None): 7 | """ 8 | Hybrid search that queries the user doc index or the group doc index 9 | depending on doc type. 10 | If document_id is None, we just search the user index for the user's docs 11 | OR you could unify that logic further (maybe search both). 12 | """ 13 | query_embedding = generate_embedding(query) 14 | if query_embedding is None: 15 | return None 16 | 17 | search_client_user = CLIENTS['search_client_user'] 18 | search_client_group = CLIENTS['search_client_group'] 19 | 20 | vector_query = VectorizedQuery( 21 | vector=query_embedding, 22 | k_nearest_neighbors=top_n, 23 | fields="embedding" 24 | ) 25 | 26 | if doc_scope == "all": 27 | if document_id: 28 | user_results = search_client_user.search( 29 | search_text=query, 30 | vector_queries=[vector_query], 31 | filter=f"user_id eq '{user_id}' and document_id eq '{document_id}'", 32 | query_type="semantic", 33 | semantic_configuration_name="nexus-user-index-semantic-configuration", 34 | query_caption="extractive", 35 | query_answer="extractive", 36 | select=["id", "chunk_text", "chunk_id", "file_name", "user_id", "version", "chunk_sequence", "upload_date", "document_classification", "page_number", "author", "chunk_keywords", "title", "chunk_summary"] 37 | ) 38 | 39 | group_results = search_client_group.search( 40 | search_text=query, 41 | vector_queries=[vector_query], 42 | filter=f"group_id eq '{active_group_id}' and document_id eq '{document_id}'", 43 | query_type="semantic", 44 | semantic_configuration_name="nexus-group-index-semantic-configuration", 45 | query_caption="extractive", 46 | query_answer="extractive", 47 | select=["id", "chunk_text", "chunk_id", "file_name", "group_id", "version", "chunk_sequence", "upload_date", "document_classification", "page_number", "author", "chunk_keywords", "title", "chunk_summary"] 48 | ) 49 | else: 50 | user_results = search_client_user.search( 51 | search_text=query, 52 | vector_queries=[vector_query], 53 | filter=f"user_id eq '{user_id}'", 54 | query_type="semantic", 55 | semantic_configuration_name="nexus-user-index-semantic-configuration", 56 | query_caption="extractive", 57 | query_answer="extractive", 58 | select=["id", "chunk_text", "chunk_id", "file_name", "user_id", "version", "chunk_sequence", "upload_date", "document_classification", "page_number", "author", "chunk_keywords", "title", "chunk_summary"] 59 | ) 60 | 61 | group_results = search_client_group.search( 62 | search_text=query, 63 | vector_queries=[vector_query], 64 | filter=f"group_id eq '{active_group_id}'", 65 | query_type="semantic", 66 | semantic_configuration_name="nexus-group-index-semantic-configuration", 67 | query_caption="extractive", 68 | query_answer="extractive", 69 | select=["id", "chunk_text", "chunk_id", "file_name", "group_id", "version", "chunk_sequence", "upload_date", "document_classification", "page_number", "author", "chunk_keywords", "title", "chunk_summary"] 70 | ) 71 | 72 | user_results_final = extract_search_results(user_results, top_n) 73 | group_results_final = extract_search_results(group_results, top_n) 74 | results = user_results_final + group_results_final 75 | 76 | elif doc_scope == "personal": 77 | if document_id: 78 | user_results = search_client_user.search( 79 | search_text=query, 80 | vector_queries=[vector_query], 81 | filter=f"user_id eq '{user_id}' and document_id eq '{document_id}'", 82 | query_type="semantic", 83 | semantic_configuration_name="nexus-user-index-semantic-configuration", 84 | query_caption="extractive", 85 | query_answer="extractive", 86 | select=["id", "chunk_text", "chunk_id", "file_name", "user_id", "version", "chunk_sequence", "upload_date", "document_classification", "page_number", "author", "chunk_keywords", "title", "chunk_summary"] 87 | ) 88 | results = extract_search_results(user_results, top_n) 89 | else: 90 | user_results = search_client_user.search( 91 | search_text=query, 92 | vector_queries=[vector_query], 93 | filter=f"user_id eq '{user_id}'", 94 | query_type="semantic", 95 | semantic_configuration_name="nexus-user-index-semantic-configuration", 96 | query_caption="extractive", 97 | query_answer="extractive", 98 | select=["id", "chunk_text", "chunk_id", "file_name", "user_id", "version", "chunk_sequence", "upload_date", "document_classification", "page_number", "author", "chunk_keywords", "title", "chunk_summary"] 99 | ) 100 | results = extract_search_results(user_results, top_n) 101 | 102 | elif doc_scope == "group": 103 | if document_id: 104 | group_results = search_client_group.search( 105 | search_text=query, 106 | vector_queries=[vector_query], 107 | filter=f"group_id eq '{active_group_id}' and document_id eq '{document_id}'", 108 | query_type="semantic", 109 | semantic_configuration_name="nexus-group-index-semantic-configuration", 110 | query_caption="extractive", 111 | query_answer="extractive", 112 | select=["id", "chunk_text", "chunk_id", "file_name", "group_id", "version", "chunk_sequence", "upload_date", "document_classification", "page_number", "author", "chunk_keywords", "title", "chunk_summary"] 113 | ) 114 | results = extract_search_results(group_results, top_n) 115 | else: 116 | group_results = search_client_group.search( 117 | search_text=query, 118 | vector_queries=[vector_query], 119 | filter=f"group_id eq '{active_group_id}'", 120 | query_type="semantic", 121 | semantic_configuration_name="nexus-group-index-semantic-configuration", 122 | query_caption="extractive", 123 | query_answer="extractive", 124 | select=["id", "chunk_text", "chunk_id", "file_name", "group_id", "version", "chunk_sequence", "upload_date", "document_classification", "page_number", "author", "chunk_keywords", "title", "chunk_summary"] 125 | ) 126 | results = extract_search_results(group_results, top_n) 127 | 128 | results = sorted(results, key=lambda x: x['score'], reverse=True)[:top_n] 129 | 130 | return results 131 | 132 | def extract_search_results(paged_results, top_n): 133 | extracted = [] 134 | for i, r in enumerate(paged_results): 135 | if i >= top_n: 136 | break 137 | extracted.append({ 138 | "id": r["id"], 139 | "chunk_text": r["chunk_text"], 140 | "chunk_id": r["chunk_id"], 141 | "file_name": r["file_name"], 142 | "group_id": r.get("group_id"), 143 | "version": r["version"], 144 | "chunk_sequence": r["chunk_sequence"], 145 | "upload_date": r["upload_date"], 146 | "document_classification": r["document_classification"], 147 | "page_number": r["page_number"], 148 | "author": r["author"], 149 | "chunk_keywords": r["chunk_keywords"], 150 | "title": r["title"], 151 | "chunk_summary": r["chunk_summary"], 152 | "score": r["@search.score"] 153 | }) 154 | return extracted -------------------------------------------------------------------------------- /application/single_app/requirements.txt: -------------------------------------------------------------------------------- 1 | # requirements.txt 2 | 3 | Flask==2.2.5 4 | gunicorn 5 | Werkzeug==3.0.6 6 | requests==2.32.0 7 | openai==1.59.7 8 | docx2txt==0.8 9 | Markdown==3.3.4 10 | bleach==6.1.0 11 | azure-cosmos==4.3.0 12 | msal==1.31.0 13 | Flask-Session==0.8.0 14 | azure-ai-documentintelligence==1.0.0b4 15 | numpy==2.1.1 16 | scikit-learn==1.5.2 17 | SciPy==1.14.1 18 | joblib==1.4.2 19 | threadpoolctl==3.5.0 20 | azure-search-documents==11.5.1 21 | python-dotenv==0.19.1 22 | azure-ai-formrecognizer==3.3.3 23 | pyjwt==2.9.0 24 | pandas==2.2.3 25 | markdown2==2.5.3 26 | azure-mgmt-cognitiveservices==13.6.0 27 | azure-identity==1.17.1 28 | azure-ai-contentsafety==1.0.0 29 | azure-storage-blob==12.24.1 30 | pypdf==5.3.1 31 | python-docx==1.1.2 32 | flask-executor==1.0.0 33 | PyMuPDF==1.25.3 34 | langchain-text-splitters==0.3.7 35 | beautifulsoup4==4.13.3 36 | openpyxl==3.1.5 37 | xlrd==2.0.1 38 | pillow==11.1.0 39 | ffmpeg-binaries-compat==1.0.1 40 | ffmpeg-python==0.2.0 -------------------------------------------------------------------------------- /application/single_app/route_backend_conversations.py: -------------------------------------------------------------------------------- 1 | # route_backend_conversations.py 2 | 3 | from config import * 4 | from functions_authentication import * 5 | from functions_settings import * 6 | 7 | def register_route_backend_conversations(app): 8 | 9 | @app.route('/api/get_messages', methods=['GET']) 10 | @login_required 11 | @user_required 12 | def api_get_messages(): 13 | conversation_id = request.args.get('conversation_id') 14 | user_id = get_current_user_id() 15 | if not user_id: 16 | return jsonify({'error': 'User not authenticated'}), 401 17 | if not conversation_id: 18 | return jsonify({'error': 'No conversation_id provided'}), 400 19 | try: 20 | conversation_item = cosmos_conversations_container.read_item( 21 | item=conversation_id, 22 | partition_key=conversation_id 23 | ) 24 | # Then query the messages in cosmos_messages_container 25 | message_query = f"SELECT * FROM c WHERE c.conversation_id = '{conversation_id}' ORDER BY c.timestamp ASC" 26 | messages = list(cosmos_messages_container.query_items( 27 | query=message_query, 28 | partition_key=conversation_id 29 | )) 30 | return jsonify({'messages': messages}) 31 | except CosmosResourceNotFoundError: 32 | return jsonify({'messages': []}) 33 | except Exception: 34 | return jsonify({'error': 'Conversation not found'}), 404 35 | 36 | @app.route('/api/get_conversations', methods=['GET']) 37 | @login_required 38 | @user_required 39 | def get_conversations(): 40 | user_id = get_current_user_id() 41 | if not user_id: 42 | return jsonify({'error': 'User not authenticated'}), 401 43 | query = f"SELECT * FROM c WHERE c.user_id = '{user_id}' ORDER BY c.last_updated DESC" 44 | items = list(cosmos_conversations_container.query_items(query=query, enable_cross_partition_query=True)) 45 | return jsonify({ 46 | 'conversations': items 47 | }), 200 48 | 49 | 50 | @app.route('/api/create_conversation', methods=['POST']) 51 | @login_required 52 | @user_required 53 | def create_conversation(): 54 | user_id = get_current_user_id() 55 | if not user_id: 56 | return jsonify({'error': 'User not authenticated'}), 401 57 | 58 | conversation_id = str(uuid.uuid4()) 59 | conversation_item = { 60 | 'id': conversation_id, 61 | 'user_id': user_id, 62 | 'last_updated': datetime.utcnow().isoformat(), 63 | 'title': 'New Conversation' 64 | } 65 | cosmos_conversations_container.upsert_item(conversation_item) 66 | 67 | return jsonify({ 68 | 'conversation_id': conversation_id, 69 | 'title': 'New Conversation' 70 | }), 200 71 | 72 | @app.route('/api/conversations/', methods=['PUT']) 73 | @login_required 74 | @user_required 75 | def update_conversation_title(conversation_id): 76 | user_id = get_current_user_id() 77 | if not user_id: 78 | return jsonify({'error': 'User not authenticated'}), 401 79 | 80 | # Parse the new title from the request body 81 | data = request.get_json() 82 | new_title = data.get('title', '').strip() 83 | if not new_title: 84 | return jsonify({'error': 'Title is required'}), 400 85 | 86 | try: 87 | # Retrieve the conversation 88 | conversation_item = cosmos_conversations_container.read_item( 89 | item=conversation_id, 90 | partition_key=conversation_id 91 | ) 92 | 93 | # Ensure that the conversation belongs to the current user 94 | if conversation_item.get('user_id') != user_id: 95 | return jsonify({'error': 'Forbidden'}), 403 96 | 97 | # Update the title 98 | conversation_item['title'] = new_title 99 | 100 | # Optionally update the last_updated time 101 | from datetime import datetime 102 | conversation_item['last_updated'] = datetime.utcnow().isoformat() 103 | 104 | # Write back to Cosmos DB 105 | cosmos_conversations_container.upsert_item(conversation_item) 106 | 107 | return jsonify({ 108 | 'message': 'Conversation updated', 109 | 'title': new_title, 110 | 'classification': conversation_item.get('classification', []) # Send classifications if any 111 | }), 200 112 | except Exception as e: 113 | print(e) 114 | return jsonify({'error': 'Failed to update conversation'}), 500 115 | 116 | @app.route('/api/conversations/', methods=['DELETE']) 117 | @login_required 118 | @user_required 119 | def delete_conversation(conversation_id): 120 | """ 121 | Delete a conversation. If archiving is enabled, copy it to archived_conversations first. 122 | """ 123 | settings = get_settings() 124 | archiving_enabled = settings.get('enable_conversation_archiving', False) 125 | 126 | try: 127 | conversation_item = cosmos_conversations_container.read_item( 128 | item=conversation_id, 129 | partition_key=conversation_id 130 | ) 131 | except CosmosResourceNotFoundError: 132 | return jsonify({ 133 | "error": f"Conversation {conversation_id} not found." 134 | }), 404 135 | except Exception as e: 136 | return jsonify({ 137 | "error": str(e) 138 | }), 500 139 | 140 | if archiving_enabled: 141 | archived_item = dict(conversation_item) 142 | archived_item["archived_at"] = datetime.utcnow().isoformat() 143 | cosmos_archived_conversations_container.upsert_item(archived_item) 144 | 145 | message_query = f"SELECT * FROM c WHERE c.conversation_id = '{conversation_id}'" 146 | results = list(cosmos_messages_container.query_items( 147 | query=message_query, 148 | partition_key=conversation_id 149 | )) 150 | 151 | for doc in results: 152 | if archiving_enabled: 153 | archived_doc = dict(doc) 154 | archived_doc["archived_at"] = datetime.utcnow().isoformat() 155 | cosmos_archived_messages_container.upsert_item(archived_doc) 156 | 157 | cosmos_messages_container.delete_item(doc['id'], partition_key=conversation_id) 158 | 159 | try: 160 | cosmos_conversations_container.delete_item( 161 | item=conversation_id, 162 | partition_key=conversation_id 163 | ) 164 | except Exception as e: 165 | return jsonify({ 166 | "error": str(e) 167 | }), 500 168 | 169 | return jsonify({ 170 | "success": True 171 | }), 200 -------------------------------------------------------------------------------- /application/single_app/route_backend_group_prompts.py: -------------------------------------------------------------------------------- 1 | # route_backend_group_prompts.py 2 | 3 | from config import * 4 | from functions_authentication import * 5 | from functions_settings import * 6 | from functions_prompts import * 7 | 8 | def register_route_backend_group_prompts(app): 9 | @app.route('/api/group_prompts', methods=['GET']) 10 | @login_required 11 | @user_required 12 | @enabled_required("enable_group_workspaces") 13 | def get_group_prompts(): 14 | user_id = get_current_user_id() 15 | active_group = get_user_settings(user_id)["settings"].get("activeGroupOid") 16 | if not active_group: 17 | return jsonify({"error":"No active group selected"}), 400 18 | 19 | try: 20 | items, total, page, page_size = list_prompts( 21 | user_id=user_id, 22 | prompt_type="group_prompt", 23 | args=request.args, 24 | group_id=active_group 25 | ) 26 | return jsonify({ 27 | "prompts": items, 28 | "page": page, 29 | "page_size": page_size, 30 | "total_count": total 31 | }), 200 32 | except Exception as e: 33 | app.logger.error(f"Error fetching group prompts: {e}") 34 | return jsonify({"error":"An unexpected error occurred"}), 500 35 | 36 | @app.route('/api/group_prompts', methods=['POST']) 37 | @login_required 38 | @user_required 39 | @enabled_required("enable_group_workspaces") 40 | def create_group_prompt(): 41 | user_id = get_current_user_id() 42 | active_group = get_user_settings(user_id)["settings"].get("activeGroupOid") 43 | if not active_group: 44 | return jsonify({"error":"No active group selected"}), 400 45 | 46 | data = request.get_json() or {} 47 | name = data.get("name","").strip() 48 | content = data.get("content","") 49 | if not name or not content: 50 | return jsonify({"error":"Missing 'name' or 'content'"}), 400 51 | 52 | try: 53 | result = create_prompt_doc( 54 | name=name, 55 | content=content, 56 | prompt_type="group_prompt", 57 | user_id=user_id, 58 | group_id=active_group 59 | ) 60 | return jsonify(result), 201 61 | except Exception as e: 62 | app.logger.error(f"Error creating group prompt: {e}") 63 | return jsonify({"error":"An unexpected error occurred"}), 500 64 | 65 | @app.route('/api/group_prompts/', methods=['GET']) 66 | @login_required 67 | @user_required 68 | @enabled_required("enable_group_workspaces") 69 | def get_group_prompt(prompt_id): 70 | user_id = get_current_user_id() 71 | active_group = get_user_settings(user_id)["settings"].get("activeGroupOid") 72 | if not active_group: 73 | return jsonify({"error":"No active group selected"}), 400 74 | 75 | try: 76 | item = get_prompt_doc( 77 | user_id=user_id, 78 | prompt_id=prompt_id, 79 | prompt_type="group_prompt", 80 | group_id=active_group 81 | ) 82 | if not item: 83 | return jsonify({"error":"Prompt not found or access denied"}), 404 84 | return jsonify(item), 200 85 | except Exception as e: 86 | app.logger.error(f"Unexpected error getting group prompt {prompt_id}: {e}") 87 | return jsonify({"error":"An unexpected error occurred"}), 500 88 | 89 | @app.route('/api/group_prompts/', methods=['PATCH']) 90 | @login_required 91 | @user_required 92 | @enabled_required("enable_group_workspaces") 93 | def update_group_prompt(prompt_id): 94 | user_id = get_current_user_id() 95 | active_group = get_user_settings(user_id)["settings"].get("activeGroupOid") 96 | if not active_group: 97 | return jsonify({"error":"No active group selected"}), 400 98 | 99 | data = request.get_json() or {} 100 | updates = {} 101 | if "name" in data: 102 | if not isinstance(data["name"], str) or not data["name"].strip(): 103 | return jsonify({"error":"Invalid 'name' provided"}), 400 104 | updates["name"] = data["name"].strip() 105 | if "content" in data: 106 | if not isinstance(data["content"], str): 107 | return jsonify({"error":"Invalid 'content' provided"}), 400 108 | updates["content"] = data["content"] 109 | if not updates: 110 | return jsonify({"error":"No fields provided for update"}), 400 111 | 112 | try: 113 | result = update_prompt_doc( 114 | user_id=user_id, 115 | prompt_id=prompt_id, 116 | prompt_type="group_prompt", 117 | updates=updates, 118 | group_id=active_group 119 | ) 120 | if not result: 121 | return jsonify({"error":"Prompt not found or access denied"}), 404 122 | return jsonify(result), 200 123 | except Exception as e: 124 | app.logger.error(f"Unexpected error updating group prompt {prompt_id}: {e}") 125 | return jsonify({"error":"An unexpected error occurred"}), 500 126 | 127 | @app.route('/api/group_prompts/', methods=['DELETE']) 128 | @login_required 129 | @user_required 130 | @enabled_required("enable_group_workspaces") 131 | def delete_group_prompt(prompt_id): 132 | user_id = get_current_user_id() 133 | active_group = get_user_settings(user_id)["settings"].get("activeGroupOid") 134 | if not active_group: 135 | return jsonify({"error":"No active group selected"}), 400 136 | 137 | try: 138 | success = delete_prompt_doc( 139 | user_id=user_id, 140 | prompt_id=prompt_id, 141 | group_id=active_group 142 | ) 143 | if not success: 144 | return jsonify({"error":"Prompt not found or access denied"}), 404 145 | return jsonify({"message":"Prompt deleted successfully"}), 200 146 | except Exception as e: 147 | app.logger.error(f"Unexpected error deleting group prompt {prompt_id}: {e}") 148 | return jsonify({"error":"An unexpected error occurred"}), 500 149 | -------------------------------------------------------------------------------- /application/single_app/route_backend_models.py: -------------------------------------------------------------------------------- 1 | # route_backend_models.py 2 | 3 | from config import * 4 | from functions_authentication import * 5 | from functions_settings import * 6 | 7 | 8 | def register_route_backend_models(app): 9 | """ 10 | Register backend routes for fetching Azure OpenAI models. 11 | """ 12 | 13 | @app.route('/api/models/gpt', methods=['GET']) 14 | @login_required 15 | @user_required 16 | def get_gpt_models(): 17 | """ 18 | Fetch GPT-like deployments using Azure Mgmt library. 19 | """ 20 | settings = get_settings() 21 | 22 | subscription_id = settings.get('azure_openai_gpt_subscription_id', '') 23 | resource_group = settings.get('azure_openai_gpt_resource_group', '') 24 | account_name = settings.get('azure_openai_gpt_endpoint', '').split('.')[0].replace("https://", "") 25 | 26 | if not subscription_id or not resource_group or not account_name: 27 | return jsonify({"error": "Azure GPT Model subscription/RG/endpoint not configured"}), 400 28 | 29 | if AZURE_ENVIRONMENT == "usgovernment": 30 | 31 | credential = ClientSecretCredential(TENANT_ID, CLIENT_ID, MICROSOFT_PROVIDER_AUTHENTICATION_SECRET, authority=authority) 32 | 33 | client = CognitiveServicesManagementClient( 34 | credential=credential, 35 | subscription_id=subscription_id, 36 | base_url=resource_manager, 37 | credential_scopes=credential_scopes 38 | ) 39 | else: 40 | credential = ClientSecretCredential(TENANT_ID, CLIENT_ID, MICROSOFT_PROVIDER_AUTHENTICATION_SECRET) 41 | 42 | client = CognitiveServicesManagementClient( 43 | credential=credential, 44 | subscription_id=subscription_id 45 | ) 46 | 47 | models = [] 48 | try: 49 | deployments = client.deployments.list( 50 | resource_group_name=resource_group, 51 | account_name=account_name 52 | ) 53 | 54 | for d in deployments: 55 | model_name = d.properties.model.name 56 | if model_name and ( 57 | "gpt" in model_name.lower() or 58 | "o1" in model_name.lower() or 59 | "o3" in model_name.lower() 60 | ): 61 | models.append({ 62 | "deploymentName": d.name, 63 | "modelName": model_name 64 | }) 65 | 66 | except Exception as e: 67 | return jsonify({"error": str(e)}), 500 68 | 69 | return jsonify({"models": models}) 70 | 71 | 72 | @app.route('/api/models/embedding', methods=['GET']) 73 | @login_required 74 | @user_required 75 | def get_embedding_models(): 76 | """ 77 | Fetch Embedding-like deployments using Azure Mgmt library. 78 | """ 79 | settings = get_settings() 80 | 81 | subscription_id = settings.get('azure_openai_embedding_subscription_id', '') 82 | resource_group = settings.get('azure_openai_embedding_resource_group', '') 83 | account_name = settings.get('azure_openai_embedding_endpoint', '').split('.')[0].replace("https://", "") 84 | 85 | if not subscription_id or not resource_group or not account_name: 86 | return jsonify({"error": "Azure Embedding Model subscription/RG/endpoint not configured"}), 400 87 | 88 | if AZURE_ENVIRONMENT == "usgovernment": 89 | 90 | credential = ClientSecretCredential(TENANT_ID, CLIENT_ID, MICROSOFT_PROVIDER_AUTHENTICATION_SECRET, authority=authority) 91 | 92 | client = CognitiveServicesManagementClient( 93 | credential=credential, 94 | subscription_id=subscription_id, 95 | base_url=resource_manager, 96 | credential_scopes=credential_scopes 97 | ) 98 | else: 99 | credential = ClientSecretCredential(TENANT_ID, CLIENT_ID, MICROSOFT_PROVIDER_AUTHENTICATION_SECRET) 100 | 101 | client = CognitiveServicesManagementClient( 102 | credential=credential, 103 | subscription_id=subscription_id 104 | ) 105 | 106 | models = [] 107 | try: 108 | deployments = client.deployments.list( 109 | resource_group_name=resource_group, 110 | account_name=account_name 111 | ) 112 | for d in deployments: 113 | model_name = d.properties.model.name 114 | if model_name and ( 115 | "embedding" in model_name.lower() or 116 | "ada" in model_name.lower() 117 | ): 118 | models.append({ 119 | "deploymentName": d.name, 120 | "modelName": model_name 121 | }) 122 | except Exception as e: 123 | return jsonify({"error": str(e)}), 500 124 | 125 | return jsonify({"models": models}) 126 | 127 | 128 | @app.route('/api/models/image', methods=['GET']) 129 | @login_required 130 | @user_required 131 | def get_image_models(): 132 | """ 133 | Fetch DALL-E-like image-generation deployments using Azure Mgmt library. 134 | """ 135 | settings = get_settings() 136 | 137 | subscription_id = settings.get('azure_openai_image_gen_subscription_id', '') 138 | resource_group = settings.get('azure_openai_image_gen_resource_group', '') 139 | account_name = settings.get('azure_openai_image_gen_endpoint', '').split('.')[0].replace("https://", "") 140 | 141 | if not subscription_id or not resource_group or not account_name: 142 | return jsonify({"error": "Azure Image Model subscription/RG/endpoint not configured"}), 400 143 | 144 | if AZURE_ENVIRONMENT == "usgovernment": 145 | 146 | credential = ClientSecretCredential(TENANT_ID, CLIENT_ID, MICROSOFT_PROVIDER_AUTHENTICATION_SECRET, authority=authority) 147 | 148 | client = CognitiveServicesManagementClient( 149 | credential=credential, 150 | subscription_id=subscription_id, 151 | base_url=resource_manager, 152 | credential_scopes=credential_scopes 153 | ) 154 | else: 155 | credential = ClientSecretCredential(TENANT_ID, CLIENT_ID, MICROSOFT_PROVIDER_AUTHENTICATION_SECRET) 156 | 157 | client = CognitiveServicesManagementClient( 158 | credential=credential, 159 | subscription_id=subscription_id 160 | ) 161 | 162 | models = [] 163 | try: 164 | deployments = client.deployments.list( 165 | resource_group_name=resource_group, 166 | account_name=account_name 167 | ) 168 | for d in deployments: 169 | model_name = d.properties.model.name 170 | if model_name and ( 171 | "dall-e" in model_name.lower() 172 | ): 173 | models.append({ 174 | "deploymentName": d.name, 175 | "modelName": model_name 176 | }) 177 | except Exception as e: 178 | return jsonify({"error": str(e)}), 500 179 | 180 | return jsonify({"models": models}) -------------------------------------------------------------------------------- /application/single_app/route_backend_prompts.py: -------------------------------------------------------------------------------- 1 | # route_backend_prompts.py 2 | 3 | from config import * 4 | from functions_authentication import * 5 | from functions_settings import * 6 | from functions_prompts import * 7 | 8 | def register_route_backend_prompts(app): 9 | @app.route('/api/prompts', methods=['GET']) 10 | @login_required 11 | @user_required 12 | @enabled_required("enable_user_workspace") 13 | def get_prompts(): 14 | user_id = get_current_user_id() 15 | try: 16 | items, total, page, page_size = list_prompts( 17 | user_id=user_id, 18 | prompt_type="user_prompt", 19 | args=request.args 20 | ) 21 | return jsonify({ 22 | "prompts": items, 23 | "page": page, 24 | "page_size": page_size, 25 | "total_count": total 26 | }), 200 27 | except Exception as e: 28 | app.logger.error(f"Error fetching prompts: {e}") 29 | return jsonify({"error":"An unexpected error occurred"}), 500 30 | 31 | @app.route('/api/prompts', methods=['POST']) 32 | @login_required 33 | @user_required 34 | @enabled_required("enable_user_workspace") 35 | def create_prompt(): 36 | user_id = get_current_user_id() 37 | data = request.get_json() or {} 38 | name = data.get("name", "").strip() 39 | content = data.get("content", "") 40 | if not name: 41 | return jsonify({"error":"Missing or invalid 'name'"}), 400 42 | if not content: 43 | return jsonify({"error":"Missing or invalid 'content'"}), 400 44 | 45 | try: 46 | result = create_prompt_doc( 47 | name=name, 48 | content=content, 49 | prompt_type="user_prompt", 50 | user_id=user_id 51 | ) 52 | return jsonify(result), 201 53 | except Exception as e: 54 | app.logger.error(f"Error creating prompt: {e}") 55 | return jsonify({"error":"An unexpected error occurred"}), 500 56 | 57 | @app.route('/api/prompts/', methods=['GET']) 58 | @login_required 59 | @user_required 60 | @enabled_required("enable_user_workspace") 61 | def get_prompt(prompt_id): 62 | user_id = get_current_user_id() 63 | try: 64 | item = get_prompt_doc( 65 | user_id=user_id, 66 | prompt_id=prompt_id, 67 | prompt_type="user_prompt" 68 | ) 69 | if not item: 70 | return jsonify({"error":"Prompt not found or access denied"}), 404 71 | return jsonify(item), 200 72 | except Exception as e: 73 | app.logger.error(f"Unexpected error getting prompt {prompt_id}: {e}") 74 | return jsonify({"error": "An unexpected error occurred"}), 500 75 | 76 | @app.route('/api/prompts/', methods=['PATCH']) 77 | @login_required 78 | @user_required 79 | @enabled_required("enable_user_workspace") 80 | def update_prompt(prompt_id): 81 | user_id = get_current_user_id() 82 | data = request.get_json() or {} 83 | updates = {} 84 | if "name" in data: 85 | if not isinstance(data["name"], str) or not data["name"].strip(): 86 | return jsonify({"error":"Invalid 'name' provided"}), 400 87 | updates["name"] = data["name"].strip() 88 | if "content" in data: 89 | if not isinstance(data["content"], str): 90 | return jsonify({"error":"Invalid 'content' provided"}), 400 91 | updates["content"] = data["content"] 92 | if not updates: 93 | return jsonify({"error":"No fields provided for update"}), 400 94 | 95 | try: 96 | result = update_prompt_doc( 97 | user_id=user_id, 98 | prompt_id=prompt_id, 99 | prompt_type="user_prompt", 100 | updates=updates 101 | ) 102 | if not result: 103 | return jsonify({"error":"Prompt not found or access denied"}), 404 104 | return jsonify(result), 200 105 | except Exception as e: 106 | app.logger.error(f"Unexpected error updating prompt {prompt_id}: {e}") 107 | return jsonify({"error":"An unexpected error occurred"}), 500 108 | 109 | @app.route('/api/prompts/', methods=['DELETE']) 110 | @login_required 111 | @user_required 112 | @enabled_required("enable_user_workspace") 113 | def delete_prompt(prompt_id): 114 | user_id = get_current_user_id() 115 | try: 116 | success = delete_prompt_doc( 117 | user_id=user_id, 118 | prompt_id=prompt_id 119 | ) 120 | if not success: 121 | return jsonify({"error":"Prompt not found or access denied"}), 404 122 | return jsonify({"message":"Prompt deleted successfully"}), 200 123 | except Exception as e: 124 | app.logger.error(f"Unexpected error deleting prompt {prompt_id}: {e}") 125 | return jsonify({"error":"An unexpected error occurred"}), 500 126 | -------------------------------------------------------------------------------- /application/single_app/route_backend_users.py: -------------------------------------------------------------------------------- 1 | # route_backend_users.py 2 | 3 | from config import * 4 | from functions_authentication import * 5 | from functions_settings import * 6 | 7 | def register_route_backend_users(app): 8 | """ 9 | This route will expose GET /api/userSearch?query= which calls 10 | Microsoft Graph to find users by displayName, mail, userPrincipalName, etc. 11 | """ 12 | 13 | @app.route("/api/userSearch", methods=["GET"]) 14 | @login_required 15 | @user_required 16 | def api_user_search(): 17 | query = request.args.get("query", "").strip() 18 | if not query: 19 | return jsonify([]), 200 20 | 21 | token = get_valid_access_token() 22 | if not token: 23 | return jsonify({"error": "Could not acquire access token"}), 401 24 | 25 | if AZURE_ENVIRONMENT == "usgovernment" or AZURE_ENVIRONMENT == "secret": 26 | user_endpoint = "https://graph.microsoft.us/v1.0/users" 27 | if AZURE_ENVIRONMENT == "public": 28 | user_endpoint = "https://graph.microsoft.com/v1.0/users" 29 | 30 | headers = { 31 | "Authorization": f"Bearer {token}", 32 | "Content-Type": "application/json" 33 | } 34 | 35 | filter_str = ( 36 | f"startswith(displayName, '{query}') " 37 | f"or startswith(mail, '{query}') " 38 | f"or startswith(userPrincipalName, '{query}')" 39 | ) 40 | params = { 41 | "$filter": filter_str, 42 | "$top": 10, 43 | "$select": "id,displayName,mail,userPrincipalName" 44 | } 45 | 46 | try: 47 | response = requests.get(user_endpoint, headers=headers, params=params) 48 | response.raise_for_status() # Raise an exception for bad status codes (4xx or 5xx) 49 | 50 | user_results = response.json().get("value", []) 51 | results = [] 52 | for user in user_results: 53 | email = user.get("mail") or user.get("userPrincipalName") or "" 54 | results.append({ 55 | "id": user.get("id"), 56 | "displayName": user.get("displayName", "(no name)"), 57 | "email": email 58 | }) 59 | return jsonify(results), 200 60 | 61 | except requests.exceptions.RequestException as e: 62 | print(f"Graph API request failed: {e}") 63 | # Try to get more details from response if available 64 | error_details = "Unknown error" 65 | if e.response is not None: 66 | try: 67 | error_details = e.response.json() 68 | except ValueError: # Handle cases where response is not JSON 69 | error_details = e.response.text 70 | return jsonify({ 71 | "error": "Graph API request failed", 72 | "details": error_details 73 | }), getattr(e.response, 'status_code', 500) # Use response status code if available 74 | 75 | 76 | @app.route('/api/user/settings', methods=['GET', 'POST']) 77 | @login_required 78 | @user_required # Assuming this decorator confirms a valid user exists 79 | def user_settings(): 80 | try: 81 | user_id = get_current_user_id() 82 | if not user_id: # Redundant if get_current_user_id raises error, but safe 83 | return jsonify({"error": "Unable to identify user"}), 401 84 | except ValueError as e: 85 | # Handle case where get_current_user_id fails (e.g., session issue) 86 | print(f"Error getting user ID: {e}") 87 | return jsonify({"error": str(e)}), 401 88 | except Exception as e: 89 | # Catch other potential errors during user ID retrieval 90 | print(f"Unexpected error getting user ID: {e}") 91 | return jsonify({"error": "Internal server error identifying user"}), 500 92 | 93 | 94 | # --- Handle POST Request (Update Settings) --- 95 | if request.method == 'POST': 96 | try: 97 | # Expect JSON data, as sent by the fetch API in chat-layout.js 98 | data = request.get_json() 99 | 100 | if not data: 101 | return jsonify({"error": "Missing JSON body"}), 400 102 | 103 | # The JS sends { settings: { key: value, ... } } 104 | # Extract the inner 'settings' dictionary 105 | settings_to_update = data.get('settings') 106 | 107 | if settings_to_update is None: 108 | # Maybe the client sent the data flat? Handle for flexibility or error out. 109 | # If you want to be strict: 110 | return jsonify({"error": "Request body must contain a 'settings' object"}), 400 111 | # If you want to be flexible (accept flat structure like {"activeGroupOid": "..."}): 112 | # settings_to_update = data 113 | 114 | if not isinstance(settings_to_update, dict): 115 | return jsonify({"error": "'settings' must be an object"}), 400 116 | 117 | # Basic validation could go here (e.g., check allowed keys, value types) 118 | # Example: Allowed keys 119 | allowed_keys = {'activeGroupOid', 'layoutPreference', 'splitSizesPreference', 'dockedSidebarHidden', 'darkModeEnabled'} # Add others as needed 120 | invalid_keys = set(settings_to_update.keys()) - allowed_keys 121 | if invalid_keys: 122 | print(f"Warning: Received invalid settings keys: {invalid_keys}") 123 | # Decide whether to ignore them or return an error 124 | # To ignore: settings_to_update = {k: v for k, v in settings_to_update.items() if k in allowed_keys} 125 | # To error: return jsonify({"error": f"Invalid settings keys provided: {', '.join(invalid_keys)}"}), 400 126 | 127 | 128 | # Call the updated function - it handles merging and timestamp 129 | success = update_user_settings(user_id, settings_to_update) 130 | 131 | if success: 132 | return jsonify({"message": "User settings updated successfully"}), 200 133 | else: 134 | # update_user_settings should ideally log the specific error 135 | return jsonify({"error": "Failed to update settings"}), 500 136 | 137 | except Exception as e: 138 | # Catch potential JSON parsing errors or other unexpected issues 139 | print(f"Error processing POST /api/user/settings: {e}") 140 | return jsonify({"error": "Internal server error processing request"}), 500 141 | 142 | 143 | # --- Handle GET Request (Retrieve Settings) --- 144 | # This part remains largely the same as your original 145 | try: 146 | user_settings_data = get_user_settings(user_id) # This fetches the whole document 147 | # The frontend JS expects the document structure, including the 'settings' key inside it. 148 | return jsonify(user_settings_data), 200 # Return the full document or {} if not found 149 | except Exception as e: 150 | print(f"Error retrieving settings for user {user_id}: {e}") 151 | return jsonify({"error": "Failed to retrieve user settings"}), 500 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /application/single_app/route_frontend_authentication.py: -------------------------------------------------------------------------------- 1 | # route_frontend_authentication.py 2 | 3 | from config import * 4 | from functions_authentication import _build_msal_app, _load_cache, _save_cache 5 | 6 | def register_route_frontend_authentication(app): 7 | @app.route('/login') 8 | def login(): 9 | # Clear potentially stale cache/user info before starting new login 10 | session.pop("user", None) 11 | session.pop("token_cache", None) 12 | 13 | # Use helper to build app (cache not strictly needed here, but consistent) 14 | msal_app = _build_msal_app() 15 | auth_url = msal_app.get_authorization_request_url( 16 | scopes=SCOPE, # Use SCOPE from config (includes offline_access) 17 | redirect_uri=url_for('authorized', _external=True, _scheme='https') # Ensure scheme is https if deployed 18 | ) 19 | print("Redirecting to Azure AD for authentication.") 20 | return redirect(auth_url) 21 | 22 | @app.route('/getAToken') # This is your redirect URI path 23 | def authorized(): 24 | # Check for errors passed back from Azure AD 25 | if request.args.get('error'): 26 | error = request.args.get('error') 27 | error_description = request.args.get('error_description', 'No description provided.') 28 | print(f"Azure AD Login Error: {error} - {error_description}") 29 | return f"Login Error: {error} - {error_description}", 400 # Or render an error page 30 | 31 | code = request.args.get('code') 32 | if not code: 33 | print("Authorization code not found in callback.") 34 | return "Authorization code not found", 400 35 | 36 | # Build MSAL app WITH session cache (will be loaded by _build_msal_app via _load_cache) 37 | msal_app = _build_msal_app(cache=_load_cache()) # Load existing cache 38 | 39 | result = msal_app.acquire_token_by_authorization_code( 40 | code=code, 41 | scopes=SCOPE, # Request the same scopes again 42 | redirect_uri=url_for('authorized', _external=True, _scheme='https') 43 | ) 44 | 45 | if "error" in result: 46 | error_description = result.get("error_description", result.get("error")) 47 | print(f"Token acquisition failure: {error_description}") 48 | return f"Login failure: {error_description}", 500 49 | 50 | # --- Store results --- 51 | # Store user identity info (claims from ID token) 52 | session["user"] = result.get("id_token_claims") 53 | # DO NOT store access/refresh token directly in session anymore 54 | 55 | # --- CRITICAL: Save the entire cache (contains tokens) to session --- 56 | _save_cache(msal_app.token_cache) 57 | 58 | print(f"User {session['user'].get('name')} logged in successfully.") 59 | # Redirect to the originally intended page or home 60 | # You might want to store the original destination in the session during /login 61 | return redirect(url_for('index')) # Or another appropriate page 62 | 63 | @app.route('/logout') 64 | def logout(): 65 | user_name = session.get("user", {}).get("name", "User") 66 | # Get the user's email before clearing the session 67 | user_email = session.get("user", {}).get("preferred_username") or session.get("user", {}).get("email") 68 | # Clear Flask session data 69 | session.clear() 70 | # Redirect user to Azure AD logout endpoint 71 | # MSAL provides a helper for this too, but constructing manually is fine 72 | logout_uri = url_for('index', _external=True, _scheme='https') # Where to land after logout 73 | logout_url = ( 74 | f"{AUTHORITY}/oauth2/v2.0/logout" 75 | f"?post_logout_redirect_uri={quote(logout_uri)}" 76 | ) 77 | # Add logout_hint parameter if we have the user's email 78 | if user_email: 79 | logout_url += f"&logout_hint={quote(user_email)}" 80 | 81 | print(f"{user_name} logged out. Redirecting to Azure AD logout.") 82 | return redirect(logout_url) -------------------------------------------------------------------------------- /application/single_app/route_frontend_conversations.py: -------------------------------------------------------------------------------- 1 | # route_frontend_conversations.py 2 | 3 | from config import * 4 | from functions_authentication import * 5 | 6 | def register_route_frontend_conversations(app): 7 | @app.route('/conversations') 8 | @login_required 9 | @user_required 10 | def conversations(): 11 | user_id = get_current_user_id() 12 | if not user_id: 13 | return redirect(url_for('login')) 14 | 15 | query = f""" 16 | SELECT * 17 | FROM c 18 | WHERE c.user_id = '{user_id}' 19 | ORDER BY c.last_updated DESC 20 | """ 21 | items = list(cosmos_conversations_container.query_items( 22 | query=query, 23 | enable_cross_partition_query=True 24 | )) 25 | return render_template('conversations.html', conversations=items) 26 | 27 | @app.route('/conversation/', methods=['GET']) 28 | @login_required 29 | @user_required 30 | def view_conversation(conversation_id): 31 | user_id = get_current_user_id() 32 | if not user_id: 33 | return redirect(url_for('login')) 34 | try: 35 | conversation_item = cosmos_conversations_container.read_item( 36 | item=conversation_id, 37 | partition_key=conversation_id 38 | ) 39 | except Exception: 40 | return "Conversation not found", 404 41 | 42 | message_query = f""" 43 | SELECT * FROM c 44 | WHERE c.conversation_id = '{conversation_id}' 45 | ORDER BY c.timestamp ASC 46 | """ 47 | messages = list(cosmos_messages_container.query_items( 48 | query=message_query, 49 | partition_key=conversation_id 50 | )) 51 | return render_template('chat.html', conversation_id=conversation_id, messages=messages) 52 | 53 | @app.route('/conversation//messages', methods=['GET']) 54 | @login_required 55 | @user_required 56 | def get_conversation_messages(conversation_id): 57 | user_id = get_current_user_id() 58 | if not user_id: 59 | return jsonify({'error': 'User not authenticated'}), 401 60 | 61 | try: 62 | _ = cosmos_conversations_container.read_item(conversation_id, conversation_id) 63 | except CosmosResourceNotFoundError: 64 | return jsonify({'error': 'Conversation not found'}), 404 65 | 66 | msg_query = f""" 67 | SELECT * FROM c 68 | WHERE c.conversation_id = '{conversation_id}' 69 | ORDER BY c.timestamp ASC 70 | """ 71 | messages = list(cosmos_messages_container.query_items( 72 | query=msg_query, 73 | partition_key=conversation_id 74 | )) 75 | 76 | for m in messages: 77 | if m.get('role') == 'file' and 'file_content' in m: 78 | del m['file_content'] 79 | 80 | return jsonify({'messages': messages}) -------------------------------------------------------------------------------- /application/single_app/route_frontend_feedback.py: -------------------------------------------------------------------------------- 1 | # route_frontend_feedback.py 2 | 3 | from config import * 4 | from functions_authentication import * 5 | from functions_settings import * 6 | 7 | def register_route_frontend_feedback(app): 8 | 9 | @app.route("/admin/feedback_review") 10 | @login_required 11 | @admin_required 12 | @feedback_admin_required 13 | @enabled_required("enable_user_feedback") 14 | def admin_feedback_review(): 15 | """ 16 | Renders the feedback review page (feedback_review.html). 17 | """ 18 | 19 | return render_template("admin_feedback_review.html") 20 | 21 | @app.route("/my_feedback") 22 | @login_required 23 | @user_required 24 | @enabled_required("enable_user_feedback") 25 | def my_feedback(): 26 | """ 27 | Renders the "My Feedback" page for the current user. 28 | """ 29 | 30 | return render_template("my_feedback.html") -------------------------------------------------------------------------------- /application/single_app/route_frontend_group_workspaces.py: -------------------------------------------------------------------------------- 1 | # route_frontend_group_workspaces.py 2 | 3 | from config import * 4 | from functions_authentication import * 5 | from functions_settings import * 6 | 7 | def register_route_frontend_group_workspaces(app): 8 | @app.route('/group_workspaces', methods=['GET']) 9 | @login_required 10 | @user_required 11 | @enabled_required("enable_group_workspaces") 12 | def group_workspaces(): 13 | """Render the Group workspaces page for the current active group.""" 14 | user_id = get_current_user_id() 15 | settings = get_settings() 16 | public_settings = sanitize_settings_for_user(settings) 17 | active_group_id = settings.get("activeGroupOid") 18 | enable_document_classification = settings.get('enable_document_classification', False) 19 | enable_extract_meta_data = settings.get('enable_extract_meta_data', False) 20 | enable_video_file_support = settings.get('enable_video_file_support', False) 21 | enable_audio_file_support = settings.get('enable_audio_file_support', False) 22 | if not user_id: 23 | print("User not authenticated.") 24 | return redirect(url_for('login')) 25 | 26 | query = """ 27 | SELECT VALUE COUNT(1) 28 | FROM c 29 | WHERE c.group_id = @group_id 30 | AND NOT IS_DEFINED(c.percentage_complete) 31 | """ 32 | parameters = [ 33 | {"name": "@group_id", "value": active_group_id} 34 | ] 35 | 36 | legacy_docs_from_cosmos = list( 37 | cosmos_group_documents_container.query_items( 38 | query=query, 39 | parameters=parameters, 40 | enable_cross_partition_query=True 41 | ) 42 | ) 43 | legacy_count = legacy_docs_from_cosmos[0] if legacy_docs_from_cosmos else 0 44 | 45 | return render_template( 46 | 'group_workspaces.html', 47 | settings=public_settings, 48 | enable_document_classification=enable_document_classification, 49 | enable_extract_meta_data=enable_extract_meta_data, 50 | enable_video_file_support=enable_video_file_support, 51 | enable_audio_file_support=enable_audio_file_support, 52 | legacy_docs_count=legacy_count 53 | ) 54 | -------------------------------------------------------------------------------- /application/single_app/route_frontend_groups.py: -------------------------------------------------------------------------------- 1 | # route_frontend_groups.py 2 | 3 | from config import * 4 | from functions_authentication import * 5 | from functions_settings import * 6 | 7 | def register_route_frontend_groups(app): 8 | @app.route("/my_groups", methods=["GET"]) 9 | @login_required 10 | @user_required 11 | @enabled_required("enable_group_workspaces") 12 | def my_groups(): 13 | """ 14 | Renders the My Groups page (templates/my_groups.html). 15 | """ 16 | 17 | return render_template("my_groups.html") 18 | 19 | @app.route("/groups/", methods=["GET"]) 20 | @login_required 21 | @user_required 22 | @enabled_required("enable_group_workspaces") 23 | def manage_group(group_id): 24 | """ 25 | Renders a page or view for managing a single group (not shown in detail here). 26 | Could be a second template like 'manage_group.html'. 27 | """ 28 | 29 | return render_template("manage_group.html", group_id=group_id) 30 | -------------------------------------------------------------------------------- /application/single_app/route_frontend_profile.py: -------------------------------------------------------------------------------- 1 | # route_frontend_profile.py 2 | 3 | from config import * 4 | from functions_authentication import * 5 | 6 | def register_route_frontend_profile(app): 7 | @app.route('/profile') 8 | @login_required 9 | def profile(): 10 | user = session.get('user') 11 | return render_template('profile.html', user=user) -------------------------------------------------------------------------------- /application/single_app/route_frontend_safety.py: -------------------------------------------------------------------------------- 1 | # route_frontend_safety.py 2 | 3 | from config import * 4 | from functions_authentication import * 5 | from functions_settings import * 6 | 7 | def register_route_frontend_safety(app): 8 | 9 | @app.route('/admin/safety_violations', methods=['GET']) 10 | @login_required 11 | @admin_required 12 | @safety_violation_admin_required 13 | @enabled_required("enable_content_safety") 14 | def admin_safety_violations(): 15 | """ 16 | Renders the admin safety violations page (admin_safety_violations.html). 17 | """ 18 | return render_template('admin_safety_violations.html') 19 | 20 | @app.route('/safety_violations', methods=['GET']) 21 | @login_required 22 | @user_required 23 | @enabled_required("enable_content_safety") 24 | def my_safety_violations(): 25 | """ 26 | Displays the logged-in user's safety violations. 27 | """ 28 | return render_template('my_safety_violations.html') -------------------------------------------------------------------------------- /application/single_app/route_frontend_workspace.py: -------------------------------------------------------------------------------- 1 | # route_frontend_workspace.py 2 | 3 | from config import * 4 | from functions_authentication import * 5 | from functions_settings import * 6 | 7 | def register_route_frontend_workspace(app): 8 | @app.route('/workspace', methods=['GET']) 9 | @login_required 10 | @user_required 11 | @enabled_required("enable_user_workspace") 12 | def workspace(): 13 | user_id = get_current_user_id() 14 | settings = get_settings() 15 | public_settings = sanitize_settings_for_user(settings) 16 | enable_document_classification = settings.get('enable_document_classification', False) 17 | enable_extract_meta_data = settings.get('enable_extract_meta_data', False) 18 | enable_video_file_support = settings.get('enable_video_file_support', False) 19 | enable_audio_file_support = settings.get('enable_audio_file_support', False) 20 | if not user_id: 21 | print("User not authenticated.") 22 | return redirect(url_for('login')) 23 | 24 | query = """ 25 | SELECT VALUE COUNT(1) 26 | FROM c 27 | WHERE c.user_id = @user_id 28 | AND NOT IS_DEFINED(c.percentage_complete) 29 | """ 30 | parameters = [ 31 | {"name": "@user_id", "value": user_id} 32 | ] 33 | 34 | legacy_docs_from_cosmos = list( 35 | cosmos_user_documents_container.query_items( 36 | query=query, 37 | parameters=parameters, 38 | enable_cross_partition_query=True 39 | ) 40 | ) 41 | legacy_count = legacy_docs_from_cosmos[0] if legacy_docs_from_cosmos else 0 42 | 43 | return render_template( 44 | 'workspace.html', 45 | settings=public_settings, 46 | enable_document_classification=enable_document_classification, 47 | enable_extract_meta_data=enable_extract_meta_data, 48 | enable_video_file_support=enable_video_file_support, 49 | enable_audio_file_support=enable_audio_file_support, 50 | legacy_docs_count=legacy_count 51 | ) 52 | 53 | -------------------------------------------------------------------------------- /application/single_app/static/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/application/single_app/static/.DS_Store -------------------------------------------------------------------------------- /application/single_app/static/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v5.3.3 (https://getbootstrap.com/) 3 | * Copyright 2011-2024 The Bootstrap Authors 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 5 | */:root,[data-bs-theme=light]{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-primary-text-emphasis:#052c65;--bs-secondary-text-emphasis:#2b2f32;--bs-success-text-emphasis:#0a3622;--bs-info-text-emphasis:#055160;--bs-warning-text-emphasis:#664d03;--bs-danger-text-emphasis:#58151c;--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:#cfe2ff;--bs-secondary-bg-subtle:#e2e3e5;--bs-success-bg-subtle:#d1e7dd;--bs-info-bg-subtle:#cff4fc;--bs-warning-bg-subtle:#fff3cd;--bs-danger-bg-subtle:#f8d7da;--bs-light-bg-subtle:#fcfcfd;--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:#9ec5fe;--bs-secondary-border-subtle:#c4c8cb;--bs-success-border-subtle:#a3cfbb;--bs-info-border-subtle:#9eeaf9;--bs-warning-border-subtle:#ffe69c;--bs-danger-border-subtle:#f1aeb5;--bs-light-border-subtle:#e9ecef;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-color-rgb:33,37,41;--bs-body-bg:#fff;--bs-body-bg-rgb:255,255,255;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb:33,37,41;--bs-secondary-bg:#e9ecef;--bs-secondary-bg-rgb:233,236,239;--bs-tertiary-color:rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb:33,37,41;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248,249,250;--bs-heading-color:inherit;--bs-link-color:#0d6efd;--bs-link-color-rgb:13,110,253;--bs-link-decoration:underline;--bs-link-hover-color:#0a58ca;--bs-link-hover-color-rgb:10,88,202;--bs-code-color:#d63384;--bs-highlight-color:#212529;--bs-highlight-bg:#fff3cd;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(13, 110, 253, 0.25);--bs-form-valid-color:#198754;--bs-form-valid-border-color:#198754;--bs-form-invalid-color:#dc3545;--bs-form-invalid-border-color:#dc3545}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222,226,230;--bs-body-bg:#212529;--bs-body-bg-rgb:33,37,41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222,226,230;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52,58,64;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222,226,230;--bs-tertiary-bg:#2b3035;--bs-tertiary-bg-rgb:43,48,53;--bs-primary-text-emphasis:#6ea8fe;--bs-secondary-text-emphasis:#a7acb1;--bs-success-text-emphasis:#75b798;--bs-info-text-emphasis:#6edff6;--bs-warning-text-emphasis:#ffda6a;--bs-danger-text-emphasis:#ea868f;--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:#031633;--bs-secondary-bg-subtle:#161719;--bs-success-bg-subtle:#051b11;--bs-info-bg-subtle:#032830;--bs-warning-bg-subtle:#332701;--bs-danger-bg-subtle:#2c0b0e;--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:#084298;--bs-secondary-border-subtle:#41464b;--bs-success-border-subtle:#0f5132;--bs-info-border-subtle:#087990;--bs-warning-border-subtle:#997404;--bs-danger-border-subtle:#842029;--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:inherit;--bs-link-color:#6ea8fe;--bs-link-hover-color:#8bb9fe;--bs-link-color-rgb:110,168,254;--bs-link-hover-color-rgb:139,185,254;--bs-code-color:#e685b5;--bs-highlight-color:#dee2e6;--bs-highlight-bg:#664d03;--bs-border-color:#495057;--bs-border-color-translucent:rgba(255, 255, 255, 0.15);--bs-form-valid-color:#75b798;--bs-form-valid-border-color:#75b798;--bs-form-invalid-color:#ea868f;--bs-form-invalid-border-color:#ea868f}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2;color:var(--bs-heading-color)}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:underline}a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-secondary-color);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important} 6 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /application/single_app/static/css/bootstrap-reboot.rtl.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v5.3.3 (https://getbootstrap.com/) 3 | * Copyright 2011-2024 The Bootstrap Authors 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 5 | */:root,[data-bs-theme=light]{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-primary-text-emphasis:#052c65;--bs-secondary-text-emphasis:#2b2f32;--bs-success-text-emphasis:#0a3622;--bs-info-text-emphasis:#055160;--bs-warning-text-emphasis:#664d03;--bs-danger-text-emphasis:#58151c;--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:#cfe2ff;--bs-secondary-bg-subtle:#e2e3e5;--bs-success-bg-subtle:#d1e7dd;--bs-info-bg-subtle:#cff4fc;--bs-warning-bg-subtle:#fff3cd;--bs-danger-bg-subtle:#f8d7da;--bs-light-bg-subtle:#fcfcfd;--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:#9ec5fe;--bs-secondary-border-subtle:#c4c8cb;--bs-success-border-subtle:#a3cfbb;--bs-info-border-subtle:#9eeaf9;--bs-warning-border-subtle:#ffe69c;--bs-danger-border-subtle:#f1aeb5;--bs-light-border-subtle:#e9ecef;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-color-rgb:33,37,41;--bs-body-bg:#fff;--bs-body-bg-rgb:255,255,255;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb:33,37,41;--bs-secondary-bg:#e9ecef;--bs-secondary-bg-rgb:233,236,239;--bs-tertiary-color:rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb:33,37,41;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248,249,250;--bs-heading-color:inherit;--bs-link-color:#0d6efd;--bs-link-color-rgb:13,110,253;--bs-link-decoration:underline;--bs-link-hover-color:#0a58ca;--bs-link-hover-color-rgb:10,88,202;--bs-code-color:#d63384;--bs-highlight-color:#212529;--bs-highlight-bg:#fff3cd;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(13, 110, 253, 0.25);--bs-form-valid-color:#198754;--bs-form-valid-border-color:#198754;--bs-form-invalid-color:#dc3545;--bs-form-invalid-border-color:#dc3545}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222,226,230;--bs-body-bg:#212529;--bs-body-bg-rgb:33,37,41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222,226,230;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52,58,64;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222,226,230;--bs-tertiary-bg:#2b3035;--bs-tertiary-bg-rgb:43,48,53;--bs-primary-text-emphasis:#6ea8fe;--bs-secondary-text-emphasis:#a7acb1;--bs-success-text-emphasis:#75b798;--bs-info-text-emphasis:#6edff6;--bs-warning-text-emphasis:#ffda6a;--bs-danger-text-emphasis:#ea868f;--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:#031633;--bs-secondary-bg-subtle:#161719;--bs-success-bg-subtle:#051b11;--bs-info-bg-subtle:#032830;--bs-warning-bg-subtle:#332701;--bs-danger-bg-subtle:#2c0b0e;--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:#084298;--bs-secondary-border-subtle:#41464b;--bs-success-border-subtle:#0f5132;--bs-info-border-subtle:#087990;--bs-warning-border-subtle:#997404;--bs-danger-border-subtle:#842029;--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:inherit;--bs-link-color:#6ea8fe;--bs-link-hover-color:#8bb9fe;--bs-link-color-rgb:110,168,254;--bs-link-hover-color-rgb:139,185,254;--bs-code-color:#e685b5;--bs-highlight-color:#dee2e6;--bs-highlight-bg:#664d03;--bs-border-color:#495057;--bs-border-color-translucent:rgba(255, 255, 255, 0.15);--bs-form-valid-color:#75b798;--bs-form-valid-border-color:#75b798;--bs-form-invalid-color:#ea868f;--bs-form-invalid-border-color:#ea868f}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2;color:var(--bs-heading-color)}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-right:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-right:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:underline}a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-secondary-color);text-align:right}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:right;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:right}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=email],[type=number],[type=tel],[type=url]{direction:ltr}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important} 6 | /*# sourceMappingURL=bootstrap-reboot.rtl.min.css.map */ -------------------------------------------------------------------------------- /application/single_app/static/css/simplemde.css: -------------------------------------------------------------------------------- 1 | .CodeMirror { 2 | height: auto; 3 | min-height: 300px; 4 | border: 1px solid #ddd; 5 | border-bottom-left-radius: 4px; 6 | border-bottom-right-radius: 4px; 7 | padding: 10px; 8 | font: inherit; 9 | z-index: 1; 10 | } 11 | 12 | .CodeMirror-scroll { 13 | min-height: 300px 14 | } 15 | 16 | .CodeMirror-fullscreen { 17 | background: #fff; 18 | position: fixed !important; 19 | top: 50px; 20 | left: 0; 21 | right: 0; 22 | bottom: 0; 23 | height: auto; 24 | z-index: 9; 25 | } 26 | 27 | .CodeMirror-sided { 28 | width: 50% !important; 29 | } 30 | 31 | .editor-toolbar { 32 | position: relative; 33 | opacity: .6; 34 | -webkit-user-select: none; 35 | -moz-user-select: none; 36 | -ms-user-select: none; 37 | -o-user-select: none; 38 | user-select: none; 39 | padding: 0 10px; 40 | border-top: 1px solid #bbb; 41 | border-left: 1px solid #bbb; 42 | border-right: 1px solid #bbb; 43 | border-top-left-radius: 4px; 44 | border-top-right-radius: 4px; 45 | } 46 | 47 | .editor-toolbar:after, 48 | .editor-toolbar:before { 49 | display: block; 50 | content: ' '; 51 | height: 1px; 52 | } 53 | 54 | .editor-toolbar:before { 55 | margin-bottom: 8px 56 | } 57 | 58 | .editor-toolbar:after { 59 | margin-top: 8px 60 | } 61 | 62 | .editor-toolbar:hover, 63 | .editor-wrapper input.title:focus, 64 | .editor-wrapper input.title:hover { 65 | opacity: .8 66 | } 67 | 68 | .editor-toolbar.fullscreen { 69 | width: 100%; 70 | height: 50px; 71 | overflow-x: auto; 72 | overflow-y: hidden; 73 | white-space: nowrap; 74 | padding-top: 10px; 75 | padding-bottom: 10px; 76 | box-sizing: border-box; 77 | background: #fff; 78 | border: 0; 79 | position: fixed; 80 | top: 0; 81 | left: 0; 82 | opacity: 1; 83 | z-index: 9; 84 | } 85 | 86 | .editor-toolbar.fullscreen::before { 87 | width: 20px; 88 | height: 50px; 89 | background: -moz-linear-gradient(left, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%); 90 | background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 1)), color-stop(100%, rgba(255, 255, 255, 0))); 91 | background: -webkit-linear-gradient(left, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%); 92 | background: -o-linear-gradient(left, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%); 93 | background: -ms-linear-gradient(left, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%); 94 | background: linear-gradient(to right, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%); 95 | position: fixed; 96 | top: 0; 97 | left: 0; 98 | margin: 0; 99 | padding: 0; 100 | } 101 | 102 | .editor-toolbar.fullscreen::after { 103 | width: 20px; 104 | height: 50px; 105 | background: -moz-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%); 106 | background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(100%, rgba(255, 255, 255, 1))); 107 | background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%); 108 | background: -o-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%); 109 | background: -ms-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%); 110 | background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%); 111 | position: fixed; 112 | top: 0; 113 | right: 0; 114 | margin: 0; 115 | padding: 0; 116 | } 117 | 118 | .editor-toolbar a { 119 | display: inline-block; 120 | text-align: center; 121 | text-decoration: none!important; 122 | color: #2c3e50!important; 123 | width: 30px; 124 | height: 30px; 125 | margin: 0; 126 | border: 1px solid transparent; 127 | border-radius: 3px; 128 | cursor: pointer; 129 | } 130 | 131 | .editor-toolbar a.active, 132 | .editor-toolbar a:hover { 133 | background: #fcfcfc; 134 | border-color: #95a5a6; 135 | } 136 | 137 | .editor-toolbar a:before { 138 | line-height: 30px 139 | } 140 | 141 | .editor-toolbar i.separator { 142 | display: inline-block; 143 | width: 0; 144 | border-left: 1px solid #d9d9d9; 145 | border-right: 1px solid #fff; 146 | color: transparent; 147 | text-indent: -10px; 148 | margin: 0 6px; 149 | } 150 | 151 | .editor-toolbar a.fa-header-x:after { 152 | font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; 153 | font-size: 65%; 154 | vertical-align: text-bottom; 155 | position: relative; 156 | top: 2px; 157 | } 158 | 159 | .editor-toolbar a.fa-header-1:after { 160 | content: "1"; 161 | } 162 | 163 | .editor-toolbar a.fa-header-2:after { 164 | content: "2"; 165 | } 166 | 167 | .editor-toolbar a.fa-header-3:after { 168 | content: "3"; 169 | } 170 | 171 | .editor-toolbar a.fa-header-bigger:after { 172 | content: "▲"; 173 | } 174 | 175 | .editor-toolbar a.fa-header-smaller:after { 176 | content: "▼"; 177 | } 178 | 179 | .editor-toolbar.disabled-for-preview a:not(.no-disable) { 180 | pointer-events: none; 181 | background: #fff; 182 | border-color: transparent; 183 | text-shadow: inherit; 184 | } 185 | 186 | @media only screen and (max-width: 700px) { 187 | .editor-toolbar a.no-mobile { 188 | display: none; 189 | } 190 | } 191 | 192 | .editor-statusbar { 193 | padding: 8px 10px; 194 | font-size: 12px; 195 | color: #959694; 196 | text-align: right; 197 | } 198 | 199 | .editor-statusbar span { 200 | display: inline-block; 201 | min-width: 4em; 202 | margin-left: 1em; 203 | } 204 | 205 | .editor-statusbar .lines:before { 206 | content: 'lines: ' 207 | } 208 | 209 | .editor-statusbar .words:before { 210 | content: 'words: ' 211 | } 212 | 213 | .editor-statusbar .characters:before { 214 | content: 'characters: ' 215 | } 216 | 217 | .editor-preview { 218 | padding: 10px; 219 | position: absolute; 220 | width: 100%; 221 | height: 100%; 222 | top: 0; 223 | left: 0; 224 | background: #fafafa; 225 | z-index: 7; 226 | overflow: auto; 227 | display: none; 228 | box-sizing: border-box; 229 | } 230 | 231 | .editor-preview-side { 232 | padding: 10px; 233 | position: fixed; 234 | bottom: 0; 235 | width: 50%; 236 | top: 50px; 237 | right: 0; 238 | background: #fafafa; 239 | z-index: 9; 240 | overflow: auto; 241 | display: none; 242 | box-sizing: border-box; 243 | border: 1px solid #ddd; 244 | } 245 | 246 | .editor-preview-active-side { 247 | display: block 248 | } 249 | 250 | .editor-preview-active { 251 | display: block 252 | } 253 | 254 | .editor-preview>p, 255 | .editor-preview-side>p { 256 | margin-top: 0 257 | } 258 | 259 | .editor-preview pre, 260 | .editor-preview-side pre { 261 | background: #eee; 262 | margin-bottom: 10px; 263 | } 264 | 265 | .editor-preview table td, 266 | .editor-preview table th, 267 | .editor-preview-side table td, 268 | .editor-preview-side table th { 269 | border: 1px solid #ddd; 270 | padding: 5px; 271 | } 272 | 273 | .CodeMirror .CodeMirror-code .cm-tag { 274 | color: #63a35c; 275 | } 276 | 277 | .CodeMirror .CodeMirror-code .cm-attribute { 278 | color: #795da3; 279 | } 280 | 281 | .CodeMirror .CodeMirror-code .cm-string { 282 | color: #183691; 283 | } 284 | 285 | .CodeMirror .CodeMirror-selected { 286 | background: #d9d9d9; 287 | } 288 | 289 | .CodeMirror .CodeMirror-code .cm-header-1 { 290 | font-size: 200%; 291 | line-height: 200%; 292 | } 293 | 294 | .CodeMirror .CodeMirror-code .cm-header-2 { 295 | font-size: 160%; 296 | line-height: 160%; 297 | } 298 | 299 | .CodeMirror .CodeMirror-code .cm-header-3 { 300 | font-size: 125%; 301 | line-height: 125%; 302 | } 303 | 304 | .CodeMirror .CodeMirror-code .cm-header-4 { 305 | font-size: 110%; 306 | line-height: 110%; 307 | } 308 | 309 | .CodeMirror .CodeMirror-code .cm-comment { 310 | background: rgba(0, 0, 0, .05); 311 | border-radius: 2px; 312 | } 313 | 314 | .CodeMirror .CodeMirror-code .cm-link { 315 | color: #7f8c8d; 316 | } 317 | 318 | .CodeMirror .CodeMirror-code .cm-url { 319 | color: #aab2b3; 320 | } 321 | 322 | .CodeMirror .CodeMirror-code .cm-strikethrough { 323 | text-decoration: line-through; 324 | } 325 | 326 | .CodeMirror .CodeMirror-placeholder { 327 | opacity: .5; 328 | } -------------------------------------------------------------------------------- /application/single_app/static/css/styles.css: -------------------------------------------------------------------------------- 1 | /* css/styles.css */ 2 | 3 | body { 4 | font-family: Arial, sans-serif; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | header { 10 | background-color: #0073e6; 11 | color: white; 12 | padding: 10px 20px; 13 | } 14 | 15 | h1 { 16 | margin: 0; 17 | } 18 | 19 | main { 20 | padding: 20px; 21 | } 22 | 23 | /* Dark mode styles */ 24 | [data-bs-theme="dark"] { 25 | --bs-body-bg: #212529; 26 | --bs-body-color: #e9ecef; 27 | } 28 | 29 | [data-bs-theme="dark"] .navbar-light { 30 | background-color: #343a40 !important; 31 | } 32 | 33 | [data-bs-theme="dark"] .navbar-light .navbar-brand, 34 | [data-bs-theme="dark"] .navbar-light .nav-link { 35 | color: #e9ecef; 36 | } 37 | 38 | [data-bs-theme="dark"] .card { 39 | background-color: #343a40; 40 | color: #e9ecef; 41 | } 42 | 43 | [data-bs-theme="dark"] .text-muted { 44 | color: #adb5bd !important; 45 | } 46 | 47 | [data-bs-theme="dark"] .modal-body::-webkit-scrollbar-track { 48 | background: #343a40; 49 | } 50 | 51 | [data-bs-theme="dark"] .modal-content { 52 | background-color: #212529; 53 | color: #e9ecef; 54 | } 55 | 56 | /* Fix for workspace file metadata in dark mode */ 57 | [data-bs-theme="dark"] .bg-light { 58 | background-color: #343a40 !important; 59 | color: #e9ecef !important; 60 | } 61 | 62 | /* Fix for group dropdown in dark mode */ 63 | [data-bs-theme="dark"] #group-dropdown .group-search-container { 64 | background-color: #343a40 !important; 65 | border-bottom: 1px solid #495057 !important; 66 | } 67 | 68 | [data-bs-theme="dark"] #group-dropdown .dropdown-item:hover { 69 | background-color: #495057 !important; 70 | color: #e9ecef !important; 71 | } 72 | 73 | [data-bs-theme="dark"] #group-dropdown .dropdown-menu { 74 | background-color: #343a40 !important; 75 | color: #e9ecef !important; 76 | border-color: #495057 !important; 77 | } 78 | 79 | /* Fix for table headers in document classification in dark mode */ 80 | [data-bs-theme="dark"] .table-light, 81 | [data-bs-theme="dark"] thead.table-light, 82 | [data-bs-theme="dark"] .table-light>th, 83 | [data-bs-theme="dark"] .table-light>td { 84 | background-color: #343a40 !important; 85 | color: #e9ecef !important; 86 | border-color: #495057 !important; 87 | } 88 | 89 | /* Fix for conversation options button in dark mode */ 90 | [data-bs-theme="dark"] .btn-light { 91 | background-color: #343a40 !important; 92 | color: #e9ecef !important; 93 | border-color: #495057 !important; 94 | } 95 | 96 | [data-bs-theme="dark"] .btn-light:hover { 97 | background-color: #495057 !important; 98 | border-color: #6c757d !important; 99 | } 100 | 101 | /* Dark mode toggle styles */ 102 | .dark-mode-toggle { 103 | cursor: pointer; 104 | padding: 0.25rem; 105 | display: flex; 106 | align-items: center; 107 | justify-content: center; 108 | height: 100%; 109 | } 110 | 111 | .dark-mode-toggle:hover { 112 | opacity: 0.8; 113 | } 114 | 115 | /* Ensure nav-link containing dark mode toggle is aligned with other nav links */ 116 | .nav-item .nav-link.dark-mode-toggle { 117 | display: flex; 118 | align-items: center; 119 | height: 100%; 120 | padding-top: 0; 121 | padding-bottom: 0; 122 | } 123 | -------------------------------------------------------------------------------- /application/single_app/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/application/single_app/static/favicon.ico -------------------------------------------------------------------------------- /application/single_app/static/fonts/bootstrap-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/application/single_app/static/fonts/bootstrap-icons.woff -------------------------------------------------------------------------------- /application/single_app/static/fonts/bootstrap-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/application/single_app/static/fonts/bootstrap-icons.woff2 -------------------------------------------------------------------------------- /application/single_app/static/images/ai-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/application/single_app/static/images/ai-avatar.png -------------------------------------------------------------------------------- /application/single_app/static/images/alert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/application/single_app/static/images/alert.png -------------------------------------------------------------------------------- /application/single_app/static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/application/single_app/static/images/logo.png -------------------------------------------------------------------------------- /application/single_app/static/images/user-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/application/single_app/static/images/user-avatar.png -------------------------------------------------------------------------------- /application/single_app/static/js/chat/chat-feedback.js: -------------------------------------------------------------------------------- 1 | // chat-feedback.js 2 | 3 | import { showToast } from "./chat-toast.js"; 4 | import { toBoolean } from "./chat-utils.js"; 5 | 6 | const feedbackForm = document.getElementById("feedback-form"); 7 | 8 | export function renderFeedbackIcons(messageId, conversationId) { 9 | if (toBoolean(window.enableUserFeedback)) { 10 | return ` 11 | 23 | `; 24 | } 25 | else { 26 | return ""; 27 | } 28 | } 29 | 30 | export function submitFeedback(messageId, conversationId, feedbackType, reason) { 31 | fetch("/feedback/submit", { 32 | method: "POST", 33 | headers: { "Content-Type": "application/json" }, 34 | body: JSON.stringify({ 35 | messageId, 36 | conversationId, 37 | feedbackType, 38 | reason 39 | }), 40 | }) 41 | .then((resp) => resp.json()) 42 | .then((data) => { 43 | if (data.success) { 44 | console.log("Feedback submitted:", data); 45 | } else { 46 | console.error("Feedback error:", data.error || data); 47 | showToast("Error submitting feedback: " + (data.error || "Unknown error."), "danger"); 48 | } 49 | }) 50 | .catch((err) => { 51 | console.error("Error sending feedback:", err); 52 | showToast("Error sending feedback.", "danger"); 53 | }); 54 | } 55 | 56 | document.addEventListener("click", function (event) { 57 | const feedbackBtn = event.target.closest(".feedback-btn"); 58 | if (!feedbackBtn) return; 59 | 60 | const feedbackType = feedbackBtn.getAttribute("data-feedback-type"); 61 | const messageId = feedbackBtn.closest(".feedback-icons").getAttribute("data-ai-message-id"); 62 | const conversationId = feedbackBtn.getAttribute("data-conversation-id"); 63 | 64 | feedbackBtn.classList.add("clicked"); 65 | 66 | if (feedbackType === "positive") { 67 | submitFeedback(messageId, conversationId, "positive", ""); 68 | 69 | setTimeout(() => { 70 | feedbackBtn.classList.remove("clicked"); 71 | }, 500); 72 | } else { 73 | const modalEl = new bootstrap.Modal(document.getElementById("feedback-modal")); 74 | document.getElementById("feedback-ai-response-id").value = messageId; 75 | document.getElementById("feedback-conversation-id").value = conversationId; 76 | document.getElementById("feedback-type").value = "negative"; 77 | document.getElementById("feedback-reason").value = ""; 78 | modalEl.show(); 79 | } 80 | }); 81 | 82 | if (feedbackForm) { 83 | feedbackForm.addEventListener("submit", (e) => { 84 | e.preventDefault(); 85 | const messageId = document.getElementById("feedback-ai-response-id").value; 86 | const conversationId = document.getElementById("feedback-conversation-id").value; 87 | const feedbackType = document.getElementById("feedback-type").value; 88 | const reason = document.getElementById("feedback-reason").value.trim(); 89 | 90 | submitFeedback(messageId, conversationId, feedbackType, reason); 91 | 92 | const modalEl = bootstrap.Modal.getInstance( 93 | document.getElementById("feedback-modal") 94 | ); 95 | if (modalEl) modalEl.hide(); 96 | }); 97 | } 98 | 99 | -------------------------------------------------------------------------------- /application/single_app/static/js/chat/chat-global.js: -------------------------------------------------------------------------------- 1 | // chat-global.js 2 | 3 | let currentConversationId = null; 4 | let personalDocs = []; 5 | let groupDocs = []; 6 | let activeGroupName = ""; 7 | let userPrompts = []; 8 | let groupPrompts = []; 9 | let currentlyEditingId = null; 10 | 11 | function scrollChatToBottom() { 12 | const chatbox = document.getElementById("chatbox"); 13 | if (chatbox) { 14 | chatbox.scrollTop = chatbox.scrollHeight; 15 | } 16 | } -------------------------------------------------------------------------------- /application/single_app/static/js/chat/chat-input-actions.js: -------------------------------------------------------------------------------- 1 | // chat-input-actions.js 2 | 3 | import { showToast } from "./chat-toast.js"; 4 | import { 5 | createNewConversation, 6 | loadConversations, 7 | } from "./chat-conversations.js"; 8 | import { 9 | showFileUploadingMessage, 10 | hideFileUploadingMessage, 11 | showLoadingIndicator, 12 | hideLoadingIndicator, 13 | } from "./chat-loading-indicator.js"; 14 | import { loadMessages } from "./chat-messages.js"; 15 | 16 | const imageGenBtn = document.getElementById("image-generate-btn"); 17 | const webSearchBtn = document.getElementById("search-web-btn"); 18 | const chooseFileBtn = document.getElementById("choose-file-btn"); 19 | const fileInputEl = document.getElementById("file-input"); 20 | const uploadBtn = document.getElementById("upload-btn"); 21 | const cancelFileSelection = document.getElementById("cancel-file-selection"); 22 | 23 | export function resetFileButton() { 24 | const fileInputEl = document.getElementById("file-input"); 25 | const fileBtn = document.getElementById("choose-file-btn"); 26 | const uploadBtn = document.getElementById("upload-btn"); 27 | const cancelFileSelection = document.getElementById("cancel-file-selection"); 28 | 29 | if (fileInputEl) { 30 | fileInputEl.value = ""; 31 | } 32 | 33 | if (fileBtn) { 34 | fileBtn.classList.remove("active"); 35 | fileBtn.querySelector(".file-btn-text").textContent = ""; 36 | } 37 | 38 | if (uploadBtn) { 39 | uploadBtn.style.display = "none"; 40 | } 41 | 42 | if (cancelFileSelection) { 43 | cancelFileSelection.style.display = "none"; 44 | } 45 | } 46 | 47 | export function uploadFileToConversation(file) { 48 | const uploadingIndicatorEl = showFileUploadingMessage(); 49 | 50 | const formData = new FormData(); 51 | formData.append("file", file); 52 | formData.append("conversation_id", currentConversationId); 53 | 54 | fetch("/upload", { 55 | method: "POST", 56 | body: formData, 57 | }) 58 | .then((response) => { 59 | hideFileUploadingMessage(uploadingIndicatorEl); 60 | 61 | let clonedResponse = response.clone(); 62 | return response.json().then((data) => { 63 | if (!response.ok) { 64 | console.error("Upload failed:", data.error || "Unknown error"); 65 | showToast( 66 | "Error uploading file: " + (data.error || "Unknown error"), 67 | "danger" 68 | ); 69 | throw new Error(data.error || "Upload failed"); 70 | } 71 | return data; 72 | }); 73 | }) 74 | .then((data) => { 75 | if (data.conversation_id) { 76 | currentConversationId = data.conversation_id; 77 | loadMessages(currentConversationId); 78 | loadConversations(); 79 | } else { 80 | console.error("No conversation_id returned from server."); 81 | showToast("Error: No conversation ID returned from server.", "danger"); 82 | } 83 | resetFileButton(); 84 | }) 85 | .catch((error) => { 86 | console.error("Error:", error); 87 | showToast("Error uploading file: " + error.message, "danger"); 88 | resetFileButton(); 89 | hideFileUploadingMessage(uploadingIndicatorEl); 90 | }); 91 | } 92 | 93 | export function fetchFileContent(conversationId, fileId) { 94 | showLoadingIndicator(); 95 | fetch("/api/get_file_content", { 96 | method: "POST", 97 | headers: { "Content-Type": "application/json" }, 98 | body: JSON.stringify({ 99 | conversation_id: conversationId, 100 | file_id: fileId, 101 | }), 102 | }) 103 | .then((response) => response.json()) 104 | .then((data) => { 105 | hideLoadingIndicator(); 106 | 107 | if (data.file_content && data.filename) { 108 | showFileContentPopup(data.file_content, data.filename, data.is_table); 109 | } else if (data.error) { 110 | showToast(data.error, "danger"); 111 | } else { 112 | ashowToastlert("Unexpected response from server.", "danger"); 113 | } 114 | }) 115 | .catch((error) => { 116 | hideLoadingIndicator(); 117 | console.error("Error fetching file content:", error); 118 | showToast("Error fetching file content.", "danger"); 119 | }); 120 | } 121 | 122 | export function showFileContentPopup(fileContent, filename, isTable) { 123 | let modalContainer = document.getElementById("file-modal"); 124 | if (!modalContainer) { 125 | modalContainer = document.createElement("div"); 126 | modalContainer.id = "file-modal"; 127 | modalContainer.classList.add("modal", "fade"); 128 | modalContainer.tabIndex = -1; 129 | modalContainer.setAttribute("aria-hidden", "true"); 130 | 131 | modalContainer.innerHTML = ` 132 | 148 | `; 149 | document.body.appendChild(modalContainer); 150 | } else { 151 | const modalTitle = modalContainer.querySelector(".modal-title"); 152 | if (modalTitle) { 153 | modalTitle.textContent = `Uploaded File: ${filename}`; 154 | } 155 | } 156 | 157 | const fileContentElement = document.getElementById("file-content"); 158 | if (!fileContentElement) return; 159 | 160 | if (isTable) { 161 | fileContentElement.innerHTML = `
${fileContent}
`; 162 | $(document).ready(function () { 163 | $("#file-content table").DataTable({ 164 | responsive: true, 165 | scrollX: true, 166 | }); 167 | }); 168 | } else { 169 | fileContentElement.innerHTML = `
${fileContent}
`; 170 | } 171 | 172 | const modal = new bootstrap.Modal(modalContainer); 173 | modal.show(); 174 | } 175 | 176 | export function getUrlParameter(name) { 177 | name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); 178 | const regex = new RegExp("[\\?&]" + name + "=([^&#]*)"); 179 | const results = regex.exec(location.search); 180 | return results === null 181 | ? "" 182 | : decodeURIComponent(results[1].replace(/\+/g, " ")); 183 | } 184 | 185 | document.addEventListener("DOMContentLoaded", function () { 186 | const tooltipTriggerList = [].slice.call( 187 | document.querySelectorAll('[data-bs-toggle="tooltip"]') 188 | ); 189 | tooltipTriggerList.forEach(function (tooltipTriggerEl) { 190 | new bootstrap.Tooltip(tooltipTriggerEl); 191 | }); 192 | }); 193 | 194 | if (imageGenBtn) { 195 | imageGenBtn.addEventListener("click", function () { 196 | this.classList.toggle("active"); 197 | 198 | const isImageGenEnabled = this.classList.contains("active"); 199 | const docBtn = document.getElementById("search-documents-btn"); 200 | const webBtn = document.getElementById("search-web-btn"); 201 | const fileBtn = document.getElementById("choose-file-btn"); 202 | 203 | if (isImageGenEnabled) { 204 | if (docBtn) { 205 | docBtn.disabled = true; 206 | docBtn.classList.remove("active"); 207 | } 208 | if (webBtn) { 209 | webBtn.disabled = true; 210 | webBtn.classList.remove("active"); 211 | } 212 | if (fileBtn) { 213 | fileBtn.disabled = true; 214 | fileBtn.classList.remove("active"); 215 | } 216 | } else { 217 | if (docBtn) docBtn.disabled = false; 218 | if (webBtn) webBtn.disabled = false; 219 | if (fileBtn) fileBtn.disabled = false; 220 | } 221 | }); 222 | } 223 | 224 | if (webSearchBtn) { 225 | webSearchBtn.addEventListener("click", function () { 226 | this.classList.toggle("active"); 227 | }); 228 | } 229 | 230 | if (chooseFileBtn) { 231 | chooseFileBtn.addEventListener("click", function () { 232 | const fileInput = document.getElementById("file-input"); 233 | if (fileInput) fileInput.click(); 234 | }); 235 | } 236 | 237 | if (fileInputEl) { 238 | fileInputEl.addEventListener("change", function () { 239 | const file = fileInputEl.files[0]; 240 | const fileBtn = document.getElementById("choose-file-btn"); 241 | const uploadBtn = document.getElementById("upload-btn"); 242 | if (!fileBtn || !uploadBtn) return; 243 | 244 | if (file) { 245 | fileBtn.classList.add("active"); 246 | fileBtn.querySelector(".file-btn-text").textContent = file.name; 247 | uploadBtn.style.display = "block"; 248 | cancelFileSelection.style.display = "inline"; 249 | } else { 250 | resetFileButton(); 251 | } 252 | }); 253 | } 254 | 255 | if (cancelFileSelection) { 256 | // Prevent the click from also triggering the "choose file" flow. 257 | cancelFileSelection.addEventListener("click", (event) => { 258 | event.stopPropagation(); 259 | resetFileButton(); 260 | }); 261 | } 262 | 263 | if (uploadBtn) { 264 | uploadBtn.addEventListener("click", () => { 265 | const fileInput = document.getElementById("file-input"); 266 | if (!fileInput) return; 267 | 268 | const file = fileInput.files[0]; 269 | if (!file) { 270 | showToast("Please select a file to upload.", "danger"); 271 | return; 272 | } 273 | 274 | if (!currentConversationId) { 275 | createNewConversation(() => { 276 | uploadFileToConversation(file); 277 | }); 278 | } else { 279 | uploadFileToConversation(file); 280 | } 281 | }); 282 | } 283 | -------------------------------------------------------------------------------- /application/single_app/static/js/chat/chat-layout.js: -------------------------------------------------------------------------------- 1 | // static/js/chat/chat-layout.js 2 | 3 | const leftPane = document.getElementById('left-pane'); 4 | const rightPane = document.getElementById('right-pane'); 5 | const dockToggleButton = document.getElementById('dock-toggle-btn'); 6 | const splitContainer = document.getElementById('split-container'); // Might not be needed directly if targeting body 7 | 8 | // --- API Function for User Settings --- 9 | async function saveUserSetting(settingsToUpdate) { 10 | 11 | 12 | try { 13 | const response = await fetch('/api/user/settings', { 14 | method: 'POST', 15 | headers: { 16 | 'Content-Type': 'application/json', 17 | // Add CSRF token header if you use CSRF protection 18 | }, 19 | body: JSON.stringify({ settings: settingsToUpdate }), // Send nested structure 20 | }); 21 | if (!response.ok) { 22 | console.error('Failed to save user settings:', response.statusText); 23 | const errorData = await response.json().catch(() => ({})); // Try to get error details 24 | console.error('Error details:', errorData); 25 | // Fallback to localStorage on API failure? 26 | // localStorage.setItem(key, JSON.stringify(value)); 27 | } else { 28 | console.log('User settings saved successfully via API for:', Object.keys(settingsToUpdate)); 29 | } 30 | } catch (error) { 31 | console.error('Error calling save settings API:', error); 32 | // Fallback to localStorage on network error? 33 | // localStorage.setItem(key, JSON.stringify(value)); 34 | } 35 | } 36 | 37 | async function loadUserSettings() { 38 | let settings = {}; 39 | 40 | try { 41 | const response = await fetch('/api/user/settings'); 42 | if (response.ok) { 43 | const data = await response.json(); 44 | settings = data.settings || {}; // Expect settings under a 'settings' key 45 | console.log('User settings loaded via API:', settings); 46 | } else { 47 | console.warn('Failed to load user settings via API:', response.statusText); 48 | // Optionally fallback to localStorage here too 49 | } 50 | } catch (error) { 51 | console.error('Error fetching user settings:', error); 52 | // Optionally fallback to localStorage here too 53 | } 54 | 55 | // Apply loaded settings 56 | currentLayout = settings[USER_SETTINGS_KEY_LAYOUT] || 'split'; // Default to 'split' 57 | currentSplitSizes = settings[USER_SETTINGS_KEY_SPLIT] || [25, 75]; // Default sizes 58 | 59 | console.log(`Applying initial layout: ${currentLayout}, sizes: ${currentSplitSizes}`); 60 | applyLayout(currentLayout, false); // Apply layout without saving again 61 | } 62 | 63 | 64 | // --- Split.js Initialization --- 65 | function initializeSplit() { 66 | if (splitInstance) { 67 | splitInstance.destroy(); // Destroy existing instance if any 68 | splitInstance = null; 69 | console.log('Split.js instance destroyed.'); 70 | } 71 | 72 | console.log('Initializing Split.js with sizes:', currentSplitSizes); 73 | splitInstance = Split(['#left-pane', '#right-pane'], { 74 | sizes: currentSplitSizes, // Use potentially loaded sizes 75 | minSize: [200, 350], // Min width in pixels [left, right] - adjust as needed 76 | gutterSize: 8, // Width of the draggable gutter 77 | cursor: 'col-resize', 78 | direction: 'horizontal', 79 | onDragEnd: function(sizes) { 80 | console.log('Split drag ended. New sizes:', sizes); 81 | currentSplitSizes = sizes; // Update current sizes 82 | // Debounce saving split sizes to avoid rapid API calls 83 | debounceSaveSplitSizes(sizes); 84 | }, 85 | elementStyle: (dimension, size, gutterSize) => ({ 86 | // Use flex-basis for sizing to work well with flex container 87 | 'flex-basis': `calc(${size}% - ${gutterSize}px)`, 88 | }), 89 | gutterStyle: (dimension, gutterSize) => ({ 90 | 'flex-basis': `${gutterSize}px`, 91 | }), 92 | }); 93 | console.log('Split.js initialized.'); 94 | } 95 | 96 | // Debounce function 97 | let debounceTimer; 98 | function debounceSaveSplitSizes(sizes) { 99 | clearTimeout(debounceTimer); 100 | debounceTimer = setTimeout(() => { 101 | saveUserSetting({ [USER_SETTINGS_KEY_SPLIT]: sizes }); 102 | }, 500); // Save 500ms after dragging stops 103 | } 104 | 105 | // --- Layout Switching Logic --- 106 | function applyLayout(layout, shouldSave = true) { 107 | currentLayout = layout; // Update global state 108 | 109 | // Remove previous layout classes 110 | document.body.classList.remove('layout-split', 'layout-docked', 'left-pane-hidden'); 111 | 112 | if (layout === 'docked') { 113 | console.log('Applying docked layout.'); 114 | document.body.classList.add('layout-docked'); 115 | if (splitInstance) { 116 | currentSplitSizes = splitInstance.getSizes(); // Save sizes before destroying 117 | splitInstance.destroy(); 118 | splitInstance = null; 119 | console.log('Split.js instance destroyed for docking.'); 120 | } 121 | // Ensure panes have correct styles applied by CSS (.layout-docked) 122 | leftPane.style.width = ''; // Let CSS handle width 123 | leftPane.style.flexBasis = ''; // Remove split.js style 124 | rightPane.style.marginLeft = ''; // Let CSS handle margin 125 | rightPane.style.width = ''; // Let CSS handle width 126 | rightPane.style.flexBasis = ''; // Remove split.js style 127 | 128 | } else { // 'split' layout 129 | console.log('Applying split layout.'); 130 | document.body.classList.add('layout-split'); // Use a class for split state too? Optional. 131 | // Re-initialize Split.js if it's not already running 132 | if (!splitInstance) { 133 | initializeSplit(); 134 | } 135 | // Remove fixed positioning styles if they were somehow manually added 136 | leftPane.style.position = ''; 137 | leftPane.style.top = ''; 138 | leftPane.style.left = ''; 139 | leftPane.style.height = ''; 140 | leftPane.style.zIndex = ''; 141 | rightPane.style.marginLeft = ''; 142 | rightPane.style.width = ''; 143 | } 144 | 145 | // Update toggle button icon/title 146 | updateDockToggleButton(); 147 | 148 | // Save the new layout preference 149 | if (shouldSave) { 150 | saveUserSetting({ [USER_SETTINGS_KEY_LAYOUT]: layout }); 151 | } 152 | } 153 | 154 | function toggleDocking() { 155 | const nextLayout = currentLayout === 'split' ? 'docked' : 'split'; 156 | applyLayout(nextLayout, true); // Apply and save 157 | } 158 | 159 | // Maybe add a function to toggle the docked sidebar visibility 160 | function toggleDockedSidebarVisibility() { 161 | if (currentLayout === 'docked') { 162 | document.body.classList.toggle('left-pane-hidden'); 163 | const isHidden = document.body.classList.contains('left-pane-hidden'); 164 | saveUserSetting({ 'dockedSidebarHidden': isHidden }); // Save visibility state if needed 165 | updateDockToggleButton(); // Update icon maybe 166 | } 167 | } 168 | 169 | function updateDockToggleButton() { 170 | const icon = dockToggleButton.querySelector('i'); 171 | if (!icon) return; // Add safety check 172 | 173 | if (currentLayout === 'docked') { 174 | icon.classList.remove('bi-layout-sidebar-inset'); 175 | icon.classList.add('bi-layout-sidebar-inset-reverse'); 176 | dockToggleButton.title = "Undock Sidebar (Split View)"; 177 | // NO MORE onclick assignment here 178 | // If you need the hide/show functionality, you might need a separate button 179 | // or add logic within the main toggleDocking based on state, 180 | // but avoid direct onclick reassignment here. 181 | 182 | } else { // 'split' layout 183 | icon.classList.remove('bi-layout-sidebar-inset-reverse'); 184 | icon.classList.add('bi-layout-sidebar-inset'); 185 | dockToggleButton.title = "Dock Sidebar Left"; 186 | // NO MORE onclick assignment here either 187 | } 188 | } 189 | 190 | 191 | // --- Event Listeners --- 192 | if (dockToggleButton) { 193 | dockToggleButton.addEventListener('click', toggleDocking); 194 | } else { 195 | console.error('Dock toggle button not found.'); 196 | } 197 | 198 | // --- Initial Load --- 199 | document.addEventListener('DOMContentLoaded', () => { 200 | loadUserSettings(); // Load settings and apply initial layout 201 | }); -------------------------------------------------------------------------------- /application/single_app/static/js/chat/chat-loading-indicator.js: -------------------------------------------------------------------------------- 1 | // chat-loading.js 2 | 3 | export function showLoadingIndicator() { 4 | let loadingSpinner = document.getElementById("loading-spinner"); 5 | if (!loadingSpinner) { 6 | loadingSpinner = document.createElement("div"); 7 | loadingSpinner.id = "loading-spinner"; 8 | loadingSpinner.innerHTML = ` 9 |
10 | Loading... 11 |
12 | `; 13 | loadingSpinner.style.position = "fixed"; 14 | loadingSpinner.style.top = "50%"; 15 | loadingSpinner.style.left = "50%"; 16 | loadingSpinner.style.transform = "translate(-50%, -50%)"; 17 | loadingSpinner.style.zIndex = "1050"; 18 | document.body.appendChild(loadingSpinner); 19 | } else { 20 | loadingSpinner.style.display = "block"; 21 | } 22 | } 23 | 24 | export function hideLoadingIndicator() { 25 | const loadingSpinner = document.getElementById("loading-spinner"); 26 | if (loadingSpinner) { 27 | loadingSpinner.style.display = "none"; 28 | } 29 | } 30 | 31 | export function showLoadingIndicatorInChatbox() { 32 | const chatbox = document.getElementById("chatbox"); 33 | if (!chatbox) return; 34 | 35 | const loadingIndicator = document.createElement("div"); 36 | loadingIndicator.classList.add("loading-indicator"); 37 | loadingIndicator.id = "loading-indicator"; 38 | loadingIndicator.innerHTML = ` 39 |
40 | AI is typing... 41 |
42 | AI is typing... 43 | `; 44 | chatbox.appendChild(loadingIndicator); 45 | chatbox.scrollTop = chatbox.scrollHeight; 46 | } 47 | 48 | export function hideLoadingIndicatorInChatbox() { 49 | const loadingIndicator = document.getElementById("loading-indicator"); 50 | if (loadingIndicator) { 51 | loadingIndicator.remove(); 52 | } 53 | } 54 | 55 | export function showFileUploadingMessage() { 56 | const chatbox = document.getElementById("chatbox"); 57 | if (!chatbox) return null; 58 | 59 | // Create a wrapper for the system message 60 | const msgWrapper = document.createElement("div"); 61 | msgWrapper.classList.add("chat-message", "system-message", "file-uploading-msg"); 62 | // You might have your own classes like "ai-message" or "system-message" 63 | 64 | // You can style this similarly to how you show "AI is typing..." 65 | msgWrapper.innerHTML = ` 66 |
67 |
68 | Uploading file to chat... 69 |
70 | Uploading file to chat... 71 |
72 | `; 73 | 74 | chatbox.appendChild(msgWrapper); 75 | chatbox.scrollTop = chatbox.scrollHeight; 76 | 77 | return msgWrapper; 78 | } 79 | 80 | export function hideFileUploadingMessage(uploadingMsgEl) { 81 | if (uploadingMsgEl && uploadingMsgEl.parentNode) { 82 | uploadingMsgEl.parentNode.removeChild(uploadingMsgEl); 83 | } 84 | } -------------------------------------------------------------------------------- /application/single_app/static/js/chat/chat-onload.js: -------------------------------------------------------------------------------- 1 | // chat-onload.js 2 | 3 | import { loadConversations } from "./chat-conversations.js"; 4 | // Import handleDocumentSelectChange 5 | import { loadAllDocs, populateDocumentSelectScope, handleDocumentSelectChange } from "./chat-documents.js"; 6 | import { getUrlParameter } from "./chat-utils.js"; // Assuming getUrlParameter is in chat-utils.js now 7 | import { loadUserPrompts, loadGroupPrompts, initializePromptInteractions } from "./chat-prompts.js"; 8 | 9 | window.addEventListener('DOMContentLoaded', () => { 10 | console.log("DOM Content Loaded. Starting initializations."); // Log start 11 | 12 | loadConversations(); // Load conversations immediately 13 | 14 | // Grab references to the relevant elements 15 | const userInput = document.getElementById("user-input"); 16 | const newConversationBtn = document.getElementById("new-conversation-btn"); 17 | const promptsBtn = document.getElementById("search-prompts-btn"); 18 | const fileBtn = document.getElementById("choose-file-btn"); 19 | 20 | // 1) Message Input Focus => Create conversation if none 21 | if (userInput && newConversationBtn) { 22 | userInput.addEventListener("focus", () => { 23 | if (!currentConversationId) { 24 | newConversationBtn.click(); 25 | } 26 | }); 27 | } 28 | 29 | // 2) Prompts Button Click => Create conversation if none 30 | if (promptsBtn && newConversationBtn) { 31 | promptsBtn.addEventListener("click", (event) => { 32 | if (!currentConversationId) { 33 | // Optionally prevent the default action if it does something immediately 34 | // event.preventDefault(); 35 | newConversationBtn.click(); 36 | 37 | // (Optional) If you need the prompt UI to appear *after* the conversation is created, 38 | // you can open the prompt UI programmatically in a small setTimeout or callback. 39 | // setTimeout(() => openPromptUI(), 100); 40 | } 41 | }); 42 | } 43 | 44 | // 3) File Upload Button Click => Create conversation if none 45 | if (fileBtn && newConversationBtn) { 46 | fileBtn.addEventListener("click", (event) => { 47 | if (!currentConversationId) { 48 | // event.preventDefault(); // If file dialog should only open once conversation is created 49 | newConversationBtn.click(); 50 | 51 | // (Optional) If you want the file dialog to appear *after* the conversation is created, 52 | // do it in a short setTimeout or callback: 53 | // setTimeout(() => fileBtn.click(), 100); 54 | } 55 | }); 56 | } 57 | 58 | // Load documents and prompts 59 | Promise.all([ 60 | loadAllDocs(), 61 | loadUserPrompts(), 62 | loadGroupPrompts() 63 | ]) 64 | .then(() => { 65 | console.log("Initial data (Docs, Prompts) loaded successfully."); // Log success 66 | 67 | // --- Initialize Document-related UI --- 68 | // This part handles URL params for documents - KEEP IT 69 | const localSearchDocsParam = getUrlParameter("search_documents") === "true"; 70 | const localDocScopeParam = getUrlParameter("doc_scope") || ""; 71 | const localDocumentIdParam = getUrlParameter("document_id") || ""; 72 | const localSearchDocsBtn = document.getElementById("search-documents-btn"); 73 | const localDocScopeSel = document.getElementById("doc-scope-select"); 74 | const localDocSelectEl = document.getElementById("document-select"); 75 | const searchDocumentsContainer = document.getElementById("search-documents-container"); 76 | 77 | if (localSearchDocsParam && localSearchDocsBtn && localDocScopeSel && localDocSelectEl && searchDocumentsContainer) { 78 | console.log("Handling document URL parameters."); // Log 79 | localSearchDocsBtn.classList.add("active"); 80 | searchDocumentsContainer.style.display = "block"; 81 | if (localDocScopeParam) { 82 | localDocScopeSel.value = localDocScopeParam; 83 | } 84 | populateDocumentSelectScope(); // Populate based on scope (might be default or from URL) 85 | 86 | if (localDocumentIdParam) { 87 | // Wait a tiny moment for populateDocumentSelectScope potentially async operations (less ideal, but sometimes needed) 88 | // A better approach would be if populateDocumentSelectScope returned a promise 89 | // setTimeout(() => { 90 | if ([...localDocSelectEl.options].some(option => option.value === localDocumentIdParam)) { 91 | localDocSelectEl.value = localDocumentIdParam; 92 | } else { 93 | console.warn(`Document ID "${localDocumentIdParam}" not found for scope "${localDocScopeSel.value}".`); 94 | } 95 | // Ensure classification updates after setting document 96 | handleDocumentSelectChange(); 97 | // }, 0); // Tiny delay 98 | } else { 99 | // If no specific doc ID, still might need to trigger change if scope changed 100 | handleDocumentSelectChange(); 101 | } 102 | } else { 103 | // If not loading from URL params, maybe still populate default scope? 104 | populateDocumentSelectScope(); 105 | } 106 | // --- End Document-related UI --- 107 | 108 | 109 | // --- Call the prompt initialization function HERE --- 110 | console.log("Calling initializePromptInteractions..."); 111 | initializePromptInteractions(); 112 | 113 | 114 | console.log("All initializations complete."); // Log end 115 | 116 | }) 117 | .catch((err) => { 118 | console.error("Error during initial data loading or setup:", err); 119 | // Maybe try to initialize prompts even if doc loading fails? Depends on requirements. 120 | // console.log("Attempting to initialize prompts despite data load error..."); 121 | // initializePromptInteractions(); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /application/single_app/static/js/chat/chat-prompts.js: -------------------------------------------------------------------------------- 1 | // chat-prompts.js 2 | 3 | import { userInput} from "./chat-messages.js"; 4 | 5 | const promptSelectionContainer = document.getElementById("prompt-selection-container"); 6 | export const promptSelect = document.getElementById("prompt-select"); // Keep export if needed elsewhere 7 | const searchPromptsBtn = document.getElementById("search-prompts-btn"); 8 | 9 | export function loadUserPrompts() { 10 | return fetch("/api/prompts") 11 | .then(r => r.json()) 12 | .then(data => { 13 | if (data.prompts) { 14 | userPrompts = data.prompts; 15 | } 16 | }) 17 | .catch(err => console.error("Error loading user prompts:", err)); 18 | } 19 | 20 | export function loadGroupPrompts() { 21 | return fetch("/api/group_prompts") 22 | .then(r => r.json()) 23 | .then(data => { 24 | if (data.prompts) { 25 | groupPrompts = data.prompts; 26 | } 27 | }) 28 | .catch(err => console.error("Error loading group prompts:", err)); 29 | } 30 | 31 | export function populatePromptSelect() { 32 | if (!promptSelect) return; 33 | 34 | promptSelect.innerHTML = ""; 35 | const defaultOpt = document.createElement("option"); 36 | defaultOpt.value = ""; 37 | defaultOpt.textContent = "Select a Prompt..."; 38 | promptSelect.appendChild(defaultOpt); 39 | 40 | const combined = [...userPrompts.map(p => ({...p, scope: "User"})), 41 | ...groupPrompts.map(p => ({...p, scope: "Group"}))]; 42 | 43 | // combined.sort((a, b) => a.name.localeCompare(b.name)); 44 | 45 | combined.forEach(promptObj => { 46 | const opt = document.createElement("option"); 47 | opt.value = promptObj.id; 48 | opt.textContent = `[${promptObj.scope}] ${promptObj.name}`; 49 | opt.dataset.promptContent = promptObj.content; 50 | promptSelect.appendChild(opt); 51 | }); 52 | } 53 | 54 | export function initializePromptInteractions() { 55 | console.log("Attempting to initialize prompt interactions..."); // Debug log 56 | // Check for elements *inside* the function that runs later 57 | if (searchPromptsBtn && promptSelectionContainer && userInput) { 58 | console.log("Elements found, adding prompt button listener."); // Debug log 59 | searchPromptsBtn.addEventListener("click", function() { 60 | const isActive = this.classList.toggle("active"); 61 | 62 | if (isActive) { 63 | promptSelectionContainer.style.display = "block"; 64 | populatePromptSelect(); 65 | userInput.classList.add("with-prompt-active"); 66 | userInput.focus(); 67 | } else { 68 | promptSelectionContainer.style.display = "none"; 69 | if (promptSelect) { 70 | promptSelect.selectedIndex = 0; 71 | } 72 | userInput.classList.remove("with-prompt-active"); 73 | userInput.focus(); 74 | } 75 | }); 76 | } else { 77 | // Log detailed errors if elements are missing WHEN this function runs 78 | if (!searchPromptsBtn) console.error("Prompt Init Error: search-prompts-btn not found."); 79 | if (!promptSelectionContainer) console.error("Prompt Init Error: prompt-selection-container not found."); 80 | // This check is crucial: is userInput null/undefined when this function executes? 81 | if (!userInput) console.error("Prompt Init Error: userInput (imported from chat-messages) is not available."); 82 | } 83 | } -------------------------------------------------------------------------------- /application/single_app/static/js/chat/chat-toast.js: -------------------------------------------------------------------------------- 1 | // chat-toast.js 2 | 3 | export function showToast(message, variant = "danger") { 4 | const container = document.getElementById("toast-container"); 5 | if (!container) return; 6 | 7 | const id = "toast-" + Date.now(); 8 | const toastHtml = ` 9 | 17 | `; 18 | container.insertAdjacentHTML("beforeend", toastHtml); 19 | 20 | const toastEl = document.getElementById(id); 21 | const bsToast = new bootstrap.Toast(toastEl, { delay: 5000 }); 22 | bsToast.show(); 23 | } -------------------------------------------------------------------------------- /application/single_app/static/js/chat/chat-utils.js: -------------------------------------------------------------------------------- 1 | // chat-utils.js 2 | export function toBoolean(value) { 3 | if (value === true || value === 1) return true; 4 | if (value === false || value === 0 || !value) return false; 5 | 6 | const strValue = String(value).toLowerCase().trim(); 7 | return strValue === "true" || strValue === "1" || strValue === "yes" || strValue === "y"; 8 | } 9 | 10 | export function isColorLight(hexColor) { 11 | if (!hexColor || typeof hexColor !== 'string' || hexColor.length < 4) return false; // Default to dark background assumption 12 | 13 | let r, g, b; 14 | hexColor = hexColor.replace('#', ''); // Remove # 15 | 16 | if (hexColor.length === 3) { // #RGB format 17 | r = parseInt(hexColor[0] + hexColor[0], 16); 18 | g = parseInt(hexColor[1] + hexColor[1], 16); 19 | b = parseInt(hexColor[2] + hexColor[2], 16); 20 | } else if (hexColor.length === 6) { // #RRGGBB format 21 | r = parseInt(hexColor.substring(0, 2), 16); 22 | g = parseInt(hexColor.substring(2, 4), 16); 23 | b = parseInt(hexColor.substring(4, 6), 16); 24 | } else { 25 | return false; // Invalid format 26 | } 27 | 28 | // Formula for perceived brightness (YIQ simplified) 29 | const brightness = ((r * 299) + (g * 587) + (b * 114)) / 1000; 30 | return brightness > 150; // Threshold adjustable (128 is middle gray, higher means more colors are considered 'light') 31 | } 32 | 33 | // --- Other utility functions like getUrlParameter can go here --- 34 | export function getUrlParameter(name) { 35 | name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]'); 36 | var regex = new RegExp('[\\?&]' + name + '=([^&#]*)'); 37 | var results = regex.exec(location.search); 38 | return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' ')); 39 | }; 40 | 41 | // Add escapeHtml if not already present globally or imported 42 | export function escapeHtml(unsafe) { 43 | if (unsafe === null || typeof unsafe === 'undefined') return ''; 44 | return unsafe.toString() 45 | .replace(/&/g, "&") 46 | .replace(//g, ">") 48 | .replace(/"/g, """) 49 | .replace(/'/g, "'"); 50 | } -------------------------------------------------------------------------------- /application/single_app/static/js/chat/split.min.js: -------------------------------------------------------------------------------- 1 | /*! Split.js - v1.5.11 */ 2 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Split=t()}(this,function(){"use strict";var L=window,T=L.document,N="addEventListener",R="removeEventListener",q="getBoundingClientRect",H="horizontal",I=function(){return!1},W=L.attachEvent&&!L[N],i=["","-webkit-","-moz-","-o-"].filter(function(e){var t=T.createElement("div");return t.style.cssText="width:"+e+"calc(9px)",!!t.style.length}).shift()+"calc",s=function(e){return"string"==typeof e||e instanceof String},X=function(e){if(s(e)){var t=T.querySelector(e);if(!t)throw new Error("Selector "+e+" did not match a DOM element");return t}return e},Y=function(e,t,n){var r=e[t];return void 0!==r?r:n},G=function(e,t,n,r){if(t){if("end"===r)return 0;if("center"===r)return e/2}else if(n){if("start"===r)return 0;if("center"===r)return e/2}return e},J=function(e,t){var n=T.createElement("div");return n.className="gutter gutter-"+t,n},K=function(e,t,n){var r={};return s(t)?r[e]=t:r[e]=W?t+"%":i+"("+t+"% - "+n+"px)",r},P=function(e,t){var n;return(n={})[e]=t+"px",n};return function(e,i){void 0===i&&(i={});var u,t,s,o,r,a,l=e;Array.from&&(l=Array.from(l));var c=X(l[0]).parentNode,n=getComputedStyle?getComputedStyle(c):null,f=n?n.flexDirection:null,m=Y(i,"sizes")||l.map(function(){return 100/l.length}),h=Y(i,"minSize",100),d=Array.isArray(h)?h:l.map(function(){return h}),g=Y(i,"expandToMin",!1),v=Y(i,"gutterSize",10),p=Y(i,"gutterAlign","center"),y=Y(i,"snapOffset",30),z=Y(i,"dragInterval",1),S=Y(i,"direction",H),b=Y(i,"cursor",S===H?"col-resize":"row-resize"),_=Y(i,"gutter",J),E=Y(i,"elementStyle",K),w=Y(i,"gutterStyle",P);function k(t,e,n,r){var i=E(u,e,n,r);Object.keys(i).forEach(function(e){t.style[e]=i[e]})}function x(){return a.map(function(e){return e.size})}function M(e){return"touches"in e?e.touches[0][t]:e[t]}function U(e){var t=a[this.a],n=a[this.b],r=t.size+n.size;t.size=e/this.size*r,n.size=r-e/this.size*r,k(t.element,t.size,this._b,t.i),k(n.element,n.size,this._c,n.i)}function O(){var e=a[this.a].element,t=a[this.b].element,n=e[q](),r=t[q]();this.size=n[u]+r[u]+this._b+this._c,this.start=n[s],this.end=n[o]}function C(s){var o=function(e){if(!getComputedStyle)return null;var t=getComputedStyle(e);if(!t)return null;var n=e[r];return 0===n?null:n-=S===H?parseFloat(t.paddingLeft)+parseFloat(t.paddingRight):parseFloat(t.paddingTop)+parseFloat(t.paddingBottom)}(c);if(null===o)return s;if(d.reduce(function(e,t){return e+t},0)>o)return s;var a=0,u=[],e=s.map(function(e,t){var n=o*e/100,r=G(v,0===t,t===s.length-1,p),i=d[t]+r;return n=this.size-(r.minSize+y+this._c)&&(t=this.size-(r.minSize+this._c)),U.call(this,t),Y(i,"onDrag",I)())}.bind(t),t.stop=function(){var e=this,t=a[e.a].element,n=a[e.b].element;e.dragging&&Y(i,"onDragEnd",I)(x()),e.dragging=!1,L[R]("mouseup",e.stop),L[R]("touchend",e.stop),L[R]("touchcancel",e.stop),L[R]("mousemove",e.move),L[R]("touchmove",e.move),e.stop=null,e.move=null,t[R]("selectstart",I),t[R]("dragstart",I),n[R]("selectstart",I),n[R]("dragstart",I),t.style.userSelect="",t.style.webkitUserSelect="",t.style.MozUserSelect="",t.style.pointerEvents="",n.style.userSelect="",n.style.webkitUserSelect="",n.style.MozUserSelect="",n.style.pointerEvents="",e.gutter.style.cursor="",e.parent.style.cursor="",T.body.style.cursor=""}.bind(t),L[N]("mouseup",t.stop),L[N]("touchend",t.stop),L[N]("touchcancel",t.stop),L[N]("mousemove",t.move),L[N]("touchmove",t.move),n[N]("selectstart",I),n[N]("dragstart",I),r[N]("selectstart",I),r[N]("dragstart",I),n.style.userSelect="none",n.style.webkitUserSelect="none",n.style.MozUserSelect="none",n.style.pointerEvents="none",r.style.userSelect="none",r.style.webkitUserSelect="none",r.style.MozUserSelect="none",r.style.pointerEvents="none",t.gutter.style.cursor=b,t.parent.style.cursor=b,T.body.style.cursor=b,O.call(t),t.dragOffset=M(e)-t.end}}S===H?(u="width",t="clientX",s="left",o="right",r="clientWidth"):"vertical"===S&&(u="height",t="clientY",s="top",o="bottom",r="clientHeight"),m=C(m);var A=[];function j(e){var t=e.i===A.length,n=t?A[e.i-1]:A[e.i];O.call(n);var r=t?n.size-e.minSize-n._c:e.minSize+n._b;U.call(n,r)}function F(e){var s=C(e);s.forEach(function(e,t){if(0 { 105 | if (darkModeToggle) { 106 | // Add click event listener to toggle 107 | darkModeToggle.addEventListener('click', toggleDarkMode); 108 | 109 | // Load user preference (to sync with server) 110 | loadDarkModePreference(); 111 | } 112 | }); -------------------------------------------------------------------------------- /application/single_app/static/js/workspace/workspace-init.js: -------------------------------------------------------------------------------- 1 | // static/js/workspace/workspace-init.js 2 | 3 | // Make sure fetch functions are available globally or imported if using modules consistently 4 | // Assuming fetchUserDocuments and fetchUserPrompts are now globally available via window.* assignments in their respective files 5 | 6 | document.addEventListener('DOMContentLoaded', () => { 7 | console.log("Workspace initializing..."); 8 | 9 | // Function to load data for the currently active tab 10 | function loadActiveTabData() { 11 | const activeTab = document.querySelector('.nav-tabs .nav-link.active'); 12 | if (!activeTab) return; 13 | 14 | const targetId = activeTab.getAttribute('data-bs-target'); 15 | 16 | if (targetId === '#documents-tab') { 17 | console.log("Loading documents tab data..."); 18 | if (typeof window.fetchUserDocuments === 'function') { 19 | window.fetchUserDocuments(); 20 | } else { 21 | console.error("fetchUserDocuments function not found."); 22 | } 23 | } else if (targetId === '#prompts-tab') { 24 | console.log("Loading prompts tab data..."); 25 | if (typeof window.fetchUserPrompts === 'function') { 26 | window.fetchUserPrompts(); 27 | } else { 28 | console.error("fetchUserPrompts function not found."); 29 | } 30 | } 31 | } 32 | 33 | // Initial load for the default active tab 34 | loadActiveTabData(); 35 | 36 | // Add event listeners to tab buttons to load data when a tab is shown 37 | const tabButtons = document.querySelectorAll('#workspaceTab button[data-bs-toggle="tab"]'); 38 | tabButtons.forEach(button => { 39 | button.addEventListener('shown.bs.tab', event => { 40 | console.log(`Tab shown: ${event.target.getAttribute('data-bs-target')}`); 41 | loadActiveTabData(); // Load data for the newly shown tab 42 | }); 43 | }); 44 | 45 | }); -------------------------------------------------------------------------------- /application/single_app/static/js/workspace/workspace-utils.js: -------------------------------------------------------------------------------- 1 | // workspace-utils.js 2 | 3 | export function escapeHtml(unsafe) { 4 | if (unsafe === null || typeof unsafe === 'undefined') return ''; 5 | return unsafe.toString() 6 | .replace(/&/g, "&") 7 | .replace(//g, ">") 9 | .replace(/"/g, """) 10 | .replace(/'/g, "'"); 11 | } -------------------------------------------------------------------------------- /application/single_app/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / -------------------------------------------------------------------------------- /application/single_app/templates/acceptable_use_policy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% extends "base.html" %} 4 | {% block title %} 5 | Acceptable Use Policy - {{ app_settings.app_title }} 6 | {% endblock %} 7 | {% block content %} 8 | 9 |
10 |

Acceptable Use Policy

11 |
12 | This is an example. 13 | Modify this template to create your own Acceptable Use Policy or 14 | Update the Landing Page content in the application settings 15 | to link to your own Acceptable Use Policy. 16 |
17 |

Last Updated: January 30, 2025

18 | 19 |

20 | Welcome to our AI-powered chat service. By using this service, 21 | you agree to follow this Acceptable Use Policy (AUP) to ensure 22 | a safe and respectful experience for all users. 23 |

24 | 25 |

1. Prohibited Activities

26 |

You must not use this service to:

27 |
    28 |
  • Engage in illegal, fraudulent, or harmful activities.
  • 29 |
  • Distribute, promote, or encourage violence, hate speech, or discrimination.
  • 30 |
  • Share offensive, explicit, or inappropriate content.
  • 31 |
  • Impersonate individuals, companies, or organizations.
  • 32 |
  • Use the AI to generate misleading or deceptive information.
  • 33 |
  • Spam, harass, or disrupt the experience for others.
  • 34 |
  • Attempt to reverse-engineer, hack, or exploit the AI system.
  • 35 |
36 | 37 |

2. Data Privacy

38 |

We respect your privacy. Please do not share personal, confidential, or sensitive information while using this service.

39 | 40 |

3. Responsible AI Use

41 |

42 | The AI assistant provides information based on patterns in data and 43 | should not be considered a replacement for professional advice (legal, 44 | medical, financial, etc.). Use AI-generated content responsibly. 45 |

46 | 47 |

4. Enforcement

48 |

49 | Violations of this policy may result in temporary or permanent 50 | suspension of access to the service. 51 |

52 | 53 |

5. Changes to This Policy

54 |

55 | We may update this AUP from time to time. Continued use of the service 56 | after any modifications constitutes your acceptance of the new terms. 57 |

58 | 59 |

60 | By using this service, you agree to this Acceptable Use Policy. 61 |

62 | 63 |
64 | {% endblock %} 65 | -------------------------------------------------------------------------------- /application/single_app/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% extends "base.html" %} 4 | {% block title %} 5 | Home - {{ app_settings.app_title }} 6 | {% endblock %} 7 | {% block content %} 8 | 9 |
10 | {% if app_settings.show_logo %} 11 | {% if app_settings.custom_logo_base64 %} 12 | Logo 16 | {% else %} 17 | Logo 21 | {% endif %} 22 | {% endif %} 23 | 24 | {% if session.get('user') and session['user'].get('roles') 25 | and ('Admin' in session['user']['roles'] or 'User' in session['user']['roles']) %} 26 |

Welcome to {{ app_settings.app_title }}

27 |
28 | {{ landing_html | safe }} 29 |
30 |
31 | Start Chatting 32 | 33 | 34 | {% else %} 35 | {% if session.get('user') %} 36 |

Welcome to {{ app_settings.app_title }}

37 |

38 | You are logged in but do not have the required permissions to access this application. 39 | Please submit a ticket to request access. 40 |

41 | {% else %} 42 |

Welcome to {{ app_settings.app_title }}

43 |
44 | {{ landing_html | safe }} 45 |
46 |

47 | Please sign in to continue. 48 |

49 | {% endif %} 50 | {% endif %} 51 |
52 | {% endblock %} 53 | -------------------------------------------------------------------------------- /application/single_app/templates/profile.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% extends "base.html" %} 4 | {% block title %} 5 | Profile - {{ app_settings.app_title }} 6 | {% endblock %} 7 | {% block content %} 8 | 9 |

User Profile

10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
Name:{{ user.get('name', 'N/A') }}
Email:{{ user.get('preferred_username', 'N/A') }}
Object ID:{{ user.get('oid', 'N/A') }}
24 | 25 |

Version: {{ config['VERSION'] }}

26 | 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /artifacts/app_service_manifest/manifest-ms_graph.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/artifacts/app_service_manifest/manifest-ms_graph.json -------------------------------------------------------------------------------- /artifacts/architecture.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/artifacts/architecture.vsdx -------------------------------------------------------------------------------- /artifacts/cosmos_examples/cosmos-conversation-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "", 3 | "user_id": "", 4 | "last_updated": "", 5 | "title": "what is m365 copilot?", 6 | "classification": [ 7 | "Pending" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /artifacts/cosmos_examples/cosmos-feedback-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "", 3 | "userId": "", 4 | "prompt": "What is the Cybersecurity Framework? Describe in detail using Introduction, headers, body, bullets, and closing", 5 | "aiResponse": "### Introduction\nThe Cybersecurity Framework (CSF) is a structured approach devised by the National Institute of Standards and Technology (NIST)...", 6 | "feedbackType": "negative", 7 | "reason": "The response was too thorough.", 8 | "timestamp": "", 9 | "adminReview": { 10 | "acknowledged": false, 11 | "analyzedBy": null, 12 | "analysisNotes": null, 13 | "responseToUser": null, 14 | "actionTaken": null, 15 | "reviewTimestamp": null 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /artifacts/cosmos_examples/cosmos-file_processing-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "", 3 | "document_id": "", 4 | "user_id": "", 5 | "log": "Status: Metadata generated for document ", 6 | "timestamp": "" 7 | } 8 | -------------------------------------------------------------------------------- /artifacts/cosmos_examples/cosmos-group_documents-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "", 3 | "file_name": "NanoPZ.pdf", 4 | "num_chunks": 0, 5 | "number_of_pages": 52, 6 | "current_file_chunk": 52, 7 | "num_file_chunks": 1, 8 | "upload_date": "", 9 | "last_updated": "", 10 | "version": 1, 11 | "status": "Processing complete", 12 | "percentage_complete": 100, 13 | "document_classification": "Pending", 14 | "type": "document_metadata", 15 | "group_id": "", 16 | "document_id": "", 17 | "user_id": "", 18 | "title": "NanoPZ", 19 | "authors": [ 20 | "Firstname Lastname" 21 | ], 22 | "enhanced_citations": true, 23 | "organization": "Newport Corporation", 24 | "publication_date": "08/2005", 25 | "keywords": [ 26 | "NanoPZ system", 27 | "piezo motor actuator", 28 | "PZA12", 29 | "PZC200 controller", 30 | "ultra-high resolution motion", 31 | "optical mounts", 32 | "laboratory equipment", 33 | "motion control", 34 | "RS-485 connectivity", 35 | "precision instruments" 36 | ], 37 | "abstract": "The NanoPZ system is an ultra-high resolution motion system designed for precise positioning in laboratory settings, providing nanometer-scale accuracy with its piezo motor actuators and controllers." 38 | } 39 | -------------------------------------------------------------------------------- /artifacts/cosmos_examples/cosmos-group_prompts.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "", 3 | "group_id": "", 4 | "user_id": "", 5 | "name": "Formatted output", 6 | "content": "Please, use the following format to generate your response:\n\n# Title\nTwo sentence abstract\n\n## Header\nDetails\n* bullet points when necessary\n\n## Summary", 7 | "type": "user_prompt", 8 | "created_at": "", 9 | "updated_at": "" 10 | } 11 | -------------------------------------------------------------------------------- /artifacts/cosmos_examples/cosmos-message-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "", 3 | "conversation_id": "", 4 | "role": "assistant", 5 | "content": "Code review can be conducted using an AI-powered framework that scans code for quality and security issues. Here’s a systematic approach...", 6 | "timestamp": "", 7 | "augmented": true, 8 | "hybrid_citations": [ 9 | { 10 | "file_name": "AI-based_VB.NET_Code_Review_Framework.docx", 11 | "citation_id": "", 12 | "page_number": 3, 13 | "chunk_id": "3", 14 | "chunk_sequence": 3, 15 | "score": 0.0322, 16 | "group_id": null, 17 | "version": 1, 18 | "classification": "Pending" 19 | }, 20 | { 21 | "file_name": "AI-based_VB.NET_Code_Review_Framework.docx", 22 | "citation_id": "", 23 | "page_number": 2, 24 | "chunk_id": "2", 25 | "chunk_sequence": 2, 26 | "score": 0.0333, 27 | "group_id": null, 28 | "version": 1, 29 | "classification": "Pending" 30 | }, 31 | { 32 | "file_name": "AI-based_VB.NET_Code_Review_Framework.docx", 33 | "citation_id": "", 34 | "page_number": 1, 35 | "chunk_id": "1", 36 | "chunk_sequence": 1, 37 | "score": 0.0327, 38 | "group_id": null, 39 | "version": 1, 40 | "classification": "Pending" 41 | } 42 | ], 43 | "hybridsearch_query": "how to review code?", 44 | "web_search_citations": [], 45 | "user_message": "how to review code?", 46 | "model_deployment_name": "gpt-4o" 47 | } 48 | -------------------------------------------------------------------------------- /artifacts/cosmos_examples/cosmos-safety-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "", 3 | "user_id": "", 4 | "conversation_id": "", 5 | "message": "", 6 | "triggered_categories": [ 7 | { 8 | "category": "Hate", 9 | "severity": 0 10 | }, 11 | { 12 | "category": "SelfHarm", 13 | "severity": 0 14 | }, 15 | { 16 | "category": "Sexual", 17 | "severity": 0 18 | }, 19 | { 20 | "category": "Violence", 21 | "severity": 4 22 | } 23 | ], 24 | "blocklist_matches": [], 25 | "timestamp": "", 26 | "reason": "Max severity >= 4" 27 | } 28 | -------------------------------------------------------------------------------- /artifacts/cosmos_examples/cosmos-settings-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "app_settings", 3 | "app_title": "Simple Chat", 4 | "landing_page_text": "You agree to our [acceptable user policy](acceptable_use_policy.html) by using this service.", 5 | "show_logo": true, 6 | "logo_path": "images/custom_logo.png", 7 | "enable_gpt_apim": false, 8 | "azure_openai_gpt_endpoint": "", 9 | "azure_openai_gpt_api_version": "2024-12-01-preview", 10 | "azure_openai_gpt_authentication_type": "key", 11 | "azure_openai_gpt_subscription_id": "", 12 | "azure_openai_gpt_resource_group": "", 13 | "azure_openai_gpt_key": "", 14 | "gpt_model": { 15 | "selected": [ 16 | { "deploymentName": "gpt-4o", "modelName": "gpt-4o" } 17 | ], 18 | "all": [ 19 | { "deploymentName": "o1", "modelName": "o1" }, 20 | { "deploymentName": "gpt-4o", "modelName": "gpt-4o" }, 21 | { "deploymentName": "gpt-4", "modelName": "gpt-4" }, 22 | { "deploymentName": "o3-mini", "modelName": "o3-mini" } 23 | ] 24 | }, 25 | "enable_embedding_apim": false, 26 | "azure_openai_embedding_endpoint": "", 27 | "azure_openai_embedding_api_version": "2024-12-01-preview", 28 | "azure_openai_embedding_authentication_type": "key", 29 | "azure_openai_embedding_subscription_id": "", 30 | "azure_openai_embedding_resource_group": "", 31 | "azure_openai_embedding_key": "", 32 | "embedding_model": { 33 | "selected": [ 34 | { "deploymentName": "text-embedding-3-small", "modelName": "text-embedding-3-small" } 35 | ], 36 | "all": [ 37 | { "deploymentName": "text-embedding-3-small", "modelName": "text-embedding-3-small" } 38 | ] 39 | }, 40 | "enable_image_generation": true, 41 | "azure_openai_image_gen_endpoint": "", 42 | "azure_openai_image_gen_api_version": "2024-12-01-preview", 43 | "azure_openai_image_gen_authentication_type": "key", 44 | "azure_openai_image_gen_subscription_id": "", 45 | "azure_openai_image_gen_resource_group": "", 46 | "azure_openai_image_gen_key": "", 47 | "image_gen_model": { 48 | "selected": [ 49 | { "deploymentName": "dall-e-3", "modelName": "dall-e-3" } 50 | ], 51 | "all": [ 52 | { "deploymentName": "dall-e-3", "modelName": "dall-e-3" } 53 | ] 54 | }, 55 | "enable_user_workspace": true, 56 | "enable_group_workspaces": true, 57 | "enable_content_safety": true, 58 | "content_safety_endpoint": "", 59 | "content_safety_key": "", 60 | "content_safety_authentication_type": "key", 61 | "enable_user_feedback": true, 62 | "enable_conversation_archiving": true, 63 | "enable_web_search": true, 64 | "bing_search_key": "", 65 | "azure_ai_search_endpoint": "", 66 | "azure_ai_search_key": "", 67 | "azure_ai_search_authentication_type": "key", 68 | "azure_document_intelligence_endpoint": "", 69 | "azure_document_intelligence_key": "", 70 | "azure_document_intelligence_authentication_type": "key", 71 | "max_file_size_mb": 150, 72 | "conversation_history_limit": 6, 73 | "default_system_prompt": "", 74 | "enable_video_file_support": true, 75 | "enable_audio_file_support": true, 76 | "enable_enhanced_citations": true, 77 | "office_docs_storage_account_url": "", 78 | "office_docs_authentication_type": "key", 79 | "video_files_storage_account_url": "", 80 | "video_files_authentication_type": "key", 81 | "audio_files_storage_account_url": "", 82 | "audio_files_authentication_type": "key", 83 | "enable_extract_meta_data": true, 84 | "enable_file_processing_logs": true, 85 | "enable_document_classification": true, 86 | "document_classification_categories": [ 87 | { "label": "None", "color": "#808080" }, 88 | { "label": "N/A", "color": "#808080" }, 89 | { "label": "Pending", "color": "#79bcfb" }, 90 | { "label": "Public", "color": "#0aa1ff" }, 91 | { "label": "CUI", "color": "#20ce09" }, 92 | { "label": "ITAR", "color": "#d10000" } 93 | ], 94 | "require_member_of_create_group": false, 95 | "require_member_of_safety_violation_admin": false, 96 | "require_member_of_feedback_admin": false, 97 | "version_check": { 98 | "last_checked_datetime": "None", 99 | "latest_release_version": "", 100 | "url": "https://github.com/microsoft/simplechat/releases" 101 | }, 102 | "enable_summarize_content_history_for_search": false, 103 | "enable_summarize_content_history_beyond_conversation_history_limit": false, 104 | "number_of_historical_messages_to_summarize": 10, 105 | "enable_enhanced_citations_mount": false, 106 | "enhanced_citations_mount": "/view_documents", 107 | "default_logo_path": "images/logo.svg", 108 | "enable_public_workspaces": false, 109 | "require_member_of_create_public_workspace": false, 110 | "custom_logo_base64": "", 111 | "logo_version": 9 112 | } 113 | -------------------------------------------------------------------------------- /artifacts/cosmos_examples/cosmos-user_documents-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "", 3 | "file_name": "CISA203.020Guidebook v1.1.pdf", 4 | "num_chunks": 0, 5 | "number_of_pages": 29, 6 | "current_file_chunk": 29, 7 | "num_file_chunks": 1, 8 | "upload_date": "", 9 | "last_updated": "", 10 | "version": 1, 11 | "status": "Processing complete", 12 | "percentage_complete": 100, 13 | "document_classification": "Public", 14 | "type": "document_metadata", 15 | "user_id": "", 16 | "document_id": "", 17 | "title": "CISA TIC 3.0 Program Guidebook v1.1", 18 | "authors": [ 19 | "Cybersecurity and Infrastructure Security Agency" 20 | ], 21 | "enhanced_citations": true, 22 | "organization": "Department of Homeland Security (DHS)", 23 | "publication_date": "07/2021", 24 | "keywords": [ 25 | "Trusted Internet Connections", 26 | "cybersecurity", 27 | "federal networks", 28 | "TIC 3.0", 29 | "CISA guidance", 30 | "cloud adoption", 31 | "network architecture", 32 | "perimeter security", 33 | "federal IT modernization", 34 | "security capabilities" 35 | ], 36 | "abstract": "The CISA TIC 3.0 Program Guidebook outlines updated cybersecurity guidance aiming to enhance security for federal networks by adopting flexible architectures and leveraging modern technologies. It describes key documents, strategic goals, and implementation frameworks to ensure secure and scalable network architectures." 37 | } 38 | -------------------------------------------------------------------------------- /artifacts/cosmos_examples/cosmos-user_prompts-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "", 3 | "user_id": "", 4 | "name": "Formatted output", 5 | "content": "Please, use the following format to generate your response:\n\n# Title\nTwo sentence abstract\n\n## Header\nDetails\n* bullet points when necessary\n\n## Summary", 6 | "type": "user_prompt", 7 | "created_at": "", 8 | "updated_at": "" 9 | } 10 | -------------------------------------------------------------------------------- /artifacts/cosmos_examples/cosmos-user_settings-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "", 3 | "settings": { 4 | "activeGroupOid": "", 5 | "lastUpdated": "", 6 | "splitSizesPreference": [ 7 | 27.72, 8 | 72.28 9 | ], 10 | "layoutPreference": "docked" 11 | }, 12 | "lastUpdated": "" 13 | } 14 | -------------------------------------------------------------------------------- /artifacts/private_endpoints.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/artifacts/private_endpoints.vsdx -------------------------------------------------------------------------------- /images/ChatwithSearchingYourDocsDemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/ChatwithSearchingYourDocsDemo.gif -------------------------------------------------------------------------------- /images/UploadDocumentDemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/UploadDocumentDemo.gif -------------------------------------------------------------------------------- /images/add_role_assignment-job_function.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/add_role_assignment-job_function.png -------------------------------------------------------------------------------- /images/add_role_assignment-select_member-managed_identity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/add_role_assignment-select_member-managed_identity.png -------------------------------------------------------------------------------- /images/add_role_assignment-select_member-service_principal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/add_role_assignment-select_member-service_principal.png -------------------------------------------------------------------------------- /images/admin_settings-enable_and_configure_doc_classification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/admin_settings-enable_and_configure_doc_classification.png -------------------------------------------------------------------------------- /images/admin_settings-enable_audio_file_support.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/admin_settings-enable_audio_file_support.png -------------------------------------------------------------------------------- /images/admin_settings-enable_conversation_archiving.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/admin_settings-enable_conversation_archiving.png -------------------------------------------------------------------------------- /images/admin_settings-enable_enhanced_citations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/admin_settings-enable_enhanced_citations.png -------------------------------------------------------------------------------- /images/admin_settings-enable_file_processing_logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/admin_settings-enable_file_processing_logs.png -------------------------------------------------------------------------------- /images/admin_settings-enable_groups.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/admin_settings-enable_groups.png -------------------------------------------------------------------------------- /images/admin_settings-enable_user_feedback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/admin_settings-enable_user_feedback.png -------------------------------------------------------------------------------- /images/admin_settings-enable_video_file_support.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/admin_settings-enable_video_file_support.png -------------------------------------------------------------------------------- /images/admin_settings-enable_workspace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/admin_settings-enable_workspace.png -------------------------------------------------------------------------------- /images/admin_settings-upgrade_available_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/admin_settings-upgrade_available_notification.png -------------------------------------------------------------------------------- /images/admin_settings_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/admin_settings_page.png -------------------------------------------------------------------------------- /images/advanced_edit_env.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/advanced_edit_env.png -------------------------------------------------------------------------------- /images/ai_search-missing_index_fields.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/ai_search-missing_index_fields.png -------------------------------------------------------------------------------- /images/app_reg-api_permissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/app_reg-api_permissions.png -------------------------------------------------------------------------------- /images/app_reg-app_role-create_group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/app_reg-app_role-create_group.png -------------------------------------------------------------------------------- /images/app_reg-app_roles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/app_reg-app_roles.png -------------------------------------------------------------------------------- /images/app_reg-authentication.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/app_reg-authentication.png -------------------------------------------------------------------------------- /images/app_reg_edit_identity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/app_reg_edit_identity.png -------------------------------------------------------------------------------- /images/app_reg_secrets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/app_reg_secrets.png -------------------------------------------------------------------------------- /images/app_reg_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/app_reg_settings.png -------------------------------------------------------------------------------- /images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/architecture.png -------------------------------------------------------------------------------- /images/chat-classification_propagation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/chat-classification_propagation.png -------------------------------------------------------------------------------- /images/chat-delete_conversation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/chat-delete_conversation.png -------------------------------------------------------------------------------- /images/chat-feedback-negative.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/chat-feedback-negative.png -------------------------------------------------------------------------------- /images/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/chat.png -------------------------------------------------------------------------------- /images/clone_the_repo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/clone_the_repo.png -------------------------------------------------------------------------------- /images/content_safety-cosmos_container.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/content_safety-cosmos_container.png -------------------------------------------------------------------------------- /images/content_safety-in_action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/content_safety-in_action.png -------------------------------------------------------------------------------- /images/content_safety-management.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/content_safety-management.png -------------------------------------------------------------------------------- /images/content_safety-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/content_safety-settings.png -------------------------------------------------------------------------------- /images/content_safety-taking_action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/content_safety-taking_action.png -------------------------------------------------------------------------------- /images/content_safety-user_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/content_safety-user_view.png -------------------------------------------------------------------------------- /images/cosmos_container-view_archived_conversation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/cosmos_container-view_archived_conversation.png -------------------------------------------------------------------------------- /images/cosmos_container-view_archived_messages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/cosmos_container-view_archived_messages.png -------------------------------------------------------------------------------- /images/cosmos_container-view_specific_doc_file_processing_logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/cosmos_container-view_specific_doc_file_processing_logs.png -------------------------------------------------------------------------------- /images/cross_tenant-model_support.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/cross_tenant-model_support.png -------------------------------------------------------------------------------- /images/download_remote_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/download_remote_settings.png -------------------------------------------------------------------------------- /images/enable_managed_identity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/enable_managed_identity.png -------------------------------------------------------------------------------- /images/enterprise_app-add_user_to_role.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/enterprise_app-add_user_to_role.png -------------------------------------------------------------------------------- /images/feedback_review-list_all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/feedback_review-list_all.png -------------------------------------------------------------------------------- /images/feedback_review-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/feedback_review-workflow.png -------------------------------------------------------------------------------- /images/files_to_zip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/files_to_zip.png -------------------------------------------------------------------------------- /images/group_workspace-doc_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/group_workspace-doc_list.png -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/icon.png -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/logo.png -------------------------------------------------------------------------------- /images/logo_larger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/logo_larger.png -------------------------------------------------------------------------------- /images/manage_group-add_member.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/manage_group-add_member.png -------------------------------------------------------------------------------- /images/manage_group-group_details_as_owner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/manage_group-group_details_as_owner.png -------------------------------------------------------------------------------- /images/manage_group-update_member_role.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/manage_group-update_member_role.png -------------------------------------------------------------------------------- /images/my_feedback-list_all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/my_feedback-list_all.png -------------------------------------------------------------------------------- /images/my_feedback-view_specific.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/my_feedback-view_specific.png -------------------------------------------------------------------------------- /images/my_groups-find_group-request_to_join.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/my_groups-find_group-request_to_join.png -------------------------------------------------------------------------------- /images/my_groups-group_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/my_groups-group_list.png -------------------------------------------------------------------------------- /images/scale-cosmos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/scale-cosmos.png -------------------------------------------------------------------------------- /images/storage_account-view_doc_for_citation_retrieval.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/storage_account-view_doc_for_citation_retrieval.png -------------------------------------------------------------------------------- /images/upload_local_settings_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/upload_local_settings_1.png -------------------------------------------------------------------------------- /images/upload_local_settings_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/upload_local_settings_2.png -------------------------------------------------------------------------------- /images/visit_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/visit_app.png -------------------------------------------------------------------------------- /images/workflow-add_your_data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/workflow-add_your_data.png -------------------------------------------------------------------------------- /images/workflow-content_safety.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/workflow-content_safety.png -------------------------------------------------------------------------------- /images/workflow-upload_process_audio_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/workflow-upload_process_audio_file.png -------------------------------------------------------------------------------- /images/workflow-upload_video_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/workflow-upload_video_file.png -------------------------------------------------------------------------------- /images/workflow-view_and_update_classification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/workflow-view_and_update_classification.png -------------------------------------------------------------------------------- /images/workspace-doc_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/workspace-doc_list.png -------------------------------------------------------------------------------- /images/workspace-enhanced_citation_tag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/workspace-enhanced_citation_tag.png -------------------------------------------------------------------------------- /images/workspace-prompt_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/workspace-prompt_edit.png -------------------------------------------------------------------------------- /images/workspace-prompt_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/workspace-prompt_list.png -------------------------------------------------------------------------------- /images/zip_the_files.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/simplechat/6f081549a54922a5296f8fd2647e4a5195b722ad/images/zip_the_files.png --------------------------------------------------------------------------------